Files
smpp-js/README.md
T

209 lines
6.8 KiB
Markdown

# @larvit/smpp
A simplified implementation of the SMPP protocol, in TypeScript. ESM only, types included.
Successor to [larvitsmpp](https://www.npmjs.com/package/larvitsmpp) 0.4.0. The API is the same shape
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.
## Requirements
Node 18 or later.
## Install
```bash
npm install @larvit/smpp
```
## Client
The simplest possible client — connects to localhost:2775 with no credentials and sends a message:
```javascript
import { client } from '@larvit/smpp';
const { err, session } = await client();
if (err) throw err;
await session.sendSms({
from: '46701113311',
message: 'Hello world',
to: '46709771337',
});
await session.unbind();
```
With connection parameters, a delivery report and logging:
```javascript
import { Log } from '@larvit/log';
import { client } from '@larvit/smpp';
const log = new Log('debug');
const { err, session } = await client({
host: 'smpp.somewhere.com',
log,
password: 'bar',
port: 2775,
username: 'foo',
});
if (err) throw err;
session.on('dlr', dlr => {
// dlr.smsId, dlr.statusMsg, dlr.statusId
});
const { err: sendErr, smsIds } = await session.sendSms({
dlr: true,
from: '46701113311',
message: '«baff»',
to: '46709771337',
});
```
## Server
The simplest possible server — no authentication, listening on port 2775:
```javascript
import { server } from '@larvit/smpp';
const { err, server: smpp } = await server();
if (err) throw err;
smpp.on('session', session => {
session.on('sms', async sms => {
// sms.from, sms.to, sms.message, sms.dlr
});
});
```
With authentication and delivery reports:
```javascript
import { server } from '@larvit/smpp';
const { err, server: smpp } = await server({
// Replace with your own auth. Returning an object attaches it to session.userData.
authenticate: async ({ password, systemId }) => {
if (systemId !== 'foo' || password !== 'bar') return false;
return { userData: { userId: 123 } };
},
});
if (err) throw err;
smpp.on('session', session => {
session.on('sms', async sms => {
// Responding is part of the protocol, not optional.
// Defaults to ESME_ROK; see the SMPP spec for the other status codes.
await sms.sendResp();
if (sms.dlr) {
await sms.sendDlr(); // same as sms.sendDlr('DELIVERED')
}
});
});
await smpp.close();
```
`sendDlr` accepts `SCHEDULED`, `ENROUTE`, `DELIVERED`, `EXPIRED`, `DELETED`, `UNDELIVERABLE`,
`ACCEPTED`, `UNKNOWN`, `REJECTED` and `SKIPPED`.
## Errors
Nothing in this library throws. Every fallible call returns a result carrying an optional `err`, so
failures are handled in one place instead of two:
```javascript
const { err, session } = await client({ host: 'smpp.somewhere.com' });
if (err) return;
const { err: sendErr, smsIds } = await session.sendSms({ from, message, to });
```
Runtime failures on a live connection arrive as `sessionError` and `serverError` events. They are
deliberately not called `error`: Node turns an unhandled `error` event into a thrown exception, which
is exactly what this library promises not to do.
## Sessions
### Events
| Event | Fires when |
| --- | --- |
| `sms` | An SMS arrives. 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. |
| `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. |
| `incomingPdu` | A complete PDU arrived, as a buffer. |
| `incomingPduObj` | The same PDU, parsed into an object. |
### 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.
## 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 }`.
- **Renamed options:** `enqLinkTiming``enquireLinkInterval`, server `timeout``idleTimeout`.
- **`larvitsmpp.utils` is 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 }`.
- **`pduObj.isResp()` is now the standalone `isResp(pduObj)`.**
- **The `error` event is `sessionError`** (and `serverError` on the server handle).
- **`log`** takes a [`@larvit/log`](https://www.npmjs.com/package/@larvit/log) instance instead of a
`larvitutils` one, and is silent by default.
### Behaviour that changed on the wire
0.4.0 had a number of protocol defects. Fixing them changes the bytes it puts on the wire, so if you
have worked around any of these, remove the workaround:
- Multipart segments were 158 octets, over the 140-octet limit. They are now correctly sized.
- 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`.
- `flash: true` discarded UCS2, mangling flash messages containing non-GSM characters.
- The multipart reference counter was shared by every session in the process.
- `tls: true` never performed a handshake, so the connection was not actually 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 now parsed.
## Development
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 run build
```
Tests are TypeScript and run directly under Node's type stripping, so there is no build step in the
development loop. CI additionally compiles and runs them on Node 18, 20, 22 and 24 to verify the
supported range.
## License
MIT