Give server() consumers the onRequest hook the docs promise (#86)

* Regression tests for an onRequest hook on server()

* Compose the application's onRequest hook into server() past bind

* Document the server's onRequest hook and record the composition decision

* Pin that a re-bind and a pre-bind unbind never reach the hook, and that a non-function hook is refused at startup

* Keep every bind and all pre-bind traffic out of the application's hook

* Make the hook's published promises exact, and record the src layout deferral

* Trim the record to what the tests do not already forbid

* Pin that nothing answers a request whose hook failed

* Answer nothing for a request whose hook failed, on both surfaces alike

* State the hook's failure policy as it now behaves

* Name the request a failed handler left unanswered, and say what a wedged hook reports
This commit is contained in:
2026-09-06 17:20:45 +02:00
committed by GitHub
parent 0d1da1dc87
commit bcbb042c1b
8 changed files with 375 additions and 100 deletions
+27 -21
View File
@@ -240,37 +240,42 @@ since SMPP marks that field unused, so an inbound message's base is a handle of
A refusal that depends on the request rather than the reassembled message — a full queue, an unknown
recipient, an unauthorised sender — needs to land before a segment is answered, which `sendResp()`
can no longer do once it has. A hand-wired `Session` (see [Bind direction](#bind-direction)) decides
there instead, at `onRequest`, which runs before any of the library's own handling:
can no longer do once it has. `onRequest` decides there instead, on every request a bound peer
sends, before reassembly and before the `sms` event:
```javascript
import net from 'node:net';
import { isCommand, Session } from '@larvit/smpp';
import { isCommand, server } from '@larvit/smpp';
const knownRecipients = new Set(['46709771337']);
net.createServer(sock => {
const session = new Session({
onRequest: async (bound, pduObj) => {
if (!isCommand(pduObj, 'submit_sm') || knownRecipients.has(pduObj.params.destination_addr)) {
return false;
}
const { err } = await server({
onRequest: async (session, pduObj) => {
if (!isCommand(pduObj, 'submit_sm') || knownRecipients.has(pduObj.params.destination_addr)) {
return false;
}
await bound.sendReturn(pduObj, 'ESME_RINVDSTADR');
await session.sendReturn(pduObj, 'ESME_RINVDSTADR');
return true;
},
sock,
});
session.linkEnd = 'smsc';
}).listen(2775);
return true;
},
});
if (err) throw err;
```
Returning `true` means the hook has answered the PDU and the library leaves it alone; `false` lets
the built-in handling — reassembly, the `sms` event — run as usual. A hand-wired `Session` has no
bind handling of its own, though: `onRequest` is where a peer's bind gets accepted too, the way
`server()` does it.
the built-in handling — reassembly, the `sms` event — run as usual. Every segment of a concatenated
message is a request of its own, so the hook sees each one while refusing it still means something.
No bind reaches it, and neither does anything a peer sends before one: `server()` answers those and
runs `authenticate` itself, so a hook cannot intercept a bind however it is written.
A hook that throws or rejects reaches `sessionError`, and nothing else is written for that request:
the library cannot tell a hook that failed before answering from one that failed after, and a second
response on the peer's sequence number would be worse than none. The peer's own response timeout
settles it, so a hook that must reach a decision either way makes that decision itself.
A `Session` you construct yourself (see [Bind direction](#bind-direction)) takes the same hook as a
session option, and handles a failing one the same way; it is also where a peer's bind gets accepted,
since a hand-wired session has no bind handling of its own.
`sendDlr` accepts `SCHEDULED`, `ENROUTE`, `DELIVERED`, `EXPIRED`, `DELETED`, `UNDELIVERABLE`,
`ACCEPTED`, `UNKNOWN`, `REJECTED` and `SKIPPED`. `SCHEDULED` and `ENROUTE` go out as intermediate
@@ -285,6 +290,7 @@ A message whose `data_coding` says 8-bit binary arrives as Latin-1, so `Buffer.f
| --- | --- | --- |
| `host` / `port` | all interfaces / `2775` | Where to listen. Pass `0` for any free port. |
| `authenticate` | accept everything | `({ password, session, systemId, systemType }) => false \| { userData }`, sync or async. |
| `onRequest` | none | `(session, pduObj) => true \| false`, sync or async. First refusal on every request a bound peer sends; no bind, and nothing before one, reaches it. |
| `systemId` | `''` | The SMSC identity returned to the ESME in the bind response. |
| `interfaceVersion` | `0x34` | The SMPP version advertised in the bind response. The floor for sending a peer optional parameters stays `0x34`, whatever this is set to. |
| `tls` | `false` | A `tls.TlsOptions` object with your certificate and key. |