Open every todo item with a bold one-sentence tagline #31

Merged
lilleman merged 1 commits from todo-taglines into main 2026-09-27 23:50:07 +02:00
+200 -183
View File
@@ -78,31 +78,34 @@ Every defect listed in the AGENTS.md table has a regression test naming the beha
`gitea.larvit.se/larvit/smpp-js` is the repository. `github.com/larvit/smpp-js` mirrors it and is the `gitea.larvit.se/larvit/smpp-js` is the repository. `github.com/larvit/smpp-js` mirrors it and is the
place issues are filed. Maintainer's calls, 2026-09-13 and 2026-09-14. place issues are filed. Maintainer's calls, 2026-09-13 and 2026-09-14.
- [x] `larvit/smpp-js` holds `main`, from `typescript`, and `v0.4.0`, from `master`. `rewrite-base` - [x] **Move `main` and `v0.4.0` to `larvit/smpp-js`.** It holds `main`, from `typescript`, and
and the `renovate/*` branches stayed behind. `v0.4.0`, from `master`. `rewrite-base` and the `renovate/*` branches stayed behind.
- [x] Fast-forward is the only merge style. `main` takes no pushes, requires `Test / lint - [x] **Protect `main` with fast-forward merges and required checks.** Fast-forward is the only
(pull_request)` and `Test / test (*) (pull_request)`, blocks an outdated branch, and gives merge style. `main` takes no pushes, requires `Test / lint (pull_request)` and `Test / test
admins no override. Every other branch takes force pushes. (*) (pull_request)`, blocks an outdated branch, and gives admins no override. Every other
- [x] The workflows are in `.gitea/workflows/`. Tests run on pull requests only, the event the gate branch takes force pushes.
reads; Renovate runs as a scheduled workflow, as on adf-codec. - [x] **Run the workflows from `.gitea/workflows/`.** Tests run on pull requests only, the event the
- [x] The release publishes without provenance, which npm generates only on GitHub Actions and gate reads; Renovate runs as a scheduled workflow, as on adf-codec.
GitLab CI/CD. - [x] **Publish the release without provenance.** npm generates it only on GitHub Actions and GitLab
- [x] `package.json` names Gitea, and the GitHub mirror's issues as `bugs`. The README links CI/CD.
- [x] **Name Gitea in `package.json`, and the GitHub mirror's issues as `bugs`.** The README links
absolutely: npmjs.com resolves a relative link against itself when the `repository` is not on absolutely: npmjs.com resolves a relative link against itself when the `repository` is not on
GitHub. Its test badge is gone, since Gitea reports a workflow's status per branch and no GitHub. Its test badge is gone, since Gitea reports a workflow's status per branch and no
workflow runs on `main`. workflow runs on `main`.
- `RENOVATE_GITHUB_TOKEN` exists nowhere, so Renovate queries github.com unauthenticated, as - **Add a github.com token for Renovate only if its lookups hit the rate limit.**
`RENOVATE_GITHUB_TOKEN` exists nowhere, so Renovate queries github.com unauthenticated, as
adf-codec's nightly run already does without a warning. `RENOVATE_TOKEN` is the Gitea token and adf-codec's nightly run already does without a warning. `RENOVATE_TOKEN` is the Gitea token and
cannot stand in for it. Add a github.com token only if lookups hit the rate limit. cannot stand in for it.
## Before publishing 0.5.0 ## Before publishing 0.5.0
- [x] 0.5.0 rather than 1.0.0, while usage is this low. Maintainer's call, 2026-09-14. - [x] **Publish the rewrite as 0.5.0, not 1.0.0, while usage is this low.** Maintainer's call,
- [x] `NPM_TOKEN`, which `.gitea/workflows/release.yaml` needs, is a Gitea organization secret. 2026-09-14.
- [x] Tag `v0.5.0` on Gitea to publish. The first publish creates `@larvit/smpp` on npm, provided the - [x] **Hold `NPM_TOKEN` as a Gitea organization secret.** `.gitea/workflows/release.yaml` needs it.
token can publish under `@larvit`. - [x] **Tag `v0.5.0` on Gitea to publish.** The first publish creates `@larvit/smpp` on npm,
- [x] `npm deprecate larvitsmpp` pointing at `@larvit/smpp`. Maintainer's call to run it; not provided the token can publish under `@larvit`.
something CI should do. - [x] **Deprecate `larvitsmpp` on npm, pointing at `@larvit/smpp`.** `npm deprecate larvitsmpp` is
the maintainer's call to run; not something CI should do.
## Retire the GitHub repository ## Retire the GitHub repository
@@ -110,52 +113,54 @@ Nothing here starts before 0.5.0 is published. Maintainer's call, 2026-09-14. Th
deleting GitHub's old branches closes every pull request based on them without a reply, and GitHub deleting GitHub's old branches closes every pull request based on them without a reply, and GitHub
refuses to delete its default branch. refuses to delete its default branch.
- [x] Close the backlog below. - [x] **Close the backlog below.**
- [x] Close [#71](https://github.com/larvit/larvitsmpp/pull/71), pointing at Gitea. - [x] **Close [#71](https://github.com/larvit/larvitsmpp/pull/71), pointing at Gitea.**
- [x] Rename `larvit/larvitsmpp` to `larvit/smpp-js`. GitHub redirects the old URLs, and the `bugs` - [x] **Rename `larvit/larvitsmpp` to `larvit/smpp-js`.** GitHub redirects the old URLs, and the
URL in `package.json` resolves from then on. `bugs` URL in `package.json` resolves from then on.
- [x] Push `main` and make it GitHub's default branch. - [x] **Push `main` and make it GitHub's default branch.**
- [x] Renovate is Silent for this repository in the Mend Developer Portal, so it opens nothing on - [x] **Set Renovate to Silent for this repository in the Mend Developer Portal.** It then opens
GitHub while the organization-wide installation stays. CodeRabbit stays installed. nothing on GitHub while the organization-wide installation stays. CodeRabbit stays installed.
Maintainer's call, 2026-09-14. Maintainer's call, 2026-09-14.
- [x] Mirror to GitHub from `.gitea/workflows/mirror.yaml` and `mirror-delete.yaml`, with - [x] **Mirror to GitHub from `.gitea/workflows/mirror.yaml` and `mirror-delete.yaml`, with
`MIRROR_GITHUB_TOKEN`. A push of a commit carrying the workflow, and the nightly run, send all of `MIRROR_GITHUB_TOKEN`.** A push of a commit carrying the workflow, and the nightly run, send
Gitea's branches and tags, overwriting a same-named ref; a branch or tag deleted on Gitea is all of Gitea's branches and tags, overwriting a same-named ref; a branch or tag deleted on
deleted there too. Refs only GitHub has stay. Maintainer's call, 2026-09-14. Gitea is deleted there too. Refs only GitHub has stay. Maintainer's call, 2026-09-14.
- [x] GitHub's wiki, projects and Actions are off, the Travis app and webhook are gone, and its About - [x] **Strip GitHub's repository to a mirror, and give both forges the package's About.** GitHub's
matches the package. Gitea carries the same description, website and topics, and sends issues wiki, projects and Actions are off, the Travis app and webhook are gone, and its About matches
to GitHub as its external tracker. the package. Gitea carries the same description, website and topics, and sends issues to
GitHub as its external tracker.
## Close the GitHub backlog ## Close the GitHub backlog
**Answer and close as fixed by 0.5.0**, the reply naming what fixed it: **Answer and close as fixed by 0.5.0**, the reply naming what fixed it:
- [x] [#2](https://github.com/larvit/larvitsmpp/issues/2) Tests for the README examples: - [x] **Close [#2](https://github.com/larvit/larvitsmpp/issues/2), tests for the README examples.**
`test/readme.test.ts`. Fixed by `test/readme.test.ts`.
- [x] [#3](https://github.com/larvit/larvitsmpp/issues/3) Tests for flash messages: - [x] **Close [#3](https://github.com/larvit/larvitsmpp/issues/3), tests for flash messages.** Fixed
`test/session.test.ts`. by `test/session.test.ts`.
- [x] [#4](https://github.com/larvit/larvitsmpp/issues/4) DLR errors with `message_state` missing: - [x] **Close [#4](https://github.com/larvit/larvitsmpp/issues/4), DLR errors with `message_state`
`dlrFromPdu()` parses the `stat:` receipt text when the TLVs are absent. missing.** `dlrFromPdu()` parses the `stat:` receipt text when the TLVs are absent.
- [x] [#13](https://github.com/larvit/larvitsmpp/issues/13) Limit a long SMS to fewer segments: the - [x] **Close [#13](https://github.com/larvit/larvitsmpp/issues/13), limit a long SMS to fewer
`maxSegments` send option. segments.** Fixed by the `maxSegments` send option.
- [x] [#16](https://github.com/larvit/larvitsmpp/issues/16) Support all three bind types: bound and - [x] **Close [#16](https://github.com/larvit/larvitsmpp/issues/16), support all three bind types.**
enforced in both directions. Bound and enforced in both directions.
- [x] [#17](https://github.com/larvit/larvitsmpp/issues/17) `addr_ton`/`addr_npi` should be - [x] **Close [#17](https://github.com/larvit/larvitsmpp/issues/17), `addr_ton`/`addr_npi` should be
settable: `sendSms()` takes all four, documented and tested. settable.** `sendSms()` takes all four, documented and tested.
- [x] [#20](https://github.com/larvit/larvitsmpp/issues/20) Tests fail on current dependency - [x] **Close [#20](https://github.com/larvit/larvitsmpp/issues/20), tests fail on current
versions: the mocha suite is gone; `node:test` on Node 18 to 26. dependency versions.** The mocha suite is gone; `node:test` on Node 18 to 26.
- [x] [#33](https://github.com/larvit/larvitsmpp/issues/33) Large inbound text arrives as raw - [x] **Close [#33](https://github.com/larvit/larvitsmpp/issues/33), large inbound text arrives as
`Buffer` segments: `IncomingRequests` reassembles a UDH-carrying `deliver_sm` into one `sms` raw `Buffer` segments.** `IncomingRequests` reassembles a UDH-carrying `deliver_sm` into one
event. `sms` event.
- [x] [#68](https://github.com/larvit/larvitsmpp/pull/68), a pull request: `message_id` in - [x] **Close [#68](https://github.com/larvit/larvitsmpp/pull/68), a pull request for `message_id`
`submit_sm_resp`, spec DLR codes. All four hold: `sendResp()` always answers a `message_id`, in `submit_sm_resp` and spec DLR codes.** All four hold: `sendResp()` always answers a
per segment; `stat:UNDELIV` is the 7-character code. Credit the reporter — the fork found real `message_id`, per segment; `stat:UNDELIV` is the 7-character code. Credit the reporter — the
defects. fork found real defects.
**Close as superseded**, all against 0.4.0 dependencies the rewrite does not have — `async`, **Close as superseded**, all against 0.4.0 dependencies the rewrite does not have — `async`,
`coveralls`, `eslint`, `iconv-lite`, `larvitutils`, `mocha`, `mocha-eslint`, `portfinder`, `uuid`: `coveralls`, `eslint`, `iconv-lite`, `larvitutils`, `mocha`, `mocha-eslint`, `portfinder`, `uuid`:
- [x] [#40](https://github.com/larvit/larvitsmpp/pull/40), - [x] **Close the twelve dependency pull requests as superseded.**
[#40](https://github.com/larvit/larvitsmpp/pull/40),
[#41](https://github.com/larvit/larvitsmpp/pull/41), [#41](https://github.com/larvit/larvitsmpp/pull/41),
[#42](https://github.com/larvit/larvitsmpp/pull/42), [#42](https://github.com/larvit/larvitsmpp/pull/42),
[#45](https://github.com/larvit/larvitsmpp/pull/45), [#45](https://github.com/larvit/larvitsmpp/pull/45),
@@ -175,8 +180,9 @@ the rewrite, for a dependency added later. Maintainer's call, 2026-09-14.
**Close as tracked here**, the reply saying it will be implemented on Gitea: **Close as tracked here**, the reply saying it will be implemented on Gitea:
- [x] [#8](https://github.com/larvit/larvitsmpp/issues/8) The socket's remote host and port on log - [x] **Close [#8](https://github.com/larvit/larvitsmpp/issues/8), the socket's remote host and port
messages: under Worth doing, not blocking. Maintainer's call, 2026-09-14. on log messages, as tracked here.** It sits under Worth doing, not blocking. Maintainer's
call, 2026-09-14.
## 0.6.0 ## 0.6.0
@@ -446,78 +452,80 @@ next work ([decision](docs/decisions.md#internals-and-tests)).
as `true`. One fix closes all three, and `valueText()` in `defs/types.ts` is the quoted as `true`. One fix closes all three, and `valueText()` in `defs/types.ts` is the quoted
spelling to take it from. Raised by review, 2026-09-20. spelling to take it from. Raised by review, 2026-09-20.
- [ ] **A send the codec will refuse waits for a link and a window slot first.** `refuse()` in - [ ] **Refuse a send the codec cannot build before it waits for a link and a window slot.** Today
`outgoing-requests.ts` runs `misuse()` and the abort check before the wait, precisely so a call it waits first. `refuse()` in `outgoing-requests.ts` runs `misuse()` and the abort check
that can never go out does not queue for what it will never use; a body `objToPdu()` refuses on before the wait, precisely so a call that can never go out does not queue for what it will
every attempt is the same case, and #98 made it a common one. On a down link the caller waits never use; a body `objToPdu()` refuses on every attempt is the same case, and #98 made it a
`responseTimeout` and is told the link failed rather than that the body could not be built — common one. On a down link the caller waits `responseTimeout` and is told the link failed
goal 2's wrong answer about what happened. The cheap fix builds the PDU twice, so the shape is rather than that the body could not be built — goal 2's wrong answer about what happened. The
the open half. Raised by the architecture review of cheap fix builds the PDU twice, so the shape is the open half. Raised by the architecture
review of [#98](https://github.com/larvit/larvitsmpp/pull/98), 2026-09-09.
- [ ] **Split the SMPP time format out of `message.ts` once the file has to move anyway.** It
answers two questions: message coding and the SMPP time format (`smppDate`, `smppTime`) share
the file, which the architecture map in AGENTS.md already spells out as four concerns. Nothing
is wrong today; if the file has to move for another reason, `smpp-time.ts` is the split.
Raised by the architecture review of
[#98](https://github.com/larvit/larvitsmpp/pull/98), 2026-09-09. [#98](https://github.com/larvit/larvitsmpp/pull/98), 2026-09-09.
- [ ] **`message.ts` answers two questions.** Message coding and the SMPP time format (`smppDate`, - [ ] **Add a gate that refuses a floating version anywhere in the repo.** Maintainer's ask on
`smppTime`) share the file, which the architecture map in AGENTS.md already spells out as four [#71](https://github.com/larvit/larvitsmpp/pull/71), 2026-09-06, on the `release.yaml` pinning
concerns. Nothing is wrong today; if the file has to move for another reason, `smpp-time.ts` is thread. Pinning every action and runner by hand is what the ask followed; the gate is what
the split. Raised by the architecture review of keeps them pinned. It has to cover workflow `uses:` and `runs-on:`, compose `image:`, and
[#98](https://github.com/larvit/larvitsmpp/pull/98), 2026-09-09.
- [ ] **A gate that refuses a floating version anywhere in the repo.** Maintainer's ask on
[#71](https://github.com/larvit/larvitsmpp/pull/71), 2026-09-06, on the `release.yaml`
pinning thread. Pinning every action and runner by hand is what the ask followed; the gate is
what keeps them pinned. It has to cover workflow `uses:` and `runs-on:`, compose `image:`, and
Dockerfile `FROM`, and the conventions differ per kind — actions take a semver tag, images the Dockerfile `FROM`, and the conventions differ per kind — actions take a semver tag, images the
full patch version — so one grep for `latest` is not it. full patch version — so one grep for `latest` is not it.
- [ ] **A gate that fails when the test matrix misses the current Node.** Maintainer's ask on - [ ] **Add a gate that catches the test matrix missing the current Node.** Maintainer's ask on
[#71](https://github.com/larvit/larvitsmpp/pull/71), 2026-09-06, on the Node 26 thread. Node [#71](https://github.com/larvit/larvitsmpp/pull/71), 2026-09-06, on the Node 26 thread. Node
26 was added by hand; nothing notices when 27 ships. Needs a source for what Current is — the 26 was added by hand; nothing notices when 27 ships. Needs a source for what Current is — the
Node release schedule is published as JSON — and a decision on whether a new Current fails the Node release schedule is published as JSON — and a decision on whether a new Current fails the
build or opens a PR, which is what Renovate already does for everything else here. build or opens a PR, which is what Renovate already does for everything else here.
- [ ] **CodeRabbit reviews through the GitHub mirror.** CodeRabbit does not support Gitea, so mirror - [ ] **Have CodeRabbit review Gitea pull requests through the GitHub mirror.** CodeRabbit does not
each Gitea pull request to GitHub for it to review there. Maintainer's ask, 2026-09-14; not support Gitea, so mirror each Gitea pull request to GitHub for it to review there.
started until asked. Maintainer's ask, 2026-09-14; not started until asked.
- [ ] **`leftOf()` and the link gate's own budget are one concept counted twice.** - [ ] **Count what is left of a budget one way in `leftOf()` and the link gate.** Today they are one
`idle-waiters.ts` reads what is left of a budget as `Math.max(1, deadline - now)`, because 0 concept counted twice. `idle-waiters.ts` reads what is left of a budget as `Math.max(1,
means "forever" there; `link-gate.ts` runs the same subtraction and calls `<= 0` expired. deadline - now)`, because 0 means "forever" there; `link-gate.ts` runs the same subtraction
Neither is reachable from the other, so nothing can disagree today, but a reader who learns one and calls `<= 0` expired. Neither is reachable from the other, so nothing can disagree today,
and applies it to the other is wrong. A budget type both take would close it. Raised by review, but a reader who learns one and applies it to the other is wrong. A budget type both take
2026-09-01. would close it. Raised by review, 2026-09-01.
- [ ] **`err:` on a receipt for a state that neither delivered nor failed.** `receiptText()` now - [ ] **Write a non-zero `err:` on a receipt only for a state that failed.** `receiptText()` now
writes `err:000` for `DELIVERED` and for the two transient states, and `err:001` for every writes `err:000` for `DELIVERED` and for the two transient states, and `err:001` for every
other — so `ACCEPTED`, `SKIPPED`, `UNKNOWN` and `DELETED` still announce an error code the SMSC other — so `ACCEPTED`, `SKIPPED`, `UNKNOWN` and `DELETED` still announce an error code the
never had. Which of those are failures is the open half. Raised by review, 2026-09-03; needs a SMSC never had. Which of those are failures is the open half. Raised by review, 2026-09-03;
decision. needs a decision.
- [ ] **`once()` is copied into four test files, and two copies never give up.** - [ ] **Share one `once()` across the test files, one that gives up.** Today it is copied into four
`session-extras.test.ts` and `readme.test.ts` reject after 5000 ms; `session.test.ts` and test files, and two copies never give up. `session-extras.test.ts` and `readme.test.ts` reject
`tls.test.ts` wait forever, so an event that never fires still hangs the run the way an after 5000 ms; `session.test.ts` and `tls.test.ts` wait forever, so an event that never fires
unclosed listener used to. One shared, guarded copy closes the rest of that class. still hangs the run the way an unclosed listener used to. One shared, guarded copy closes the
rest of that class.
- [ ] **The peer's address and bind on every session log message.** `remoteAddress` and `remotePort` - [ ] **Carry the peer's address and bind on every session log message.** `remoteAddress` and
reach only `server - incoming connection`, and `systemId` only the bind messages, so with `remotePort` reach only `server - incoming connection`, and `systemId` only the bind messages,
several peers connected one session's lines cannot be told apart, and a reconnect leaves nothing so with several peers connected one session's lines cannot be told apart, and a reconnect
stable to filter on. Carrying them in every session message's metadata is a change to every leaves nothing stable to filter on. Carrying them in every session message's metadata is a
call site. From [#8](https://github.com/larvit/larvitsmpp/issues/8), closed there as tracked change to every call site. From [#8](https://github.com/larvit/larvitsmpp/issues/8), closed
here; maintainer's call, 2026-09-14. there as tracked here; maintainer's call, 2026-09-14.
- [ ] **A peer whose message ids share one base logs a refused merge on every send.** `smsc01-000123` - [ ] **Settle how a peer whose message ids share one base logs its refused merges.** Today it logs
and `smsc01-000124` carry the same base, so `DlrMerger` merges the first message and refuses one on every send. `smsc01-000123` and `smsc01-000124` carry the same base, so `DlrMerger`
every one after it, one log line per send. Left at `info` — nothing the operator can fix is merges the first message and refuses every one after it, one log line per send. Left at `info`
wrong — but a rate guard or silence may suit it better. Raised by review, 2026-08-30. — nothing the operator can fix is wrong — but a rate guard or silence may suit it better.
Raised by review, 2026-08-30.
- [ ] **`submit_multi` and the broadcast commands** encode and decode, but nothing exercises them - [ ] **Exercise `submit_multi` and the broadcast commands end to end.** They encode and decode, but
end to end. The interop suite is the natural place. 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 - [ ] **Move to TypeScript 7 once `typescript-eslint` supports it.** `renovate.json` pins TypeScript
below 6.1 for exactly that reason. below 6.1 for exactly that reason.
- [ ] **An `onReceipt` hook.** Receipt text is only loosely specified and operators disagree on it, - [ ] **Add an `onReceipt` hook.** Receipt text is only loosely specified and operators disagree on
but `dlrFromPdu()` is wired into `IncomingRequests` with no seam of its own: an application it, but `dlrFromPdu()` is wired into `IncomingRequests` with no seam of its own: an
facing a format we do not parse has to take the whole PDU on `onRequest` and reimplement the application facing a format we do not parse has to take the whole PDU on `onRequest` and
dispatch, which owns the response as well. reimplement the dispatch, which owns the response as well. Mirror the `onRequest` seam —
Mirror the `onRequest` seam — return a `Dlr` to own the receipt, `undefined` to fall through return a `Dlr` to own the receipt, `undefined` to fall through to the built-in parser.
to the built-in parser.
## Gaps against other SMPP libraries ## Gaps against other SMPP libraries
@@ -527,38 +535,40 @@ Each lands under goal 7: an option or a hook, with the call that passes none unc
### Sending ### Sending
- [ ] **A limiter hook, with a messages-per-second cap built on it.** Maintainer's call, 2026-09-14, - [ ] **Add a limiter hook, with a messages-per-second cap built on it.** Maintainer's call,
reversing the earlier decline: Kannel, Jasmin, go-smpp and `smpp-js-sdk` all limit throughput. 2026-09-14, reversing the earlier decline: Kannel, Jasmin, go-smpp and `smpp-js-sdk` all limit
Count PDUs, not `sendSms()` calls — a long message is one `submit_sm` per segment and the throughput. Count PDUs, not `sendSms()` calls — a long message is one `submit_sm` per segment
operator counts those, which an application wrapping `sendSms()` cannot see. The hook takes a and the operator counts those, which an application wrapping `sendSms()` cannot see. The hook
limiter the application already runs; the built-in cap counts per session until the store at the takes a limiter the application already runs; the built-in cap counts per session until the
bottom lets it span sessions and processes. The default stays uncapped. Open: the hook's shape (a store at the bottom lets it span sessions and processes. The default stays uncapped. Open: the
wait that resolves when a PDU may go, cut short by the send's `signal`), which requests it gates hook's shape (a wait that resolves when a PDU may go, cut short by the send's `signal`), which
— messages, never `enquire_link`, `unbind` or a response — and whether its wait counts against requests it gates — messages, never `enquire_link`, `unbind` or a response — and whether its
`responseTimeout`. wait counts against `responseTimeout`.
- [ ] **Back off and resend on `ESME_RTHROTTLED`.** The SMSC refused the PDU, so resending cannot - [ ] **Back off and resend on `ESME_RTHROTTLED`.** The SMSC refused the PDU, so resending cannot
duplicate it and goal 2 holds, and the retry needs nothing wider than the session. Needs a duplicate it and goal 2 holds, and the retry needs nothing wider than the session. Needs a
decision: on by default with a bounded budget, as goal 5 suggests, and whether `ESME_RMSGQFUL` decision: on by default with a bounded budget, as goal 5 suggests, and whether `ESME_RMSGQFUL`
counts too. A throttled answer is also what a limiter hook wants to hear about. counts too. A throttled answer is also what a limiter hook wants to hear about.
- [ ] **`sendSms()` takes the rest of `submit_sm`.** `service_type`, `priority_flag`, `protocol_id`, - [ ] **Let `sendSms()` take the rest of `submit_sm`.** `service_type`, `priority_flag`,
`replace_if_present_flag` and TLVs on every segment, and `registered_delivery` beyond final `protocol_id`, `replace_if_present_flag` and TLVs on every segment, and `registered_delivery`
receipts: on failure only, and intermediate notifications. Today each needs `send()`, which gives beyond final receipts: on failure only, and intermediate notifications. Today each needs
up splitting, the alphabet checks and receipt merging. TLVs are the common case: India's DLT `send()`, which gives up splitting, the alphabet checks and receipt merging. TLVs are the
rules put `PE_ID` (0x1400) and `TEMPLATE_ID` (0x1401) on every `submit_sm`, and USSD rides on common case: India's DLT rules put `PE_ID` (0x1400) and `TEMPLATE_ID` (0x1401) on every
`ussd_service_op`. Refuse a TLV the send composes itself (`sar_*`, `message_payload`). Open: one `submit_sm`, and USSD rides on `ussd_service_op`. Refuse a TLV the send composes itself
spelling for receipts, since `dlr: true` and a raw `registered_delivery` could disagree, and (`sar_*`, `message_payload`). Open: one spelling for receipts, since `dlr: true` and a raw
whether goal 4's rule on optional parameters binds a TLV the caller named. `registered_delivery` could disagree, and whether goal 4's rule on optional parameters binds a
TLV the caller named.
- [ ] **Choose how a long message is spelled on the wire.** Only an 8-bit UDH reference goes out - [ ] **Choose how a long message is spelled on the wire.** Only an 8-bit UDH reference goes out
(`message.ts`), though the reader takes a 16-bit UDH, `sar_*` and `message_payload` alike. Some (`message.ts`), though the reader takes a 16-bit UDH, `sar_*` and `message_payload` alike. Some
SMSCs take only `sar_*` or `message_payload`; php-smpp offers all three. A 16-bit reference also SMSCs take only `sar_*` or `message_payload`; php-smpp offers all three. A 16-bit reference also
makes a collision rarer: the 8-bit one wraps every 255 sends on a session. makes a collision rarer: the 8-bit one wraps every 255 sends on a session.
- [ ] **Failover across SMSC hosts.** Maintainer's call, 2026-09-14. `client()` takes one `host` and - [ ] **Fail over across SMSC hosts.** Maintainer's call, 2026-09-14. `client()` takes one `host`
`port`; Kannel, Jasmin and php-smpp take several. A list the reconnect loop walks holds only and `port`; Kannel, Jasmin and php-smpp take several. A list the reconnect loop walks holds
which host the one session is on, so it needs no store. Two options, both maintainer's calls: only which host the one session is on, so it needs no store. Two options, both maintainer's
calls:
- **Order**, `fixed` or round robin, default `fixed`. Fixed starts every reconnect at the first - **Order**, `fixed` or round robin, default `fixed`. Fixed starts every reconnect at the first
host; round robin at the host after the one the link was last on. host; round robin at the host after the one the link was last on.
- **Starting over**, a boolean, default on: once the last host has been tried, go back to the - **Starting over**, a boolean, default on: once the last host has been tried, go back to the
@@ -569,26 +579,29 @@ Each lands under goal 7: an option or a hook, with the call that passes none unc
### The server ### The server
- [ ] **`sendSms()` on a `server()` session sends `submit_sm` toward the ESME.** Kannel answers - [ ] **Send `deliver_sm` from `sendSms()` on a `server()` session.** Today it sends `submit_sm`
`ESME_RINVCMDID` (`interop-tests/kannel.test.ts`, "MO to Kannel"), and goal 1 says that PDU never toward the ESME. Kannel answers `ESME_RINVCMDID` (`interop-tests/kannel.test.ts`, "MO to
goes out. A server has no other way to send an MO message either: `sendMo()` in that test builds Kannel"), and goal 1 says that PDU never goes out. A server has no other way to send an MO
one from `submitSmParams()` and `ConcatReference`, neither exported. Choosing `deliver_sm` by message either: `sendMo()` in that test builds one from `submitSmParams()` and
`linkEnd` gives MO messages the splitting and checks, keeps one method for one goal, and refuses `ConcatReference`, neither exported. Choosing `deliver_sm` by `linkEnd` gives MO messages the
the options 3.4 has `deliver_sm` leave empty (`scheduleDeliveryTime`, `validityPeriod`). splitting and checks, keeps one method for one goal, and refuses the options 3.4 has
`deliver_sm` leave empty (`scheduleDeliveryTime`, `validityPeriod`).
- [ ] **Error TLVs on a response this library builds.** `buildBody()` in `pdu.ts` writes no body for - [ ] **Let a response this library builds carry error TLVs.** `buildBody()` in `pdu.ts` writes no
any non-zero status, so a server cannot answer a `data_sm` with `delivery_failure_reason`, body for any non-zero status, so a server cannot answer a `data_sm` with
`network_error_code` or `additional_status_info_text`, and a 5.0 peer gets none of its error TLVs. `delivery_failure_reason`, `network_error_code` or `additional_status_info_text`, and a 5.0
3.4 omits the body on error for `submit_sm_resp` by name; read each response's section before peer gets none of its error TLVs. 3.4 omits the body on error for `submit_sm_resp` by name;
widening it. Reading needs nothing: an error response carrying a body already parses. read each response's section before widening it. Reading needs nothing: an error response
carrying a body already parses.
- [ ] **PROXY protocol on `server()`.** Behind HAProxy or an AWS NLB every session's remote address is - [ ] **Accept the PROXY protocol on `server()`.** Behind HAProxy or an AWS NLB every session's
the balancer's, so `authenticate` cannot allow-list by IP and logs name the wrong peer. v1 is remote address is the balancer's, so `authenticate` cannot allow-list by IP and logs name the
text; v2 is binary and the only one an NLB sends. `smpp` accepts v1 from anyone; accept either wrong peer. v1 is text; v2 is binary and the only one an NLB sends. `smpp` accepts v1 from
only from addresses the option names. anyone; accept either only from addresses the option names.
- [ ] **`outbind`.** In the command table, handled nowhere: a client cannot take an SMSC's `outbind` - [ ] **Handle `outbind`.** It is in the command table, handled nowhere: a client cannot take an
and bind back, and `server()` cannot send one. Rare; take it on with a peer that uses it. SMSC's `outbind` and bind back, and `server()` cannot send one. Rare; take it on with a peer
that uses it.
- [ ] **Register vendor-specific commands.** 3.4 reserves `command_id` `0x00010200`–`0x000102FF` for - [ ] **Register vendor-specific commands.** 3.4 reserves `command_id` `0x00010200`–`0x000102FF` for
SMSC vendors; today one arrives as a `PduRefusedError`. `smpp` has `addCommand()`. The same SMSC vendors; today one arrives as a `PduRefusedError`. `smpp` has `addCommand()`. The same
@@ -610,9 +623,9 @@ Each lands under goal 7: an option or a hook, with the call that passes none unc
on one coding are an `err`. Settle whether a claim on 0x00 reaches the class groups on one coding are an `err`. Settle whether a claim on 0x00 reaches the class groups
`messageClassEncoding()` reads GSM 7-bit from, which `flash` writes under. `messageClassEncoding()` reads GSM 7-bit from, which `flash` writes under.
- [ ] **The alphabets SMPP 3.4 names that no encoding carries.** `consts.ENCODING` lists the - [ ] **Carry the alphabets SMPP 3.4 names that no encoding carries.** `consts.ENCODING` lists the
`data_coding` ids (5.2.19); only `ASCII`, `LATIN1` and `UCS2` can be sent. Those with a published `data_coding` ids (5.2.19); only `ASCII`, `LATIN1` and `UCS2` can be sent. Those with a
definition, and what each costs: published definition, and what each costs:
- 0x01 IA5 (ITU-T T.50, ASCII in practice): trivial. - 0x01 IA5 (ITU-T T.50, ASCII in practice): trivial.
- 0x06 ISO-8859-5 (Cyrillic) and 0x07 ISO-8859-8 (Hebrew): 96-entry tables. - 0x06 ISO-8859-5 (Cyrillic) and 0x07 ISO-8859-8 (Hebrew): 96-entry tables.
- 0x05 JIS X 0208, 0x0D JIS X 0212, 0x0A ISO-2022-JP and 0x0E KS C 5601: two-octet sets. - 0x05 JIS X 0208, 0x0D JIS X 0212, 0x0A ISO-2022-JP and 0x0E KS C 5601: two-octet sets.
@@ -622,30 +635,34 @@ Each lands under goal 7: an option or a hook, with the call that passes none unc
A Node without full ICU throws from `new TextDecoder()`, which hard rule 1 wraps into an `err`. A Node without full ICU throws from `new TextDecoder()`, which hard rule 1 wraps into an `err`.
- 0x09 pictogram has no published definition; leave it out. - 0x09 pictogram has no published definition; leave it out.
- [ ] **GSM 7-bit national language shift tables.** 3GPP TS 23.038 defines them for Turkish, Spanish - [ ] **Read and send the GSM 7-bit national language shift tables.** 3GPP TS 23.038 defines them
(single shift only), Portuguese and ten Indian languages — Bengali, Gujarati, Hindi, Kannada, for Turkish, Spanish (single shift only), Portuguese and ten Indian languages — Bengali,
Malayalam, Oriya, Punjabi, Tamil, Telugu and Urdu — selected per message by UDH elements 0x25 Gujarati, Hindi, Kannada, Malayalam, Oriya, Punjabi, Tamil, Telugu and Urdu — selected per
(locking) and 0x24 (single). They keep that text near GSM's segment size instead of UCS2's 67 message by UDH elements 0x25 (locking) and 0x24 (single). They keep that text near GSM's
characters. Reading means honouring those elements in `decodeMessage()`; sending means `detect()` segment size instead of UCS2's 67 characters. Reading means honouring those elements in
picking a table, with each element's 3 octets off the segment budget. `smpp` has Turkish, Spanish `decodeMessage()`; sending means `detect()` picking a table, with each element's 3 octets off
and Portuguese, used only when the caller writes the UDH. the segment budget. `smpp` has Turkish, Spanish and Portuguese, used only when the caller
writes the UDH.
- [ ] **Packed GSM 7-bit, opt-in.** Everything goes out unpacked, SMPP's convention (AGENTS.md, "GSM - [ ] **Offer packed GSM 7-bit, opt-in.** Everything goes out unpacked, SMPP's convention
7-bit is sent unpacked"); go-smpp carries a packed codec for SMSCs that want septets. Find an SMSC (AGENTS.md, "GSM 7-bit is sent unpacked"); go-smpp carries a packed codec for SMSCs that want
that needs it before building it. septets. Find an SMSC that needs it before building it.
- [ ] **`consts.ENCODING` spells five alphabets twice.** `CYRILLIC`/`ISO_8859_5`, - [ ] **Give each alphabet in `consts.ENCODING` one name.** Five are spelled twice:
`HEBREW`/`ISO_8859_8`, `JIS`/`X_0208_1990`, `EXTENDED_KANJI_JIS`/`X_0212_1990` and `CYRILLIC`/`ISO_8859_5`, `HEBREW`/`ISO_8859_8`, `JIS`/`X_0208_1990`,
`LATIN1`/`ISO_8859_1`; `FLASH` is a message class, not an alphabet. One name each before `EXTENDED_KANJI_JIS`/`X_0212_1990` and `LATIN1`/`ISO_8859_1`; `FLASH` is a message class, not
registration starts taking names. A breaking change to an export. an alphabet. One name each before registration starts taking names. A breaking change to an
export.
### Observability ### Observability
- [ ] **Metrics.** Inbound traffic has `data`, `incomingPdu` and `incomingPduObj`; outbound has no - [ ] **Add metrics: outbound PDU events and counts of requests waiting.** Inbound traffic has
event, and nothing counts requests in flight, queued for a window slot, waiting for a link, or `data`, `incomingPdu` and `incomingPduObj`; outbound has no event, and nothing counts requests
unanswered. `smpp` and `@semyonf/smpp` emit `metrics`; cloudhopper keeps per-session counters. An in flight, queued for a window slot, waiting for a link, or unanswered. `smpp` and
`outgoingPdu`/`outgoingPduObj` pair mirrors the inbound events; the counters can be one read-only `@semyonf/smpp` emit `metrics`; cloudhopper keeps per-session counters. An
snapshot, read from the owner of each count rather than a second tally that can drift. `outgoingPdu`/`outgoingPduObj` pair mirrors the inbound events; the counters can be one
read-only snapshot, read from the owner of each count rather than a second tally that can
drift.
### Packaging, tests and CI ### Packaging, tests and CI
@@ -655,21 +672,21 @@ Each lands under goal 7: an option or a hook, with the call that passes none unc
`src` (41 files, 209 KB) makes go to definition land in the TypeScript, and lets a debugger or `src` (41 files, 209 KB) makes go to definition land in the TypeScript, and lets a debugger or
`--enable-source-maps` show it. `--enable-source-maps` show it.
- [ ] **A coverage report and a floor in the gate.** `node --test --experimental-test-coverage - [ ] **Add a coverage report and a floor to the gate.** `node --test --experimental-test-coverage
--test-coverage-include='src/**' test/*.test.ts` on Node 24.18.0, 2026-09-14: 98.77% lines, --test-coverage-include='src/**' test/*.test.ts` on Node 24.18.0, 2026-09-14: 98.77% lines,
93.85% branches, 98.28% functions. Gate at 98, 93 and 98 with `--test-coverage-lines`, 93.85% branches, 98.28% functions. Gate at 98, 93 and 98 with `--test-coverage-lines`,
`--test-coverage-branches` and `--test-coverage-functions`, which Node 22 and later take — a job `--test-coverage-branches` and `--test-coverage-functions`, which Node 22 and later take — a
of its own on 24, since the matrix runs compiled JavaScript — and add it to `main`'s required job of its own on 24, since the matrix runs compiled JavaScript — and add it to `main`'s
checks. Raise the floor as coverage rises; never lower it. required checks. Raise the floor as coverage rises; never lower it.
- [ ] **Mutation testing.** `@stryker-mutator/tap-runner` runs `node:test` suites and measures whether - [ ] **Add mutation testing.** `@stryker-mutator/tap-runner` runs `node:test` suites and measures
a test notices a change, which coverage cannot; `@semyonf/smpp` runs Stryker in CI. The session whether a test notices a change, which coverage cannot; `@semyonf/smpp` runs Stryker in CI.
suites are timer-heavy, so start with the codec and the encodings. The session suites are timer-heavy, so start with the codec and the encodings.
- [ ] **A Node-RED node, as a package of its own.** `@leissner/node-red-smpp` is the only SMPP node in - [ ] **Build a Node-RED node, as a package of its own.** `@leissner/node-red-smpp` is the only SMPP
the Node-RED library, and by a read of its source it never parses a receipt and never answers the node in the Node-RED library, and by a read of its source it never parses a receipt and never
SMSC's `enquire_link`. Its UI is a fair list of what operators set. It builds on this package, answers the SMSC's `enquire_link`. Its UI is a fair list of what operators set. It builds on
never inside it. this package, never inside it.
## Declined ## Declined
@@ -683,9 +700,9 @@ Each lands under goal 7: an option or a hook, with the call that passes none unc
## An optional store ## An optional store
- [ ] **Pooling, and state that survives a restart, through an optional store.** Maintainer's call, - [ ] **Add pooling, and state that survives a restart, through an optional store.** Maintainer's
2026-09-14. It replaces two declines — merge state surviving a restart, and a pool of sessions — call, 2026-09-14. It replaces two declines — merge state surviving a restart, and a pool of
and goal 9 was rewritten for it. Big: design before code. sessions — and goal 9 was rewritten for it. Big: design before code.
- **What it holds.** Receipts still awaited and the groups `DlrMerger` collects. Segments of a - **What it holds.** Receipts still awaited and the groups `DlrMerger` collects. Segments of a
message already answered but not yet whole, which the peer will not send again (goal 2). The message already answered but not yet whole, which the peer will not send again (goal 2). The
concatenation reference, so a restart does not reuse one. For a pool, the ids every session concatenation reference, so a restart does not reuse one. For a pool, the ids every session