Files
smpp-js/todo.md
T

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. 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.
  • 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/

All done, with tests in test/encodings.test.ts, test/types.test.ts and test/commands.test.ts.

  • constants.tsconsts + constsById.
  • errors.ts — all ESME_* codes plus errorsById, isErrorName, errorNameById.
  • 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.
  • encodings.ts — GSM 03.38, LATIN1, UCS2, FLASH, detect and encodingByDataCoding. iconv-lite is gone; Buffer does it natively.
  • tlvs.ts — 67 TLV definitions, tlvsById, and the two aliases.
  • commands.ts — all 33 commands, wire-ordered, plus cmdsById and the PduParams<C> / PduParamsInput<C> per-command types.
  • filters.tsnot 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.

2. Message helpers — src/message.ts

  • bitCount(msg, encoding?), encodeMessage, decodeMessage, smppDate, splitMessage.
  • smppTime.encode(value) / smppTime.decode(value) — absolute and relative SMPP time formats, replacing the dormant filters.time. Used by validityPeriod and scheduleDeliveryTime.
  • Fix: segments carry 153 GSM characters or 67 UCS2 characters. 0.4.0 produces 152/66, because it pushes msgPart.slice(0, -1) after accumulating a full segment. Read the "GSM 7-bit is sent unpacked" section of AGENTS.md before touching these numbers.
  • Fix: smppDate must add 1 to getMonth() and zero-pad correctly.
  • Fix: decodeMessage must resolve the alphabet through encodingByDataCoding (already written and tested) rather than scanning the alias table, which is what broke LATIN1.
  • 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. It is now an explicit decision in the parser: types.buffer.size() no longer silently drops a trailing 0x00, because that corrupted every UCS2 message ending in one (see AGENTS.md).
  • 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.jstest/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.yamlnpm 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.