11 KiB
todo.md
Remaining work for the @larvit/smpp 1.0.0 rewrite. Read AGENTS.md first — the hard
rules there constrain every item below.
How to resume
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.
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.
defsstays as a grouped export alongside the individual tables (cmds,consts,encodings,errors,tlvs,types, and the*ByIdmaps). utilsis 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()andsession.sendReturn()— that is how the other 29 commands are reached. pduObj.isResp()is now the standalone exportisResp(pduObj); PDU objects are pure data.
Tasks
1. Definition tables — src/defs/
constants.ts—consts+constsById.errors.ts— allESME_*codes pluserrorsById.types.ts— wire typesint8,int16,int32,string,cstring,buffer,dest_address_array,unsuccess_sme_array, and thetlvvariants. Each is{ default, read(buffer, offset, length?), size(value), write(value, buffer, offset) }.readmust be bounds-checked and return a result rather than throwing.encodings.ts— GSM 03.38 (ASCII),LATIN1,UCS2,FLASH(alias of ASCII) anddetect. Dropiconv-lite: LATIN1 isBuffer.from(str, 'latin1'), UCS2 isBuffer.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 plustlvsByIdand the two aliases (alert_on_msg_delivery,failed_broadcast_area_identifier).commands.ts— all 33 commands with ids and wire-ordered parameter lists, pluscmdsById.
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:
smppDatemust add 1 togetMonth()and zero-pad correctly. - Fix: LATIN1 must actually decode — resolve
data_codingto a concrete encoding, not to whichever alias happens to sort last. - The concatenation reference counter is per session, not module-global.
splitMessagetherefore 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_messagethat 0.4.0 has — real peers send it. - Guard
cmdLengthagainst 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:
pduToObjreturns a union discriminated oncmdName, andobjToPdunarrowsparamsto the named command's fields.
4. Session — src/session.ts
- Framing off the socket. Replace 0.4.0's
Buffer.concatper chunk with an accumulating reader — concatenating the whole queue on everydataevent is quadratic. - Sequence numbers, wrapping at 2147483646.
send()withresponseTimeout,maxOutstandingwindowing and a queue, andAbortSignal. Listeners must be removed on every exit path — timeout, abort and close included.sendReturn(),unbind(),close().submit_smhandling, including long-SMS reassembly withmaxReassemblyand a realreassemblyTimeouttimer.deliver_smhandling: 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
dlrplus mergedmessageDlr. enquire_linkandunbindhandling.
5. Sms handle — src/sms.ts
sendResp(status?)andsendDlr(status?)returning result DTOs.- Generated
message_idvalues are UUID v7. - Fix:
sendDlremits the 7-character spec status (UNDELIV, notUNDELIVERABLE).
6. Client and server — src/client.ts, src/server.ts
client()— bind bybindType, 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
minDelayandmaxDelay, re-binding and emittingreconnected. Decide and document what happens to in-flight sends across a reconnect. server()— resolves once when listening, exposesport,close()and thesessionevent.authenticatehook;userDataattached to the session.idleTimeoutdrops 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
smpppackage (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 --provenanceon av*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 larvitsmpppointing at@larvit/smpponce 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.