From 54b73205ac4ecd560535ab16e6f163d200ab1d1c Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Tue, 25 Aug 2026 17:02:57 +0200 Subject: [PATCH] Document the finished API and make the test scripts portable across Node versions --- AGENTS.md | 16 ++++ README.md | 118 ++++++++++++++++++++---- package.json | 4 +- todo.md | 246 +++++++++++---------------------------------------- 4 files changed, 173 insertions(+), 211 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 73ec054..9b95af9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -102,6 +102,18 @@ implementation (see todo.md). | Dormant filters | `defs.filters` is declared on commands and TLVs but never invoked anywhere. Dropped in the rewrite; SMPP time formatting is exported as `smppTime` instead | | Unchecked reads | Wire reads index straight into the buffer, so a short or malformed PDU throws out of the codec. Reads are bounds-checked and return results now | | Unrangechecked writes | Integer params are handed to `writeUInt8`/`writeUInt16BE` unvalidated, so an out-of-range value throws from inside Node | +| `submit_multi` missing `sm_length` | The field is commented out of the command table, so `short_message` never round-trips for that command | +| Per-parameter defaults never applied | `calcCmdLength` reads `paramType.default` (the wire type's) rather than the parameter's, so `interface_version: 0x50` on the bind commands did nothing and every bind declared version 0x00 | +| `ESME_RINVBCASTCHANIND` typo | Defined as `0x011`, three hex digits; the spec value is `0x0112` | + +## Multipart sends and the send window + +`sendSms` puts every segment of a message on the wire together instead of waiting for each response +in turn. This is not an optimisation: this library's own server holds segments until the whole +message is reassembled before it answers any of them, so sending them one-after-a-response +deadlocks. It follows that a message with more segments than `maxOutstanding` cannot be delivered to +a server that defers responses that way — real SMSCs answer each `submit_sm` immediately, so this +only bites when both ends are this library. ## GSM 7-bit is sent unpacked @@ -123,3 +135,7 @@ exactly 140. 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. +- A test that needs a dummy peer must `resume()` its sockets. An unread socket never processes the + peer's FIN, so `server.close()` hangs forever — that is a test bug, not a library one. +- `assert.equal` from `node:assert/strict` narrows its first argument, so a following `?.` on the + same value is flagged as unnecessary. Assert once with `assert.ok(x)` and use plain access after. diff --git a/README.md b/README.md index e83594e..416d7ba 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,13 @@ Successor to [larvitsmpp](https://www.npmjs.com/package/larvitsmpp) 0.4.0. The A 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. +> **Not published yet.** The implementation is complete and tested, but 1.0.0 has not been released +> to npm. Until it is, use `larvitsmpp` 0.4.0. Remaining release steps are in [todo.md](todo.md). ## Requirements -Node 18 or later. +Node 18 or later. The only runtime dependency is +[`@larvit/log`](https://www.npmjs.com/package/@larvit/log). ## Install @@ -68,6 +68,46 @@ const { err: sendErr, smsIds } = await session.sendSms({ }); ``` +### Client options + +Every one is optional. + +| Option | Default | | +| --- | --- | --- | +| `host` / `port` | `localhost` / `2775` | Where to connect. | +| `username` / `password` | `user` / `pass` | Bind credentials (`system_id` and `password`). | +| `bindType` | `transceiver` | `transceiver`, `transmitter` or `receiver`. | +| `systemType`, `addressRange`, `addrTon`, `addrNpi`, `interfaceVersion` | `''`, `''`, `0`, `0`, `0x50` | The remaining bind fields, for operators that require them. | +| `tls` | `false` | `true` for defaults, or a `tls.ConnectionOptions` object for a private CA or a client certificate. | +| `enquireLinkInterval` | `20000` | How often to send `enquire_link` on a quiet link. | +| `responseTimeout` | `30000` | How long to wait for a response before giving up on it. | +| `maxOutstanding` | `10` | Requests allowed on the wire at once; further sends queue. | +| `reconnect` | off | `{ minDelay, maxDelay }` to re-bind automatically after a drop, with exponential backoff. | +| `log` | silent | A `@larvit/log` instance. | +| `signal` | — | An `AbortSignal` that cancels connecting and tears the session down. | + +### Sending + +```javascript +await session.sendSms({ + dlr: true, // ask for a delivery report + encoding: 'UCS2', // override the automatic choice + flash: false, + from: 'MyBrand', // alphanumeric -> TON 5, digits -> TON 1 + message: 'Hello world', + scheduleDeliveryTime: new Date(Date.now() + 3600_000), + to: '46709771337', + validityPeriod: 3600, // seconds, or a Date +}, { signal }); // optional per-call AbortSignal +``` + +Messages too long for one SMS are split automatically and sent as a concatenated message. You get +one id per segment: + +```javascript +const { err, pduObjs, smsIds } = await session.sendSms({ from, message, to }); +``` + ## Server The simplest possible server — no authentication, listening on port 2775: @@ -112,12 +152,25 @@ smpp.on('session', session => { }); }); -await smpp.close(); +console.log(smpp.port); // the port actually bound, useful when 0 was requested +await smpp.close(); // stop listening and close every live session ``` `sendDlr` accepts `SCHEDULED`, `ENROUTE`, `DELIVERED`, `EXPIRED`, `DELETED`, `UNDELIVERABLE`, `ACCEPTED`, `UNKNOWN`, `REJECTED` and `SKIPPED`. +### Server options + +| Option | Default | | +| --- | --- | --- | +| `host` / `port` | all interfaces / `2775` | Where to listen. Pass `0` for any free port. | +| `authenticate` | accept everything | `({ password, session, systemId, systemType }) => false \| { userData }`, sync or async. | +| `tls` | `false` | `true`, or a `tls.TlsOptions` object with your certificate and key. | +| `idleTimeout` | `40000` | Drop a peer that has been silent this long. | +| `maxReassembly` | `1000` | Incomplete multipart messages held per session. | +| `reassemblyTimeout` | `300000` | How long an incomplete multipart message is held. | +| `responseTimeout`, `maxOutstanding`, `log`, `signal` | as for the client | | + ## Errors Nothing in this library throws. Every fallible call returns a result carrying an optional `err`, so @@ -140,10 +193,10 @@ is exactly what this library promises not to do. | Event | Fires when | | --- | --- | -| `sms` | An SMS arrives. Carries `sendResp()` and `sendDlr()`. | +| `sms` | An SMS arrives, reassembled if it was multipart. 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. | +| `close` | The connection 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. | @@ -153,22 +206,48 @@ is exactly what this library promises not to do. ### 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. +commands the codec knows, not just the four the session handles natively: + +```javascript +const { err, pduObj } = await session.send({ + cmdName: 'query_sm', + params: { message_id: smsId }, +}); +``` + +## Working with PDUs directly + +The codec is exported, synchronous, and never throws — handy for inspecting captured traffic: + +```javascript +import { isCommand, objToPdu, pduToObj } from '@larvit/smpp'; + +const { err, pduObj } = pduToObj(buffer); +if (err) return; + +if (isCommand(pduObj, 'submit_sm')) { + pduObj.params.destination_addr; // typed as a string +} +``` + +The spec tables are exported both individually (`cmds`, `consts`, `encodings`, `errors`, `tlvs`, +`types`, and the matching `*ById` maps) and grouped as `defs`. ## 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 }`. +- **`server()` resolves once, when it is listening**, and gives you a handle with `close()`, `port` + and a `session` event. It no longer calls your callback once per incoming connection. +- **`checkuserpass` is now `authenticate`**, takes `{ password, session, systemId, systemType }` 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`, `smppTime`, `splitMessage`. The PDU codec is - synchronous and returns `{ err, pduObj }` / `{ err, buffer }`. -- **`pduObj.isResp()` is now the standalone `isResp(pduObj)`.** + `encodeMessage`, `objToPdu`, `pduReturn`, `pduToObj`, `smppDate`, `smppTime`, `splitMessage`. The + PDU codec is synchronous and returns `{ err, pduObj }` / `{ err, buffer }`. +- **`pduObj.isResp()` is now the standalone `isResp(pduObj)`**, and `pduObj.cmdStatus` is `undefined` + for a status code the library does not know, with the raw number in `pduObj.cmdStatusId`. - **`defs.filters` is gone.** It was declared on every command and TLV but never invoked, so it did nothing. SMPP time formatting, the one part worth keeping, is exported as `smppTime`. - **The `error` event is `sessionError`** (and `serverError` on the server handle). @@ -196,6 +275,12 @@ have worked around any of these, remove the workaround: reported the full length, so it went out corrupt. In UCS2 that is any message ending in a character like 一 (U+4E00), which made the bug routine for CJK text. - Short or malformed PDUs threw out of the codec instead of being reported as a parse failure. +- Binds now declare `interface_version` 0x50. 0.4.0 declared 0x00, because the default in its own + command table was never applied. Pass `interfaceVersion: 0x34` to bind as SMPP 3.4. +- `submit_multi` was missing its `sm_length` field, so its `short_message` never round-tripped. + +The corrected framing is cross-checked against [node-smpp](https://github.com/farhadi/node-smpp), an +independent implementation, in both directions and over a live session. ## Development @@ -203,8 +288,9 @@ 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 test # lint, typecheck and tests docker compose run --rm node npm run build +docker compose run --rm node npm run test:compiled # what CI runs on older Node versions ``` Tests are TypeScript and run directly under Node's type stripping, so there is no build step in the diff --git a/package.json b/package.json index 68417cf..37f0c4f 100644 --- a/package.json +++ b/package.json @@ -44,8 +44,8 @@ "build": "tsc --project tsconfig.build.json", "lint": "eslint . && tsc --noEmit", "prepack": "npm run build", - "test": "npm run lint && node --test 'test/**/*.test.ts'", - "test:compiled": "tsc --project tsconfig.test.json && node --test dist-test/test/" + "test": "npm run lint && node --test test/*.test.ts", + "test:compiled": "tsc --project tsconfig.test.json && node --test dist-test/test/*.test.js" }, "devDependencies": { "@eslint/js": "10.0.1", diff --git a/todo.md b/todo.md index 5b9bece..8c63316 100644 --- a/todo.md +++ b/todo.md @@ -3,226 +3,86 @@ 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 +## Status + +The rewrite is **feature complete and green**: 136 tests, lint and typecheck clean, verified on Node +18, 20, 22 and 24. What is left is release work and a few things worth adding before or after 1.0.0. ```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. +Settled with the maintainer before implementation. Do not change any of it without asking. The +public surface is documented in [README.md](README.md); this is the short form. ```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 +import { client, server } from '@larvit/smpp'; +const { err, session } = await client({ host, password, port, username }); +const { err, pduObjs, smsIds } = await session.sendSms({ dlr, from, message, to }); 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(); +const { err, server: smpp } = await server({ authenticate, port }); 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 + await sms.sendResp(); + if (sms.dlr) await sms.sendDlr('DELIVERED'); }); }); +await smpp.close(); ``` -### 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 +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`, `smppTime`, `splitMessage`. -- **`defs.filters` is gone** — it never did anything. `smppTime` replaces the one useful part. +- **Named exports only**, no default export. `defs` is exported as a group alongside the individual + tables. - **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. + `session.sendReturn()`. -## Tasks +## Done -### 1. Definition tables — `src/defs/` +| | Covered by | +| --- | --- | +| Definition tables: constants, errors, encodings, wire types, TLVs, commands | `test/encodings.test.ts`, `test/types.test.ts`, `test/commands.test.ts` | +| Message helpers: splitting, bit counting, SMPP dates and times | `test/message.test.ts` | +| PDU codec: parse, build, respond, per-command typing, bounds checks | `test/pdu.test.ts` | +| Stream framing | `test/pdu-framer.test.ts` | +| Delivery receipt parsing, TLV and text | `test/dlr.test.ts` | +| Session, client, server: bind, auth, send, reassembly, DLRs, timeouts, abort, send window | `test/session.test.ts` | +| Merged multipart DLRs, reconnect, reassembly bounds, per-send abort | `test/session-extras.test.ts` | +| Cross-checked against node-smpp both ways and over a live session | `test/interop.test.ts` | +| CI on Node 18/20/22/24, Renovate, tag-triggered publish | `.github/workflows/` | -All done, with tests in `test/encodings.test.ts`, `test/types.test.ts` and `test/commands.test.ts`. +Every defect listed in the AGENTS.md table has a regression test naming the behaviour. -- [x] `constants.ts` — `consts` + `constsById`. -- [x] `errors.ts` — all `ESME_*` codes plus `errorsById`, `isErrorName`, `errorNameById`. -- [x] `types.ts` — wire types. `read` is bounds-checked and reports `bytesRead`, so no caller has to - re-derive a length that could disagree with what was written. `size` and `write` validate their - input and return results. -- [x] `encodings.ts` — GSM 03.38, LATIN1, UCS2, FLASH, `detect` and `encodingByDataCoding`. - `iconv-lite` is gone; Buffer does it natively. -- [x] `tlvs.ts` — 67 TLV definitions, `tlvsById`, and the two aliases. -- [x] `commands.ts` — all 33 commands, wire-ordered, plus `cmdsById` and the `PduParams` / - `PduParamsInput` per-command types. -- [x] `filters.ts` — **not ported.** `defs.filters` was declared on commands and TLVs but never - invoked anywhere in 0.4.0. The one piece that is genuinely needed, SMPP time formatting, is - task 2's `smppTime`. +## Before publishing 1.0.0 -### 2. Message helpers — `src/message.ts` +- [ ] **Decide the `interface_version` default.** Binds now declare 0x50, which is what 0.4.0's own + command table intended but never applied — it actually sent 0x00. 0x34 (SMPP 3.4) is the more + conservative choice and is what most SMSCs implement. Flagged to the maintainer; the README + documents 0x50 and the `interfaceVersion` option overrides it either way. +- [ ] Create the `@larvit/smpp` package on npm and add `NPM_TOKEN` to the repository secrets, which + `.github/workflows/release.yaml` needs. +- [ ] Tag `v1.0.0` to publish. +- [ ] `npm deprecate larvitsmpp` pointing at `@larvit/smpp`. Maintainer's call to run it; not + something CI should do. +- [ ] Decide what happens to `master`: this branch is an orphan, so merging it is a deliberate act. -Done, tested in `test/message.test.ts`. Segments are 153 GSM / 67 UCS2 characters, `smppDate` is -UTC and one-based, `decodeMessage` goes through `encodingByDataCoding`, and `splitMessage` takes the -concatenation reference as an argument rather than owning a module-global counter. +## Worth doing, not blocking -- [x] `bitCount`, `encodeMessage`, `decodeMessage`, `smppDate`, `splitMessage`, `smppTime`. - -### 3. PDU codec — `src/pdu.ts` - -Done, tested in `test/pdu.test.ts` — which includes the two byte-for-byte comparisons against 0.4.0's -output and the real captured SMSC PDUs from its suite. - -- [x] `pduToObj`, `objToPdu`, `pduReturn`, `isResp`, `isCommand`. -- [x] Trailing-NULL retry kept, now an explicit second parse rather than a side effect of a length - function. -- [x] `cmdLength` guarded by `maxPduLength` (1 MiB) before anything is allocated. -- [x] `objToPdu` narrows `params` to the named command. `pduToObj` returns loosely-typed params — - the command is only known at runtime — and `isCommand(pduObj, 'submit_sm')` narrows them. - -The encoder measures each field, allocates exactly that, and writes into it, so a `size`/`write` -disagreement surfaces as an error instead of a corrupt PDU. That class of bug is what the trailing -NULL defect was. - -### 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. +- [ ] **In-flight sends across a reconnect.** They currently fail with "Session closed before a + response arrived" and the caller retries. Re-queueing them automatically would be friendlier + but risks duplicate delivery, so it needs a decision before it is built. +- [ ] **TLS is untested.** The code path is right (`tls.connect` / `tls.createServer`, options + passed through) but no test exercises a handshake. Needs a self-signed certificate fixture. +- [ ] **`submit_multi` and the broadcast commands** encode and decode, but nothing exercises them + end to end. The interop suite is the natural place. +- [ ] **Move to TypeScript 7** once `typescript-eslint` supports it; `renovate.json` pins TypeScript + below 6.1 for exactly that reason. +- [ ] **Coverage reporting.** `node --test --experimental-test-coverage` works today; nothing + publishes the numbers.