Derive the notation names from one table and refuse an smsIdFormat that is not the pair
This commit is contained in:
@@ -287,3 +287,16 @@ exactly 140.
|
|||||||
`node:24.18.0-bookworm-slim` ships no openssl binary, so a shelled-out fixture would pass in CI
|
`node:24.18.0-bookworm-slim` ships no openssl binary, so a shelled-out fixture would pass in CI
|
||||||
and fail on every developer machine, and a committed key leaks in a public repository. Valid while
|
and fail on every developer machine, and a committed key leaks in a public repository. Valid while
|
||||||
the dev image has no openssl.
|
the dev image has no openssl.
|
||||||
|
|
||||||
|
- **The notation a peer writes message ids in is named per place, and normalisation never reaches
|
||||||
|
inside a `<base>-<n>` id.** An SMSC may answer `submit_sm_resp` in hex and write the receipt's
|
||||||
|
`id:` in decimal, so one transform over both sides cannot make them equal — `smsIdFormat` names
|
||||||
|
`receipt` and `submitResp` separately and reads both into a plain decimal value before `smsIds`
|
||||||
|
and `dlr.smsId` are compared. Omitting a place is what leaving it alone means, so there is no
|
||||||
|
`raw` notation, and a caller-supplied formatter is refused because it would make the promise that
|
||||||
|
those two are comparable unverifiable — `onRequest` and the PDU on the `dlr` event are the escape
|
||||||
|
hatches, and the `onReceipt` hook in todo.md is the seam if one is wanted. An id no notation reads
|
||||||
|
is left exactly as it arrived, which is what keeps `DlrMerger` working: a `<base>-<n>` id parses
|
||||||
|
as no number and so reaches `expect()` and `collect()` unchanged. Normalising the base instead
|
||||||
|
would break that pair. The option is on `client()` only — a `server()` session generates its own
|
||||||
|
ids and writes its own receipts, so both places are already one notation.
|
||||||
|
|||||||
@@ -172,8 +172,8 @@ before you see them:
|
|||||||
const { err, session } = await client({ smsIdFormat: { receipt: 'decimal', submitResp: 'hex' } });
|
const { err, session } = await client({ smsIdFormat: { receipt: 'decimal', submitResp: 'hex' } });
|
||||||
```
|
```
|
||||||
|
|
||||||
An id that is not a number in the notation given is left exactly as it arrived, and `pduObjs` and
|
An id that is not a number in the notation given is left exactly as it arrived, and the PDUs carry
|
||||||
`dlr.receipt` carry the id as the peer wrote it either way.
|
what the peer wrote either way — `pduObjs` from the send, and the second argument of the `dlr` event.
|
||||||
|
|
||||||
## Server
|
## Server
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@ import type { ConnectionOptions } from 'node:tls';
|
|||||||
import type { Result, VoidResult } from './result.ts';
|
import type { Result, VoidResult } from './result.ts';
|
||||||
import type { BindType } from './session-options.ts';
|
import type { BindType } from './session-options.ts';
|
||||||
import type { SmppLog } from './log.ts';
|
import type { SmppLog } from './log.ts';
|
||||||
import type { SmsIdFormats } from './sms-id.ts';
|
import type { SmsIdFormat } from './sms-id.ts';
|
||||||
import type { Socket } from 'node:net';
|
import type { Socket } from 'node:net';
|
||||||
export type { BindType };
|
export type { BindType };
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ export type ClientOptions = {
|
|||||||
responseTimeout?: number;
|
responseTimeout?: number;
|
||||||
shutdownTimeout?: number;
|
shutdownTimeout?: number;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
smsIdFormat?: SmsIdFormats;
|
smsIdFormat?: SmsIdFormat;
|
||||||
systemType?: string;
|
systemType?: string;
|
||||||
tls?: ConnectionOptions | boolean;
|
tls?: ConnectionOptions | boolean;
|
||||||
username?: string;
|
username?: string;
|
||||||
|
|||||||
+5
-5
@@ -1,7 +1,7 @@
|
|||||||
import type { MessageState } from './defs/constants.ts';
|
import type { MessageState } from './defs/constants.ts';
|
||||||
import type { ParamValue } from './defs/types.ts';
|
import type { ParamValue } from './defs/types.ts';
|
||||||
import type { PduObject } from './pdu.ts';
|
import type { PduObject } from './pdu.ts';
|
||||||
import type { SmsIdFormat } from './sms-id.ts';
|
import type { SmsIdNotation } from './sms-id.ts';
|
||||||
import { consts, constsById, messageTypeOf } from './defs/constants.ts';
|
import { consts, constsById, messageTypeOf } from './defs/constants.ts';
|
||||||
import { decodeMessage } from './message.ts';
|
import { decodeMessage } from './message.ts';
|
||||||
import { normaliseSmsId } from './sms-id.ts';
|
import { normaliseSmsId } from './sms-id.ts';
|
||||||
@@ -164,11 +164,11 @@ function receiptBody(pduObj: PduObject): string {
|
|||||||
function receiptId(
|
function receiptId(
|
||||||
tlvId: ParamValue | undefined,
|
tlvId: ParamValue | undefined,
|
||||||
receipt: Receipt | undefined,
|
receipt: Receipt | undefined,
|
||||||
format: SmsIdFormat | undefined,
|
notation: SmsIdNotation | undefined,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
const id = nonEmptyText(tlvId) ?? nonEmptyText(receipt?.id);
|
const id = nonEmptyText(tlvId) ?? nonEmptyText(receipt?.id);
|
||||||
|
|
||||||
return id === undefined ? undefined : normaliseSmsId(id, format);
|
return id === undefined ? undefined : normaliseSmsId(id, notation);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMessageState(name: string | undefined): name is MessageState {
|
function isMessageState(name: string | undefined): name is MessageState {
|
||||||
@@ -192,14 +192,14 @@ function receiptStatus(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The delivery report a deliver_sm carries, or nothing when it carries a message instead. */
|
/** The delivery report a deliver_sm carries, or nothing when it carries a message instead. */
|
||||||
export function dlrFromPdu(pduObj: PduObject, format?: SmsIdFormat): Dlr | undefined {
|
export function dlrFromPdu(pduObj: PduObject, notation?: SmsIdNotation): Dlr | undefined {
|
||||||
const type = messageType(pduObj);
|
const type = messageType(pduObj);
|
||||||
|
|
||||||
if (type === 'other') return undefined;
|
if (type === 'other') return undefined;
|
||||||
|
|
||||||
const body = receiptBody(pduObj);
|
const body = receiptBody(pduObj);
|
||||||
const receipt = body === '' ? undefined : parseReceipt(body);
|
const receipt = body === '' ? undefined : parseReceipt(body);
|
||||||
const smsId = receiptId(pduObj.tlvs.receipted_message_id?.tagValue, receipt, format);
|
const smsId = receiptId(pduObj.tlvs.receipted_message_id?.tagValue, receipt, notation);
|
||||||
const { statusId, statusMsg } = receiptStatus(pduObj.tlvs.message_state?.tagValue, receipt);
|
const { statusId, statusMsg } = receiptStatus(pduObj.tlvs.message_state?.tagValue, receipt);
|
||||||
|
|
||||||
if (type === 'unmarked' && (smsId === undefined || statusMsg === undefined)) return undefined;
|
if (type === 'unmarked' && (smsId === undefined || statusMsg === undefined)) return undefined;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { OnRequest } from './session-options.ts';
|
|||||||
import type { PduObject } from './pdu.ts';
|
import type { PduObject } from './pdu.ts';
|
||||||
import type { Session } from './session.ts';
|
import type { Session } from './session.ts';
|
||||||
import type { SmppLog } from './log.ts';
|
import type { SmppLog } from './log.ts';
|
||||||
import type { SmsIdFormat } from './sms-id.ts';
|
import type { SmsIdNotation } from './sms-id.ts';
|
||||||
import { Reassembler, decodeSegments } from './reassembly.ts';
|
import { Reassembler, decodeSegments } from './reassembly.ts';
|
||||||
import { bindCommands, defaults } from './session-options.ts';
|
import { bindCommands, defaults } from './session-options.ts';
|
||||||
import { concatInfo } from './udh.ts';
|
import { concatInfo } from './udh.ts';
|
||||||
@@ -19,7 +19,7 @@ export type IncomingRequestsOptions = {
|
|||||||
maxReassembly?: number | undefined;
|
maxReassembly?: number | undefined;
|
||||||
onRequest?: OnRequest | undefined;
|
onRequest?: OnRequest | undefined;
|
||||||
reassemblyTimeout?: number | undefined;
|
reassemblyTimeout?: number | undefined;
|
||||||
receiptIdFormat?: SmsIdFormat | undefined;
|
receiptIdNotation?: SmsIdNotation | undefined;
|
||||||
session: Session;
|
session: Session;
|
||||||
systemId?: string | undefined;
|
systemId?: string | undefined;
|
||||||
};
|
};
|
||||||
@@ -30,7 +30,7 @@ export class IncomingRequests {
|
|||||||
private readonly log: SmppLog;
|
private readonly log: SmppLog;
|
||||||
private readonly onRequest: OnRequest | undefined;
|
private readonly onRequest: OnRequest | undefined;
|
||||||
private readonly reassembler: Reassembler;
|
private readonly reassembler: Reassembler;
|
||||||
private readonly receiptIdFormat: SmsIdFormat | undefined;
|
private readonly receiptIdNotation: SmsIdNotation | undefined;
|
||||||
private readonly session: Session;
|
private readonly session: Session;
|
||||||
private readonly systemId: string;
|
private readonly systemId: string;
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ export class IncomingRequests {
|
|||||||
maxOctets: options.maxOctets,
|
maxOctets: options.maxOctets,
|
||||||
timeout: options.reassemblyTimeout ?? defaults.reassemblyTimeout,
|
timeout: options.reassemblyTimeout ?? defaults.reassemblyTimeout,
|
||||||
});
|
});
|
||||||
this.receiptIdFormat = options.receiptIdFormat;
|
this.receiptIdNotation = options.receiptIdNotation;
|
||||||
this.session = options.session;
|
this.session = options.session;
|
||||||
this.systemId = options.systemId ?? defaults.systemId;
|
this.systemId = options.systemId ?? defaults.systemId;
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,7 @@ export class IncomingRequests {
|
|||||||
|
|
||||||
/** SMPP carries a mobile-originated message and a delivery receipt on the same command. */
|
/** SMPP carries a mobile-originated message and a delivery receipt on the same command. */
|
||||||
private async onDeliverSm(pduObj: PduObject): Promise<void> {
|
private async onDeliverSm(pduObj: PduObject): Promise<void> {
|
||||||
const dlr = dlrFromPdu(pduObj, this.receiptIdFormat);
|
const dlr = dlrFromPdu(pduObj, this.receiptIdNotation);
|
||||||
|
|
||||||
if (!dlr) {
|
if (!dlr) {
|
||||||
this.onMessage(pduObj);
|
this.onMessage(pduObj);
|
||||||
|
|||||||
+1
-1
@@ -39,7 +39,7 @@ export type { SendRespOptions, Sms, SmsInput } from './sms.ts';
|
|||||||
export type { ConcatInfo } from './udh.ts';
|
export type { ConcatInfo } from './udh.ts';
|
||||||
export type { Result, VoidResult } from './result.ts';
|
export type { Result, VoidResult } from './result.ts';
|
||||||
export type { SmppLog } from './log.ts';
|
export type { SmppLog } from './log.ts';
|
||||||
export type { SmsIdFormat, SmsIdFormats } from './sms-id.ts';
|
export type { SmsIdFormat, SmsIdNotation } from './sms-id.ts';
|
||||||
export type {
|
export type {
|
||||||
AuthenticateInput,
|
AuthenticateInput,
|
||||||
AuthenticateResult,
|
AuthenticateResult,
|
||||||
|
|||||||
+5
-5
@@ -3,7 +3,7 @@ import type { ParamValue } from './defs/types.ts';
|
|||||||
import type { PduObject, PduObjectInput } from './pdu.ts';
|
import type { PduObject, PduObjectInput } from './pdu.ts';
|
||||||
import type { Result } from './result.ts';
|
import type { Result } from './result.ts';
|
||||||
import type { SmppLog } from './log.ts';
|
import type { SmppLog } from './log.ts';
|
||||||
import type { SmsIdFormat } from './sms-id.ts';
|
import type { SmsIdNotation } from './sms-id.ts';
|
||||||
import { consts } from './defs/constants.ts';
|
import { consts } from './defs/constants.ts';
|
||||||
import { detect } from './defs/encodings.ts';
|
import { detect } from './defs/encodings.ts';
|
||||||
import { normaliseSmsId } from './sms-id.ts';
|
import { normaliseSmsId } from './sms-id.ts';
|
||||||
@@ -34,7 +34,7 @@ export type SendSmsResult = { err?: Error; pduObjs: PduObject[]; smsIds: string[
|
|||||||
export type SendSmsDeps = {
|
export type SendSmsDeps = {
|
||||||
log: SmppLog;
|
log: SmppLog;
|
||||||
reference: number;
|
reference: number;
|
||||||
respIdFormat?: SmsIdFormat | undefined;
|
respIdNotation?: SmsIdNotation | undefined;
|
||||||
send: (input: PduObjectInput) => Promise<Result<{ pduObj: PduObject }>>;
|
send: (input: PduObjectInput) => Promise<Result<{ pduObj: PduObject }>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ function checkSegments(allowed: number, segments: number): Error | undefined {
|
|||||||
|
|
||||||
function collectSent(
|
function collectSent(
|
||||||
sent: Result<{ pduObj: PduObject }>[],
|
sent: Result<{ pduObj: PduObject }>[],
|
||||||
format: SmsIdFormat | undefined,
|
notation: SmsIdNotation | undefined,
|
||||||
): SendSmsResult {
|
): SendSmsResult {
|
||||||
const pduObjs: PduObject[] = [];
|
const pduObjs: PduObject[] = [];
|
||||||
const smsIds: string[] = [];
|
const smsIds: string[] = [];
|
||||||
@@ -114,7 +114,7 @@ function collectSent(
|
|||||||
failure ??= one.err;
|
failure ??= one.err;
|
||||||
} else if (one.pduObj.cmdStatus === 'ESME_ROK') {
|
} else if (one.pduObj.cmdStatus === 'ESME_ROK') {
|
||||||
pduObjs.push(one.pduObj);
|
pduObjs.push(one.pduObj);
|
||||||
smsIds.push(normaliseSmsId(paramText(one.pduObj.params.message_id), format));
|
smsIds.push(normaliseSmsId(paramText(one.pduObj.params.message_id), notation));
|
||||||
} else {
|
} else {
|
||||||
const refusal = one.pduObj.cmdStatus ?? String(one.pduObj.cmdStatusId);
|
const refusal = one.pduObj.cmdStatus ?? String(one.pduObj.cmdStatusId);
|
||||||
|
|
||||||
@@ -144,5 +144,5 @@ export async function submitSms(deps: SendSmsDeps, sms: SendSmsOptions): Promise
|
|||||||
params: submitSmParams(sms, segment, { encoding, multipart }),
|
params: submitSmParams(sms, segment, { encoding, multipart }),
|
||||||
})));
|
})));
|
||||||
|
|
||||||
return collectSent(sent, deps.respIdFormat);
|
return collectSent(sent, deps.respIdNotation);
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-14
@@ -4,10 +4,10 @@ import type { PduObject } from './pdu.ts';
|
|||||||
import type { Result, VoidResult } from './result.ts';
|
import type { Result, VoidResult } from './result.ts';
|
||||||
import type { Session } from './session.ts';
|
import type { Session } from './session.ts';
|
||||||
import type { SmppLog } from './log.ts';
|
import type { SmppLog } from './log.ts';
|
||||||
import type { SmsIdFormats } from './sms-id.ts';
|
import type { SmsIdFormat } from './sms-id.ts';
|
||||||
import type { Sms } from './sms.ts';
|
import type { Sms } from './sms.ts';
|
||||||
import type { Socket } from 'node:net';
|
import type { Socket } from 'node:net';
|
||||||
import { isSmsIdFormat } from './sms-id.ts';
|
import { isSmsIdNotation, smsIdNotations, smsIdPlaces } from './sms-id.ts';
|
||||||
|
|
||||||
export type SessionEvents = {
|
export type SessionEvents = {
|
||||||
close: [];
|
close: [];
|
||||||
@@ -86,7 +86,7 @@ export type SessionOptions = {
|
|||||||
/** How long a drain waits for the requests already on the wire. 0 waits forever. */
|
/** How long a drain waits for the requests already on the wire. 0 waits forever. */
|
||||||
shutdownTimeout?: number | undefined;
|
shutdownTimeout?: number | undefined;
|
||||||
/** The notation the peer writes message ids in, where it is not the one they are compared in. */
|
/** The notation the peer writes message ids in, where it is not the one they are compared in. */
|
||||||
smsIdFormat?: SmsIdFormats | undefined;
|
smsIdFormat?: SmsIdFormat | undefined;
|
||||||
sock: Socket;
|
sock: Socket;
|
||||||
/** This end's own identity, answered to the peer in place of the one it sent. */
|
/** This end's own identity, answered to the peer in place of the one it sent. */
|
||||||
systemId?: string | undefined;
|
systemId?: string | undefined;
|
||||||
@@ -131,19 +131,31 @@ export function checkSessionOptions(options: CheckableOptions): VoidResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return checkSmsIdFormats(options.smsIdFormat);
|
return checkSmsIdFormat(options.smsIdFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkSmsIdFormats(smsIdFormat: CheckableOptions['smsIdFormat']): VoidResult {
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
const formats: [string, string | undefined][] = [
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||||
['receipt', smsIdFormat?.receipt],
|
|
||||||
['submitResp', smsIdFormat?.submitResp],
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const [place, format] of formats) {
|
|
||||||
if (format !== undefined && !isSmsIdFormat(format)) {
|
|
||||||
return { err: new Error(`smsIdFormat.${place} must be decimal or hex, got ${format}`) };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkSmsIdFormat(smsIdFormat: unknown): VoidResult {
|
||||||
|
if (smsIdFormat === undefined) return {};
|
||||||
|
|
||||||
|
if (!isRecord(smsIdFormat)) {
|
||||||
|
return { err: new Error('smsIdFormat names a notation per place, as { receipt, submitResp }') };
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowed = smsIdNotations.join(' or ');
|
||||||
|
|
||||||
|
for (const place of smsIdPlaces) {
|
||||||
|
const notation = smsIdFormat[place];
|
||||||
|
|
||||||
|
if (notation === undefined || isSmsIdNotation(notation)) continue;
|
||||||
|
|
||||||
|
// String() throws on a null-prototype object, and this value is whatever the caller passed.
|
||||||
|
const got = typeof notation === 'string' ? notation : typeof notation;
|
||||||
|
|
||||||
|
return { err: new Error(`smsIdFormat.${place} must be ${allowed}, got ${got}`) };
|
||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
@@ -157,5 +169,5 @@ export type CheckableOptions = {
|
|||||||
reassemblyTimeout?: number | undefined;
|
reassemblyTimeout?: number | undefined;
|
||||||
responseTimeout?: number | undefined;
|
responseTimeout?: number | undefined;
|
||||||
shutdownTimeout?: number | undefined;
|
shutdownTimeout?: number | undefined;
|
||||||
smsIdFormat?: { receipt?: string | undefined; submitResp?: string | undefined } | undefined;
|
smsIdFormat?: unknown;
|
||||||
};
|
};
|
||||||
|
|||||||
+2
-2
@@ -120,7 +120,7 @@ export class Session extends EventEmitter<SessionEvents> {
|
|||||||
maxReassembly: options.maxReassembly,
|
maxReassembly: options.maxReassembly,
|
||||||
onRequest: options.onRequest,
|
onRequest: options.onRequest,
|
||||||
reassemblyTimeout: options.reassemblyTimeout,
|
reassemblyTimeout: options.reassemblyTimeout,
|
||||||
receiptIdFormat: options.smsIdFormat?.receipt,
|
receiptIdNotation: options.smsIdFormat?.receipt,
|
||||||
session: this,
|
session: this,
|
||||||
systemId: options.systemId,
|
systemId: options.systemId,
|
||||||
});
|
});
|
||||||
@@ -210,7 +210,7 @@ export class Session extends EventEmitter<SessionEvents> {
|
|||||||
const sent = await submitSms({
|
const sent = await submitSms({
|
||||||
log: this.log,
|
log: this.log,
|
||||||
reference: this.nextConcatReference(),
|
reference: this.nextConcatReference(),
|
||||||
respIdFormat: this.options.smsIdFormat?.submitResp,
|
respIdNotation: this.options.smsIdFormat?.submitResp,
|
||||||
send: input => this.send(input, options),
|
send: input => this.send(input, options),
|
||||||
}, sms);
|
}, sms);
|
||||||
|
|
||||||
|
|||||||
+25
-20
@@ -1,32 +1,37 @@
|
|||||||
/** The notation a peer writes message ids in. */
|
|
||||||
export type SmsIdFormat = 'decimal' | 'hex';
|
|
||||||
|
|
||||||
/** The notation per place the peer writes an id. An omitted place is left as it arrived. */
|
|
||||||
export type SmsIdFormats = {
|
|
||||||
receipt?: SmsIdFormat | undefined;
|
|
||||||
submitResp?: SmsIdFormat | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function isSmsIdFormat(value: unknown): value is SmsIdFormat {
|
|
||||||
return value === 'decimal' || value === 'hex';
|
|
||||||
}
|
|
||||||
|
|
||||||
// SMPP 3.4 caps message_id at 64 octets, and BigInt on a longer string is a peer-controlled cost.
|
|
||||||
const maxIdLength = 64;
|
|
||||||
|
|
||||||
const notations = {
|
const notations = {
|
||||||
decimal: { digits: /^[0-9]+$/, prefix: '' },
|
decimal: { digits: /^[0-9]+$/, prefix: '' },
|
||||||
hex: { digits: /^[0-9a-f]+$/i, prefix: '0x' },
|
hex: { digits: /^[0-9a-f]+$/i, prefix: '0x' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** The notation a peer writes message ids in. */
|
||||||
|
export type SmsIdNotation = keyof typeof notations;
|
||||||
|
|
||||||
|
/** The notation per place the peer writes an id. An omitted place is left as it arrived. */
|
||||||
|
export type SmsIdFormat = {
|
||||||
|
receipt?: SmsIdNotation | undefined;
|
||||||
|
submitResp?: SmsIdNotation | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const smsIdNotations: string[] = Object.keys(notations);
|
||||||
|
|
||||||
|
export const smsIdPlaces: (keyof SmsIdFormat)[] = ['receipt', 'submitResp'];
|
||||||
|
|
||||||
|
export function isSmsIdNotation(value: unknown): value is SmsIdNotation {
|
||||||
|
return typeof value === 'string' && Object.hasOwn(notations, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// SMPP 3.4 caps message_id at 64 octets, and BigInt on a longer string is a peer-controlled cost.
|
||||||
|
const maxIdLength = 64;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The id as a plain decimal value, so an SMSC that answers a submit in one notation and writes the
|
* The id as a plain decimal value, so an SMSC that answers a submit in one notation and writes the
|
||||||
* receipt in another still correlates. An id the notation cannot read is left as it arrived.
|
* receipt in another still correlates. An id the notation cannot read is left as it arrived, which
|
||||||
|
* is what leaves a `<base>-<n>` id whole for DlrMerger.
|
||||||
*/
|
*/
|
||||||
export function normaliseSmsId(id: string, format: SmsIdFormat | undefined): string {
|
export function normaliseSmsId(id: string, notation: SmsIdNotation | undefined): string {
|
||||||
if (format === undefined || id.length > maxIdLength) return id;
|
if (notation === undefined || id.length > maxIdLength) return id;
|
||||||
|
|
||||||
const { digits, prefix } = notations[format];
|
const { digits, prefix } = notations[notation];
|
||||||
|
|
||||||
return digits.test(id) ? BigInt(`${prefix}${id}`).toString(10) : id;
|
return digits.test(id) ? BigInt(`${prefix}${id}`).toString(10) : id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -872,6 +872,8 @@ describe('message id notation', () => {
|
|||||||
|
|
||||||
assert.ok(checked.err instanceof Error);
|
assert.ok(checked.err instanceof Error);
|
||||||
assert.match(checked.err.message, /smsIdFormat\.receipt/);
|
assert.match(checked.err.message, /smsIdFormat\.receipt/);
|
||||||
|
// The shape todo.md sketched, which a caller without types would otherwise pass unnoticed.
|
||||||
|
assert.ok(checkSessionOptions({ smsIdFormat: 'hex' }).err instanceof Error);
|
||||||
assert.equal(checkSessionOptions({ smsIdFormat: { submitResp: 'hex' } }).err, undefined);
|
assert.equal(checkSessionOptions({ smsIdFormat: { submitResp: 'hex' } }).err, undefined);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test, { describe } from 'node:test';
|
||||||
|
import { normaliseSmsId } from '../src/sms-id.ts';
|
||||||
|
|
||||||
|
describe('normaliseSmsId()', () => {
|
||||||
|
test('reads an id the length a message_id may be, and leaves a longer one alone', () => {
|
||||||
|
const padded = `0${'1'.repeat(63)}`;
|
||||||
|
const tooLong = `0${'1'.repeat(64)}`;
|
||||||
|
|
||||||
|
assert.equal(normaliseSmsId(padded, 'decimal'), '1'.repeat(63));
|
||||||
|
assert.equal(normaliseSmsId(tooLong, 'decimal'), tooLong);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('leaves an id the notation cannot read as it arrived', () => {
|
||||||
|
assert.equal(normaliseSmsId('', 'hex'), '');
|
||||||
|
assert.equal(normaliseSmsId('0x1f', 'hex'), '0x1f');
|
||||||
|
assert.equal(normaliseSmsId('beef-1', 'hex'), 'beef-1', 'the segment convention stays whole');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reads either case of a hexadecimal id', () => {
|
||||||
|
assert.equal(normaliseSmsId('1a2B', 'hex'), normaliseSmsId('1A2b', 'hex'));
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user