Rewrite the README task-first for 1.0.0 and move the 0.4.0 guide to MIGRATION.md (#102)
Test / lint (push) Successful in 20s
Test / test (18) (push) Successful in 18s
Test / test (20) (push) Successful in 18s
Test / test (22) (push) Successful in 18s
Test / test (24) (push) Successful in 24s
Test / test (26) (push) Successful in 18s

This commit is contained in:
2026-09-13 12:05:13 +02:00
committed by GitHub
parent 549b3260c9
commit 7310634d7d
4 changed files with 542 additions and 527 deletions
+6 -4
View File
@@ -135,16 +135,16 @@ docker compose run --rm node npm run build
- `erasableSyntaxOnly` is on, so no enums, no namespaces, no parameter properties. Use `as const` - `erasableSyntaxOnly` is on, so no enums, no namespaces, no parameter properties. Use `as const`
objects plus union types. objects plus union types.
- The published floor is Node 18, but the dev container runs Node 24 (type stripping needs it). CI - 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. compiles the tests and runs them on 18, every LTS above it, and current, 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` is pinned to the 6.x line because `typescript-eslint` peer-requires `<6.1.0`. Move to
TypeScript 7 once that constraint lifts. TypeScript 7 once that constraint lifts.
## Defects found in 0.4.0 ## Defects found in 0.4.0
Every row names what 0.4.0's own code did, so it is not rebuilt here. Every row names what 0.4.0's own code did, so it is not rebuilt here.
[README.md](README.md#behaviour-that-changed-on-the-wire) names what changed for a consumer, and is [MIGRATION.md](MIGRATION.md) names what changed for a consumer, and is the only place that does.
the only place that does. Confirmed by reading the 0.4.0 source; each row has a regression test Confirmed by reading the 0.4.0 source; each row has a regression test naming the behaviour.
naming the behaviour.
| Defect | 0.4.0 behaviour | | Defect | 0.4.0 behaviour |
| --- | --- | | --- | --- |
@@ -235,6 +235,8 @@ Each file answers one question, and a fact belongs to the file whose question it
- **README.md — what you can rely on.** Observable behaviour, for someone using the package. It - **README.md — what you can rely on.** Observable behaviour, for someone using the package. It
carries a reason only where the reason changes how you would call the thing. carries a reason only where the reason changes how you would call the thing.
- **MIGRATION.md — what a 0.4.0 consumer has to change.** Renamed and removed surface, and the
behaviour that changed on the wire.
- **AGENTS.md — what may not change, and why.** Goals, hard rules, architecture, conventions, and the - **AGENTS.md — what may not change, and why.** Goals, hard rules, architecture, conventions, and the
decisions the goals do not already settle. It does not restate behaviour README states. decisions the goals do not already settle. It does not restate behaviour README states.
- **todo.md** is a temporary working file that sets its own rules; nothing here governs it. - **todo.md** is a temporary working file that sets its own rules; nothing here governs it.
+93
View File
@@ -0,0 +1,93 @@
# Migrating from larvitsmpp 0.4.0
`@larvit/smpp` 1.0.0 succeeds [larvitsmpp](https://www.npmjs.com/package/larvitsmpp) 0.4.0. The
shape is the same, connect, send, listen for delivery reports, with callbacks replaced by promises.
## API changes
- **The package is `@larvit/smpp`** and ESM only. `require()` no longer works.
- **Callbacks are gone.** `client`, `server`, `sendSms`, `sendResp`, `sendDlr`, `unbind` and
`session.close` are promises resolving to a result with an optional `err`. Nothing rejects. Await
`close()`, or the socket outlives the call.
- **`server()` resolves once, when it is listening**, with a handle carrying `close()`, `port` and
a `session` event. It no longer calls back once per connection.
- **The id a message is answered with goes to `sendResp({ smsId })`.** `sms.smsId` is read-only: the
id the segments were answered with, the id `sendResp()` was given, or the generated UUID v7.
Assigning to it throws a `TypeError`, since modules are strict mode.
- **`smsIds` from `sendSms()` is `(string | undefined)[]`**, one entry per segment, positional with
`pduObjs`, `undefined` where the SMSC took the segment without naming an id.
- **`checkuserpass` is `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
codec is synchronous and returns `{ err, pduObj }` / `{ err, buffer }`.
- **`pduObj.isResp()` is the standalone `isResp(pduObj)`.** `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 and never invoked. SMPP time
formatting, the one part worth keeping, is `smppTime`.
- **`DATAGRAM`, `FORWARD` and `STORE_FORWARD` moved from `consts.ESM_CLASS` to
`consts.MESSAGING_MODE`**, which also names `SMSC_DEFAULT`. They are bits 1-0 of `esm_class`, not
whole values of it. Read them from the new group, or pass `messagingMode` to `sendSms()`. A stale
`consts.ESM_CLASS.STORE_FORWARD` reads `undefined`, which OR-s into an `esm_class` carrying no mode.
- **The `error` event is `sessionError`**, and `serverError` on the server handle.
- **`log`** takes any object with `debug`, `error`, `info`, `verbose` and `warn` methods instead of
a `larvitutils` one, and is silent by default: [README](README.md#logging).
- **`consts.ENCODING.ASCII` is gone**; the same entry is `consts.ENCODING.IA5`, the other name SMPP
3.4 5.2.19 gives 0x01. `dataCodingByEncoding` is the alphabet `sendSms()` writes, which is 0x00.
## Behaviour that changed on the wire
0.4.0 had protocol defects. Fixing them changes the bytes on the wire, so remove any workaround you
have for these:
- Every multipart segment was one character short (152 GSM characters instead of 153, 66 UCS2
instead of 67), so long messages were split into more segments than necessary, each one billed.
- 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`.
- Every receipt went out as `esm_class` 0x04, the report of a message's final state. A receipt for a
transient state, `sendDlr('ENROUTE')`, is now marked 0x20, the intermediate delivery notification.
- `flash: true` discarded UCS2, mangling flash messages with non-GSM characters, and put the GSM
alphabet on a Latin-1 message that has no `data_coding` at all; that pair is refused now. Inbound,
only a `data_coding` of exactly 0x10 counted as flash, so a flash UCS2 message and the whole 0xF0
coding group arrived as ordinary messages.
- A GSM 03.38 message declared `data_coding` 0x01, which SMPP 3.4 5.2.19 defines as IA5, so `$` and
`@` reached a peer honouring the field as STX and NUL. It goes out as 0x00, the SMSC default
alphabet, and so does a receipt `sendDlr()` writes. Latin-1 and UCS2 stay at 0x03 and 0x08, and an
inbound 0x01 is still read as GSM 03.38.
- The multipart reference counter was shared by every session in the process.
- `tls: true` never performed a handshake, so the connection was not 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 parsed now.
- A message whose last octet was `0x00` was allocated one octet short while `sm_length` reported the
full length, so it went out corrupt. In UCS2 that is any message ending in a character like 一
(U+4E00), routine for CJK text.
- Every response carried a `message_id`, `deliver_sm_resp` included, where SMPP 3.4 4.6.2 makes that
field unused and NULL. Jasmin closes the connection on one. Answering an inbound message now puts
nothing in it, and `sms.smsId` is the local handle it always was.
- Binary TLVs (`message_payload`, `network_error_code`, `callback_num` and the rest) were parsed into
a hex string and written back as the ASCII of that string, so every round trip corrupted them.
They are `Buffer`s in both directions now; drop any hex encoding of your own.
- A body carried in the `message_payload` TLV was ignored, so the message arrived empty, and a
`data_sm` was answered `ESME_RINVCMDID`, so a receipt thrown on one was lost silently. Both reach
the application now: a receipt as `dlr`, answered for you, and a message as `sms` for you to answer.
- A long message segmented by the `sar_msg_ref_num`, `sar_total_segments` and `sar_segment_seqnum`
TLVs rather than a user data header was never reassembled, so each segment arrived as its own
message. Both spellings reassemble now.
- Short or malformed PDUs threw out of the codec instead of being reported as a parse failure.
- A PDU whose optional parameters do not end exactly on `command_length` is refused with
`ESME_RINVTLVSTREAM` and dropped, where 0.4.0 kept the TLVs it had read and ignored the octets
left over, losing the `receipted_message_id` that makes a receipt a receipt. The refusal reaches
`sessionError` as a `PduRefusedError` with `reason` `tlvs`.
- Binds declare `interface_version` 0x34. 0.4.0 declared 0x00, which tells the SMSC the ESME speaks
SMPP 3.3 or earlier, and a spec-following SMSC then withholds every optional parameter, the TLVs
delivery receipts are carried in included.
- A response reporting a failure carries no body, as the spec defines. 0.4.0 filled the body with
empty defaults, so a refused `submit_sm_resp` went out with an empty `message_id` a caller could
mistake for a real one.
- `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.
+442 -522
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@larvit/smpp", "name": "@larvit/smpp",
"version": "1.0.0", "version": "1.0.0",
"description": "Simplified SMPP implementation", "description": "SMPP 3.4 client and server for Node.js with the session layer built in: keepalive, reconnect, send window, long messages and delivery receipts",
"keywords": [ "keywords": [
"esm", "esm",
"pdu", "pdu",