@dszp/netsapiens-lib 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -11
- package/dist/attribution.d.ts.map +1 -1
- package/dist/attribution.js +65 -28
- package/dist/attribution.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/inventory.d.ts +190 -6
- package/dist/inventory.d.ts.map +1 -1
- package/dist/inventory.js +206 -12
- package/dist/inventory.js.map +1 -1
- package/dist/model.d.ts +13 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/nsClient.d.ts +8 -2
- package/dist/nsClient.d.ts.map +1 -1
- package/dist/nsClient.js +6 -1
- package/dist/nsClient.js.map +1 -1
- package/dist/resolver.d.ts +18 -0
- package/dist/resolver.d.ts.map +1 -1
- package/dist/resolver.js +40 -19
- package/dist/resolver.js.map +1 -1
- package/package.json +1 -1
- package/src/attribution.selftest.ts +75 -0
- package/src/attribution.ts +60 -18
- package/src/index.ts +1 -1
- package/src/inventory.selftest.ts +279 -5
- package/src/inventory.ts +356 -19
- package/src/model.ts +13 -0
- package/src/nsClient.ts +14 -3
- package/src/resolver.selftest.ts +22 -1
- package/src/resolver.ts +42 -19
package/dist/inventory.d.ts
CHANGED
|
@@ -38,6 +38,35 @@
|
|
|
38
38
|
* case-insensitively, and on nothing else — not the `dial-rule-description`, which is a portal-written
|
|
39
39
|
* note an operator can edit. **With no hosts supplied nothing is a fax line**, because a library that
|
|
40
40
|
* hardcoded one deployment's fax server would be wrong everywhere else.
|
|
41
|
+
*
|
|
42
|
+
* ## E911: the ENDPOINT is the billable unit, the address is a location
|
|
43
|
+
*
|
|
44
|
+
* An **Emergency Endpoint** is a callback number, a caller name, a billing address and a vendor. It is
|
|
45
|
+
* what the E911 carrier routes on and what it bills per, and it is counted as `e911Endpoints`. An
|
|
46
|
+
* **Emergency Address** is a dispatchable location forwarded to responders; several of them can sit
|
|
47
|
+
* under one endpoint, and nobody bills them. `e911Addresses` stays, as information.
|
|
48
|
+
*
|
|
49
|
+
* Users, devices and sites point at an endpoint through their Emergency Caller ID
|
|
50
|
+
* (`caller-id-number-emergency`) matching the endpoint's callback number. This library then INFERS two
|
|
51
|
+
* fallbacks for a blank field — a blank `emergency-address-id` reads as the domain default address, and
|
|
52
|
+
* a blank caller ID reads as that address's endpoint. **Neither is a measured platform behaviour**; see
|
|
53
|
+
* the ⚠️ on {@link EmergencyModel} for what is known and what is assumed. They live in
|
|
54
|
+
* {@link resolveEmergency} alone, so the counter and `attribution.ts` cannot disagree about them, and
|
|
55
|
+
* they fail closed: nothing to inherit leaves a user referencing nothing rather than referencing a
|
|
56
|
+
* guess.
|
|
57
|
+
*
|
|
58
|
+
* ## Legacy emergency numbers, which have no API object at all
|
|
59
|
+
*
|
|
60
|
+
* A domain still on the legacy provisioning model has no endpoint records. Every one of its users
|
|
61
|
+
* carries an EMPTY `emergency-address-id` and a `caller-id-number-emergency` set to one of a handful of
|
|
62
|
+
* DIDs — and the carrier bills per one of those DIDs, exactly as it bills per endpoint on the new
|
|
63
|
+
* model. `e911Legacy` is therefore the count of DISTINCT such numbers, and a rulebook can count the two
|
|
64
|
+
* dimensions together so one retail E911 line pays for either model.
|
|
65
|
+
*
|
|
66
|
+
* A number that IS an endpoint callback is excluded, because a half-migrated domain that counted it in
|
|
67
|
+
* both dimensions would bill the same place twice. That exclusion is also the precondition: with no
|
|
68
|
+
* endpoint list read there is nothing to exclude against, so `e911Legacy` is 0 whenever
|
|
69
|
+
* `snapshot.addressEndpoints` is `undefined` rather than an array. See {@link DomainInventory.e911Legacy}.
|
|
41
70
|
*/
|
|
42
71
|
import type { Rec, Snapshot } from './model.js';
|
|
43
72
|
export interface DomainInventory {
|
|
@@ -63,10 +92,12 @@ export interface DomainInventory {
|
|
|
63
92
|
/** Extensions whose `voicemail-transcription-enabled` is anything but empty or `no`. */
|
|
64
93
|
transcriptionEnabled: number;
|
|
65
94
|
/**
|
|
66
|
-
* Extensions with a Microsoft Teams connector device — one whose device
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
95
|
+
* Extensions with a Microsoft Teams connector device — one whose device-name SUFFIX the legend marks
|
|
96
|
+
* `teams: true`, which under {@link DEFAULT_DEVICE_SUFFIXES} is the extension number followed by `t`
|
|
97
|
+
* (`1000t`), how the TeamMate connector registers. See {@link deviceName} for which field that name is
|
|
98
|
+
* read from, and why reading the wrong one silently miscounted this. That device is NOT counted under
|
|
99
|
+
* `devices`: it is a connector, not a handset. A supplied legend with no `teams` suffix — a deployment
|
|
100
|
+
* without TeamMate — leaves this 0 and counts every device as a handset.
|
|
70
101
|
*/
|
|
71
102
|
teamsConnected: number;
|
|
72
103
|
/**
|
|
@@ -86,8 +117,25 @@ export interface DomainInventory {
|
|
|
86
117
|
fax: number;
|
|
87
118
|
all: number;
|
|
88
119
|
};
|
|
89
|
-
/**
|
|
120
|
+
/**
|
|
121
|
+
* E911 address records on the domain — dispatchable LOCATIONS, and information only. The billable
|
|
122
|
+
* unit is {@link DomainInventory.e911Endpoints}; see the module doc.
|
|
123
|
+
*/
|
|
90
124
|
e911Addresses: number;
|
|
125
|
+
/** Provisioned Emergency Endpoints — the unit the E911 carrier bills per. */
|
|
126
|
+
e911Endpoints: number;
|
|
127
|
+
/**
|
|
128
|
+
* Distinct legacy emergency numbers — the pre-endpoint model, which has no API object of its own.
|
|
129
|
+
* Comparable with {@link DomainInventory.e911Endpoints}, and never overlapping it **provided the
|
|
130
|
+
* snapshot carries an endpoint list**: the two are kept apart by excluding numbers that are already
|
|
131
|
+
* endpoint callbacks, which needs the endpoints to have been read.
|
|
132
|
+
*
|
|
133
|
+
* So this is **0 whenever `snapshot.addressEndpoints` is `undefined`** — the fetch never asked, and a
|
|
134
|
+
* count derived from the users alone would report a fully-migrated domain's every emergency caller ID
|
|
135
|
+
* as a line to bill for. An empty ARRAY is the other fact — asked, and the domain has none — and that
|
|
136
|
+
* one does support a count. Fetch with `includeAddresses: true` (see `fetchDomainSnapshot`).
|
|
137
|
+
*/
|
|
138
|
+
e911Legacy: number;
|
|
91
139
|
/** SMS-enabled numbers on the domain. */
|
|
92
140
|
smsNumbers: number;
|
|
93
141
|
/** Devices belonging to real extensions only — a system user's device is not a seat. */
|
|
@@ -132,11 +180,20 @@ export interface ExtensionItem {
|
|
|
132
180
|
* has no model), and `teams` marks the connector entry itself. `deviceCount`/`deviceModels`/`teams`
|
|
133
181
|
* above stay handset-only; this list is the one place a connector's own row shows up. Never the MAC
|
|
134
182
|
* or the SIP password.
|
|
183
|
+
*
|
|
184
|
+
* `suffix` is what the name carries AFTER the extension number (`1001wp` on ext `1001` → `wp`; a bare
|
|
185
|
+
* `1001` → `''`), lower-cased; a name that does not start with the extension has no suffix, and neither
|
|
186
|
+
* does a device on an extension with no number. `kind` is that suffix's label from the legend in
|
|
187
|
+
* {@link InventoryOptions.deviceSuffixes} — `''` when the suffix is empty OR when the legend does not
|
|
188
|
+
* carry it, because an unlisted suffix is a name this deployment has not explained, not a device type
|
|
189
|
+
* to guess at.
|
|
135
190
|
*/
|
|
136
191
|
devices: Array<{
|
|
137
192
|
name: string;
|
|
138
193
|
model: string;
|
|
139
194
|
teams: boolean;
|
|
195
|
+
suffix: string;
|
|
196
|
+
kind: string;
|
|
140
197
|
}>;
|
|
141
198
|
/** `deviceCount > 0 || teams` — has a device of any kind, handset or connector. */
|
|
142
199
|
anyDevice: boolean;
|
|
@@ -162,11 +219,43 @@ export interface AddressItem {
|
|
|
162
219
|
key: string;
|
|
163
220
|
label: string;
|
|
164
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* One provisioned Emergency Endpoint — the thing the E911 carrier bills per.
|
|
224
|
+
*
|
|
225
|
+
* An allowlist like every other item: the callback, the caller name, and the billing address reduced to
|
|
226
|
+
* the ONE line {@link AddressItem} already exposes. The endpoint record also carries a geolocation XML
|
|
227
|
+
* and a public IP, and neither belongs in front of a billing operator.
|
|
228
|
+
*/
|
|
229
|
+
export interface EndpointItem {
|
|
230
|
+
/** `e911:<callback>` — the digits, so a device's 11-digit form and the record's 10-digit form are one
|
|
231
|
+
* key. `e911:~<hash>` when the record names no callback at all. */
|
|
232
|
+
key: string;
|
|
233
|
+
/** The callback number, digits only ({@link emergencyDigits}); `''` when the record has none. */
|
|
234
|
+
callback: string;
|
|
235
|
+
/** `caller-name` — who the carrier announces; `''` when blank. */
|
|
236
|
+
callerName: string;
|
|
237
|
+
/** Street and city, one line — no more of the billing address than the address list already shows. */
|
|
238
|
+
billingAddress: string;
|
|
239
|
+
/** How many users the RECORD says are configured on it (`count-users-configured`), 0 when it says nothing. */
|
|
240
|
+
users: number;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* One legacy emergency number: a `caller-id-number-emergency` in use on a domain that has no endpoint
|
|
244
|
+
* records for it. Derived from the users, because the legacy model has no object of its own to read.
|
|
245
|
+
*/
|
|
246
|
+
export interface LegacyE911Item {
|
|
247
|
+
/** `e911legacy:<digits>`. Never a hash fallback — a blank number is not one of these. */
|
|
248
|
+
key: string;
|
|
249
|
+
/** The number, digits only ({@link emergencyDigits}). */
|
|
250
|
+
number: string;
|
|
251
|
+
/** How many real extensions reference it — counted here, not read off a record. */
|
|
252
|
+
users: number;
|
|
253
|
+
}
|
|
165
254
|
export interface SmsItem {
|
|
166
255
|
key: string;
|
|
167
256
|
number: string;
|
|
168
257
|
}
|
|
169
|
-
export type InventoryItem = ExtensionItem | NumberItem | AddressItem | SmsItem;
|
|
258
|
+
export type InventoryItem = ExtensionItem | NumberItem | AddressItem | EndpointItem | LegacyE911Item | SmsItem;
|
|
170
259
|
export interface DomainInventoryDetail {
|
|
171
260
|
/** Real seats only, same rule as the count. */
|
|
172
261
|
extensions: ExtensionItem[];
|
|
@@ -174,8 +263,24 @@ export interface DomainInventoryDetail {
|
|
|
174
263
|
systemUsers: ExtensionItem[];
|
|
175
264
|
dids: NumberItem[];
|
|
176
265
|
e911Addresses: AddressItem[];
|
|
266
|
+
e911Endpoints: EndpointItem[];
|
|
267
|
+
e911Legacy: LegacyE911Item[];
|
|
177
268
|
smsNumbers: SmsItem[];
|
|
178
269
|
}
|
|
270
|
+
/** A device-name suffix legend: `suffix → { label, teams? }`. See {@link InventoryOptions.deviceSuffixes}. */
|
|
271
|
+
export type DeviceSuffixLegend = Record<string, {
|
|
272
|
+
label: string;
|
|
273
|
+
teams?: boolean;
|
|
274
|
+
}>;
|
|
275
|
+
/**
|
|
276
|
+
* The three device-name suffixes NetSapiens itself ships — SNAPmobile Web, SNAPmobile, and the TeamMate
|
|
277
|
+
* Microsoft Teams connector. Used whenever a caller supplies no `deviceSuffixes`, and it is the same
|
|
278
|
+
* table `resolver.ts` names a simultaneous-ring device by, so a suffix means one thing in this library.
|
|
279
|
+
*
|
|
280
|
+
* A deployment's OWN suffixes (a white-labelled app, say) are not here and never will be: they belong to
|
|
281
|
+
* the operator, who supplies them through {@link InventoryOptions.deviceSuffixes}.
|
|
282
|
+
*/
|
|
283
|
+
export declare const DEFAULT_DEVICE_SUFFIXES: Readonly<DeviceSuffixLegend>;
|
|
179
284
|
/**
|
|
180
285
|
* What the caller has to tell the counter that the API cannot. Optional in full: every option absent is
|
|
181
286
|
* the pre-0.7.0 behaviour, and no option changes what a snapshot has to contain.
|
|
@@ -188,16 +293,95 @@ export interface InventoryOptions {
|
|
|
188
293
|
* caller's to supply.
|
|
189
294
|
*/
|
|
190
295
|
faxServerHosts?: readonly string[];
|
|
296
|
+
/**
|
|
297
|
+
* What a device-name SUFFIX means on this deployment: `suffix → { label, teams? }`. Compared
|
|
298
|
+
* case-insensitively, and it REPLACES {@link DEFAULT_DEVICE_SUFFIXES} wholesale rather than merging
|
|
299
|
+
* with it — a deployment that has no TeamMate omits `t` and Teams detection is then off entirely,
|
|
300
|
+
* which a merge could not express.
|
|
301
|
+
*
|
|
302
|
+
* `teams: true` marks the suffix that names a Microsoft Teams CONNECTOR rather than a handset: it is
|
|
303
|
+
* what {@link ExtensionItem.teams} and {@link DomainInventory.teamsConnected} test, and what keeps the
|
|
304
|
+
* connector out of `deviceCount`/`deviceModels`. At most one suffix normally carries it, but nothing
|
|
305
|
+
* here requires that.
|
|
306
|
+
*/
|
|
307
|
+
deviceSuffixes?: DeviceSuffixLegend;
|
|
191
308
|
}
|
|
192
309
|
export declare const str: (v: unknown) => string;
|
|
193
310
|
/** Is this user one of NetSapiens' internal routing objects rather than a seat? */
|
|
194
311
|
export declare function isSystemUser(user: Rec): boolean;
|
|
312
|
+
/**
|
|
313
|
+
* An Emergency Caller ID reduced to what two records can be compared on: its digits, with an 11-digit
|
|
314
|
+
* `1NXXNXXXXXX` collapsed to its 10-digit form so a device's spelling matches an endpoint's.
|
|
315
|
+
*
|
|
316
|
+
* `''` for "not set", which the API says three ways: empty, absent, and the `[*]` wildcard the portal
|
|
317
|
+
* renders as "Select a Caller ID for 911 calls". Treating `[*]` as a value would give every unset
|
|
318
|
+
* device on a domain one shared fake endpoint.
|
|
319
|
+
*/
|
|
320
|
+
export declare function emergencyDigits(v: unknown): string;
|
|
195
321
|
/**
|
|
196
322
|
* One record per non-blank `user`, first one wins. The same rule `attribution.ts` needs to join a
|
|
197
323
|
* number's `dial-rule-translation-destination-user` (or an SMS number, or an address) back to the
|
|
198
324
|
* user it belongs to — extracted here so there is exactly one copy of it in the library.
|
|
199
325
|
*/
|
|
200
326
|
export declare function usersByExt(users: Rec[]): Map<string, Rec>;
|
|
327
|
+
/**
|
|
328
|
+
* How a domain's E911 records join up, and the two INHERITANCES this library reads into a blank field.
|
|
329
|
+
*
|
|
330
|
+
* Both the counter and `attribution.ts` need to answer "which endpoint does this user reference?" and
|
|
331
|
+
* "which address?". Resolved in one place so the count and the site attribution cannot disagree about
|
|
332
|
+
* who references what.
|
|
333
|
+
*
|
|
334
|
+
* ⚠️ **Both inheritances are this library's inference, not a measured platform behaviour.** Two
|
|
335
|
+
* separate assumptions sit under them, and a consumer relying on the placement should know which:
|
|
336
|
+
*
|
|
337
|
+
* 1. **That a blank field falls back to the domain default at all.** A user with a blank
|
|
338
|
+
* `caller-id-number-emergency` is read here as referencing the default address's endpoint, and one
|
|
339
|
+
* with a blank `emergency-address-id` as referencing the default address. That is a plausible
|
|
340
|
+
* reading of a record the portal badges "Domain Default" — but it has not been confirmed against a
|
|
341
|
+
* live 911 call, and the same state can be read as an E911 GAP rather than an inheritance.
|
|
342
|
+
* 2. **That the default address's callback is joined by `address-name`.** An address record carries no
|
|
343
|
+
* callback field of its own (checked against a live domain and 34 captured snapshots), so the only
|
|
344
|
+
* thing tying the domain default to an endpoint is that the endpoint names the same address. Every
|
|
345
|
+
* captured domain carrying both agreed on that name.
|
|
346
|
+
*
|
|
347
|
+
* Both fail CLOSED. No default address, no endpoint naming it, or a name that does not match, and
|
|
348
|
+
* `defaultCallback` is `''` — the users who would have inherited it reference nothing and are left
|
|
349
|
+
* unattributed, rather than being attached to a guess. The COUNTS are unaffected either way
|
|
350
|
+
* (`e911Endpoints` is a record count, and a user with both fields blank is correctly not legacy); what
|
|
351
|
+
* these assumptions move is PLACEMENT, which on a split domain decides which accounts are told they
|
|
352
|
+
* need an E911 line.
|
|
353
|
+
*/
|
|
354
|
+
export interface EmergencyModel {
|
|
355
|
+
/** The `emergency-address-id` of the record marked `domain_default`; `''` when the domain has none. */
|
|
356
|
+
defaultAddressId: string;
|
|
357
|
+
/** The callback of the endpoint bound to the DEFAULT address, digits only; `''` when there is none. */
|
|
358
|
+
defaultCallback: string;
|
|
359
|
+
/** Every provisioned endpoint's callback, digits only — the "is this number already an endpoint?" test. */
|
|
360
|
+
endpointCallbacks: Set<string>;
|
|
361
|
+
/** Which address a user references: their own field, the domain default when it is blank. */
|
|
362
|
+
addressIdFor: (user: Rec) => string;
|
|
363
|
+
/** The callback a user SETS: their own field, else any of their devices'; `''` when neither does. */
|
|
364
|
+
setCallbackFor: (user: Rec) => string;
|
|
365
|
+
/** Which endpoint a user references: {@link EmergencyModel.setCallbackFor}, else the domain default's. */
|
|
366
|
+
callbackFor: (user: Rec) => string;
|
|
367
|
+
}
|
|
368
|
+
export declare function resolveEmergency(snapshot: Snapshot): EmergencyModel;
|
|
369
|
+
/**
|
|
370
|
+
* Is this user on the LEGACY emergency model — a caller ID set by hand, with no address record behind
|
|
371
|
+
* it and no endpoint provisioned for the number?
|
|
372
|
+
*
|
|
373
|
+
* All three clauses matter. A blank `emergency-address-id` alone is not legacy: a user with BOTH fields
|
|
374
|
+
* blank inherits the domain default address, which is the new model working as designed. And a number
|
|
375
|
+
* that IS an endpoint callback is the new model too — counting it here as well would bill a
|
|
376
|
+
* half-migrated domain twice for one place.
|
|
377
|
+
*
|
|
378
|
+
* ⚠️ **The `em` must come from a snapshot whose endpoints were READ.** The third clause tests against
|
|
379
|
+
* `em.endpointCallbacks`, which is empty both when the domain has no endpoints and when nobody asked
|
|
380
|
+
* for them — so on a snapshot fetched without `includeAddresses` this answers "legacy" for every user
|
|
381
|
+
* on a fully-migrated domain. {@link listDomainInventory} refuses to derive the list at all in that
|
|
382
|
+
* state; a caller using this predicate directly has to make the same check.
|
|
383
|
+
*/
|
|
384
|
+
export declare function legacyEmergencyNumber(user: Rec, em: EmergencyModel): string;
|
|
201
385
|
/**
|
|
202
386
|
* Where a phone number routes, in words a person reads at a glance — not the raw NetSapiens dial
|
|
203
387
|
* rule fields. Pure; looks the destination user up in `userByExt` ({@link usersByExt}) so it can
|
package/dist/inventory.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inventory.d.ts","sourceRoot":"","sources":["../src/inventory.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"inventory.d.ts","sourceRoot":"","sources":["../src/inventory.ts"],"names":[],"mappings":"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"}
|
package/dist/inventory.js
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three device-name suffixes NetSapiens itself ships — SNAPmobile Web, SNAPmobile, and the TeamMate
|
|
3
|
+
* Microsoft Teams connector. Used whenever a caller supplies no `deviceSuffixes`, and it is the same
|
|
4
|
+
* table `resolver.ts` names a simultaneous-ring device by, so a suffix means one thing in this library.
|
|
5
|
+
*
|
|
6
|
+
* A deployment's OWN suffixes (a white-labelled app, say) are not here and never will be: they belong to
|
|
7
|
+
* the operator, who supplies them through {@link InventoryOptions.deviceSuffixes}.
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_DEVICE_SUFFIXES = Object.freeze({
|
|
10
|
+
wp: { label: 'SNAPmobile Web' },
|
|
11
|
+
m: { label: 'SNAPmobile' },
|
|
12
|
+
t: { label: 'Teams', teams: true },
|
|
13
|
+
});
|
|
1
14
|
/** NANP toll-free area codes, 800 through 888. A number outside this set is counted local. */
|
|
2
15
|
const TOLL_FREE = new Set(['800', '833', '844', '855', '866', '877', '888']);
|
|
3
16
|
export const str = (v) => (typeof v === 'string' ? v.trim() : v == null ? '' : String(v).trim());
|
|
@@ -64,6 +77,25 @@ function isFaxLine(p, hosts) {
|
|
|
64
77
|
return false;
|
|
65
78
|
return hosts.has(str(p['dial-rule-translation-destination-host']).toLowerCase());
|
|
66
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* An Emergency Caller ID reduced to what two records can be compared on: its digits, with an 11-digit
|
|
82
|
+
* `1NXXNXXXXXX` collapsed to its 10-digit form so a device's spelling matches an endpoint's.
|
|
83
|
+
*
|
|
84
|
+
* `''` for "not set", which the API says three ways: empty, absent, and the `[*]` wildcard the portal
|
|
85
|
+
* renders as "Select a Caller ID for 911 calls". Treating `[*]` as a value would give every unset
|
|
86
|
+
* device on a domain one shared fake endpoint.
|
|
87
|
+
*/
|
|
88
|
+
export function emergencyDigits(v) {
|
|
89
|
+
const raw = str(v);
|
|
90
|
+
if (!raw || raw === '[*]')
|
|
91
|
+
return '';
|
|
92
|
+
const digits = raw.replace(/\D+/g, '');
|
|
93
|
+
if (!digits)
|
|
94
|
+
return '';
|
|
95
|
+
return digits.length === 11 && digits.startsWith('1') ? digits.slice(1) : digits;
|
|
96
|
+
}
|
|
97
|
+
/** A NetSapiens boolean, which arrives as a JSON `true` from one endpoint and as `"yes"` from another. */
|
|
98
|
+
const flag = (v) => v === true || ['yes', 'true', '1'].includes(str(v).toLowerCase());
|
|
67
99
|
/**
|
|
68
100
|
* A device's NAME — the local part of its SIP URI (`sip:103t@acme.example` → `103t`), which is the short
|
|
69
101
|
* id the portal shows and the string the Teams test matches against.
|
|
@@ -80,11 +112,46 @@ function deviceName(device) {
|
|
|
80
112
|
const at = a.indexOf('@');
|
|
81
113
|
return at === -1 ? a : a.slice(0, at);
|
|
82
114
|
}
|
|
83
|
-
|
|
115
|
+
/**
|
|
116
|
+
* The legend, lower-cased once so every lookup is a case-insensitive hit rather than a scan. A caller's
|
|
117
|
+
* legend REPLACES the default; two keys differing only in case collapse, last one wins, which is the
|
|
118
|
+
* only sane reading of a case-insensitive table.
|
|
119
|
+
*/
|
|
120
|
+
function suffixLegend(opts) {
|
|
121
|
+
const src = opts?.deviceSuffixes ?? DEFAULT_DEVICE_SUFFIXES;
|
|
122
|
+
// Prototype-free: the key is a device-name suffix off a snapshot, so `constructor`, `toString` and
|
|
123
|
+
// `hasOwnProperty` are all reachable keys, and on a plain object each would answer with something
|
|
124
|
+
// inherited. `Object.entries` copies own enumerable keys only, so nothing inherited gets in either.
|
|
125
|
+
const out = Object.create(null);
|
|
126
|
+
for (const [k, v] of Object.entries(src))
|
|
127
|
+
out[k.trim().toLowerCase()] = v;
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* What a device's name carries AFTER the extension number, lower-cased: `1001wp` on ext `1001` → `wp`,
|
|
132
|
+
* a bare `1001` → `''`. A name that does not start with the extension has no suffix at all — `sales1` on
|
|
133
|
+
* ext `1001` is a differently-named device, not a device of kind `sales1` — and neither does anything on
|
|
134
|
+
* an extension with no number, which is what keeps a device NAMED a bare `t` off the Teams legend.
|
|
135
|
+
*/
|
|
136
|
+
function deviceSuffix(name, ext) {
|
|
137
|
+
if (!ext || !name.startsWith(ext))
|
|
138
|
+
return '';
|
|
139
|
+
return name.slice(ext.length).toLowerCase();
|
|
140
|
+
}
|
|
141
|
+
function extensionItem(u, devices, legend) {
|
|
84
142
|
const ext = str(u.user);
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
const
|
|
143
|
+
// Every device, once: its name, its suffix, and what the legend says that suffix is. Computed here and
|
|
144
|
+
// read three times below, so the Teams test, the handset filter and the display list cannot disagree.
|
|
145
|
+
const rows = devices.map((d) => {
|
|
146
|
+
const name = deviceName(d);
|
|
147
|
+
const suffix = deviceSuffix(name, ext);
|
|
148
|
+
const entry = suffix ? legend[suffix] : undefined;
|
|
149
|
+
return { d, name, suffix, kind: entry?.label ?? '', teams: entry?.teams === true };
|
|
150
|
+
});
|
|
151
|
+
// A CONNECTOR is a device whose suffix the legend marks `teams` — under the default legend that is
|
|
152
|
+
// `<ext>t` and nothing else, which is exactly the test this replaced. A legend without a `teams`
|
|
153
|
+
// suffix has no connectors, and every device on the extension is a handset.
|
|
154
|
+
const handsets = rows.filter((r) => !r.teams);
|
|
88
155
|
const transcription = str(u['voicemail-transcription-enabled']).toLowerCase();
|
|
89
156
|
const teams = handsets.length !== devices.length;
|
|
90
157
|
const name = `${str(u['name-first-name'])} ${str(u['name-last-name'])}`.trim();
|
|
@@ -102,13 +169,15 @@ function extensionItem(u, devices) {
|
|
|
102
169
|
deviceCount: handsets.length,
|
|
103
170
|
// A device whose model is blank is listed under a named bucket rather than dropped: a missing
|
|
104
171
|
// model is a provisioning gap worth seeing, and a silently smaller total hides it.
|
|
105
|
-
deviceModels: handsets.map((
|
|
172
|
+
deviceModels: handsets.map((r) => str(r.d['device-models-model']) || '(unknown)'),
|
|
106
173
|
// Every device, including the Teams connector — this is a display list, not a seat count.
|
|
107
|
-
devices:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
174
|
+
devices: rows.map((r) => ({
|
|
175
|
+
name: r.name,
|
|
176
|
+
model: r.teams ? '' : str(r.d['device-models-model']) || '(unknown)',
|
|
177
|
+
teams: r.teams,
|
|
178
|
+
suffix: r.suffix,
|
|
179
|
+
kind: r.kind,
|
|
180
|
+
})),
|
|
112
181
|
anyDevice: handsets.length > 0 || teams,
|
|
113
182
|
};
|
|
114
183
|
}
|
|
@@ -126,6 +195,63 @@ export function usersByExt(users) {
|
|
|
126
195
|
}
|
|
127
196
|
return map;
|
|
128
197
|
}
|
|
198
|
+
export function resolveEmergency(snapshot) {
|
|
199
|
+
const addresses = Array.isArray(snapshot.addresses) ? snapshot.addresses : [];
|
|
200
|
+
const endpoints = Array.isArray(snapshot.addressEndpoints) ? snapshot.addressEndpoints : [];
|
|
201
|
+
const devicesByUser = (snapshot.devicesByUser ?? {});
|
|
202
|
+
// First one wins. Two records marked default is a provisioning fault, and picking one of them
|
|
203
|
+
// silently is better than resolving every blank user to nothing because two records disagree.
|
|
204
|
+
const def = addresses.find((a) => flag(a['domain_default']));
|
|
205
|
+
const defaultAddressId = def ? str(def['emergency-address-id']) : '';
|
|
206
|
+
const defName = def ? str(def['address-name']).toLowerCase() : '';
|
|
207
|
+
const boundToDefault = defName ? endpoints.find((e) => str(e['address-name']).toLowerCase() === defName) : undefined;
|
|
208
|
+
// NB: on an ENDPOINT record `emergency-address-id` holds the callback NUMBER, not an address id.
|
|
209
|
+
const defaultCallback = boundToDefault ? emergencyDigits(boundToDefault['emergency-address-id']) : '';
|
|
210
|
+
const endpointCallbacks = new Set(endpoints.map((e) => emergencyDigits(e['emergency-address-id'])).filter(Boolean));
|
|
211
|
+
const setCallbackFor = (user) => {
|
|
212
|
+
const own = emergencyDigits(user['caller-id-number-emergency']);
|
|
213
|
+
if (own)
|
|
214
|
+
return own;
|
|
215
|
+
// A user who sets none can still have a handset that does — the portal reads the device's own
|
|
216
|
+
// setting first and only then the user's, so a domain whose numbers live on the devices is
|
|
217
|
+
// invisible to a rule that reads the user record alone.
|
|
218
|
+
for (const d of devicesByUser[str(user.user)] ?? []) {
|
|
219
|
+
const dev = emergencyDigits(d['caller-id-number-emergency']);
|
|
220
|
+
if (dev)
|
|
221
|
+
return dev;
|
|
222
|
+
}
|
|
223
|
+
return '';
|
|
224
|
+
};
|
|
225
|
+
return {
|
|
226
|
+
defaultAddressId,
|
|
227
|
+
defaultCallback,
|
|
228
|
+
endpointCallbacks,
|
|
229
|
+
addressIdFor: (user) => str(user['emergency-address-id']) || defaultAddressId,
|
|
230
|
+
setCallbackFor,
|
|
231
|
+
callbackFor: (user) => setCallbackFor(user) || defaultCallback,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Is this user on the LEGACY emergency model — a caller ID set by hand, with no address record behind
|
|
236
|
+
* it and no endpoint provisioned for the number?
|
|
237
|
+
*
|
|
238
|
+
* All three clauses matter. A blank `emergency-address-id` alone is not legacy: a user with BOTH fields
|
|
239
|
+
* blank inherits the domain default address, which is the new model working as designed. And a number
|
|
240
|
+
* that IS an endpoint callback is the new model too — counting it here as well would bill a
|
|
241
|
+
* half-migrated domain twice for one place.
|
|
242
|
+
*
|
|
243
|
+
* ⚠️ **The `em` must come from a snapshot whose endpoints were READ.** The third clause tests against
|
|
244
|
+
* `em.endpointCallbacks`, which is empty both when the domain has no endpoints and when nobody asked
|
|
245
|
+
* for them — so on a snapshot fetched without `includeAddresses` this answers "legacy" for every user
|
|
246
|
+
* on a fully-migrated domain. {@link listDomainInventory} refuses to derive the list at all in that
|
|
247
|
+
* state; a caller using this predicate directly has to make the same check.
|
|
248
|
+
*/
|
|
249
|
+
export function legacyEmergencyNumber(user, em) {
|
|
250
|
+
if (str(user['emergency-address-id']))
|
|
251
|
+
return '';
|
|
252
|
+
const n = em.setCallbackFor(user);
|
|
253
|
+
return n && !em.endpointCallbacks.has(n) ? n : '';
|
|
254
|
+
}
|
|
129
255
|
/**
|
|
130
256
|
* Where a phone number routes, in words a person reads at a glance — not the raw NetSapiens dial
|
|
131
257
|
* rule fields. Pure; looks the destination user up in `userByExt` ({@link usersByExt}) so it can
|
|
@@ -198,6 +324,8 @@ export function listDomainInventory(snapshot, opts) {
|
|
|
198
324
|
const smsnumbers = Array.isArray(snapshot.smsnumbers) ? snapshot.smsnumbers : [];
|
|
199
325
|
const extensions = [];
|
|
200
326
|
const systemUsers = [];
|
|
327
|
+
// Normalised once, not per extension: the legend is the caller's and does not change mid-fold.
|
|
328
|
+
const legend = suffixLegend(opts);
|
|
201
329
|
for (let i = 0; i < users.length; i++) {
|
|
202
330
|
const u = users[i];
|
|
203
331
|
const ext = str(u.user);
|
|
@@ -207,7 +335,7 @@ export function listDomainInventory(snapshot, opts) {
|
|
|
207
335
|
// from a backup or a fixture. Dropping it would read as a clean match on a domain that has
|
|
208
336
|
// handsets nobody can see; two blank users sharing one list overcount instead, which is a
|
|
209
337
|
// visible drift an operator investigates, and that is the failure worth having.
|
|
210
|
-
const item = extensionItem(u, devicesByUser[ext] ?? []);
|
|
338
|
+
const item = extensionItem(u, devicesByUser[ext] ?? [], legend);
|
|
211
339
|
(isSystemUser(u) ? systemUsers : extensions).push(item);
|
|
212
340
|
}
|
|
213
341
|
const userByExt = usersByExt(users);
|
|
@@ -237,11 +365,51 @@ export function listDomainInventory(snapshot, opts) {
|
|
|
237
365
|
label: label || id || `(address ${i + 1})`,
|
|
238
366
|
};
|
|
239
367
|
});
|
|
368
|
+
// The two E911 lists share one resolution of the domain's inheritance — see `resolveEmergency`.
|
|
369
|
+
const em = resolveEmergency(snapshot);
|
|
370
|
+
const endpoints = Array.isArray(snapshot.addressEndpoints) ? snapshot.addressEndpoints : [];
|
|
371
|
+
const e911Endpoints = endpoints.map((e) => {
|
|
372
|
+
// NB: `emergency-address-id` on an ENDPOINT record is the callback NUMBER. See `Snapshot`.
|
|
373
|
+
const callback = emergencyDigits(e['emergency-address-id']);
|
|
374
|
+
const callerName = str(e['caller-name']);
|
|
375
|
+
const line1 = str(e['address-line-1']);
|
|
376
|
+
const city = str(e['address-city']);
|
|
377
|
+
return {
|
|
378
|
+
key: identityKey('e911', callback, `${callerName} ${line1} ${city}`),
|
|
379
|
+
callback,
|
|
380
|
+
callerName,
|
|
381
|
+
billingAddress: [line1, city].filter(Boolean).join(', '),
|
|
382
|
+
// `count-users-configured` and NOT `sub_count_total`: the two disagree on live records (a
|
|
383
|
+
// captured endpoint had 0 and 16), and only the first one names what it counts.
|
|
384
|
+
users: Number(e['count-users-configured'] ?? 0) || 0,
|
|
385
|
+
};
|
|
386
|
+
});
|
|
387
|
+
// Legacy numbers are DERIVED — there is no record to map over. One entry per distinct number, in the
|
|
388
|
+
// order the users first name it, so the list does not reshuffle between two reads of one domain.
|
|
389
|
+
//
|
|
390
|
+
// ⚠️ ONLY when the endpoint list was actually READ. `snapshot.addressEndpoints` is `undefined` when
|
|
391
|
+
// the fetch never asked for it and `[]` when it asked and the domain has none, and the difference
|
|
392
|
+
// decides whether this list can exist at all: the legacy test excludes numbers that are already
|
|
393
|
+
// endpoint callbacks, and with no endpoint list there is nothing to exclude against — so a domain
|
|
394
|
+
// fully on the ENDPOINT model, read with `includeAddresses` off, would report every distinct
|
|
395
|
+
// emergency caller ID as a legacy line the carrier bills for. `e911Addresses` answering 0 in that
|
|
396
|
+
// state is a safe under-count; this answering N is a confident over-count that looks like real data.
|
|
397
|
+
const legacyUsers = new Map();
|
|
398
|
+
if (Array.isArray(snapshot.addressEndpoints)) {
|
|
399
|
+
for (const u of users) {
|
|
400
|
+
if (isSystemUser(u))
|
|
401
|
+
continue;
|
|
402
|
+
const n = legacyEmergencyNumber(u, em);
|
|
403
|
+
if (n)
|
|
404
|
+
legacyUsers.set(n, (legacyUsers.get(n) ?? 0) + 1);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const e911Legacy = [...legacyUsers].map(([number, count]) => ({ key: `e911legacy:${number}`, number, users: count }));
|
|
240
408
|
const smsNumbers = smsnumbers.map((s) => {
|
|
241
409
|
const number = str(s.number);
|
|
242
410
|
return { key: identityKey('sms', number, JSON.stringify({ number })), number };
|
|
243
411
|
});
|
|
244
|
-
return { extensions, systemUsers, dids, e911Addresses, smsNumbers };
|
|
412
|
+
return { extensions, systemUsers, dids, e911Addresses, e911Endpoints, e911Legacy, smsNumbers };
|
|
245
413
|
}
|
|
246
414
|
/** The counts, as a fold over {@link listDomainInventory} so the two can never disagree. */
|
|
247
415
|
export function countDomainInventory(snapshot, opts) {
|
|
@@ -259,6 +427,11 @@ export function countInventoryDetail(d) {
|
|
|
259
427
|
teamsConnected: 0,
|
|
260
428
|
dids: { total: 0, tollFree: 0, local: 0, fax: 0, all: d.dids.length },
|
|
261
429
|
e911Addresses: d.e911Addresses.length,
|
|
430
|
+
// `?? []` on the two newest lists alone: a detail object cached or serialised by a consumer running
|
|
431
|
+
// an older version of this library has neither field, and a count that threw on it would take out a
|
|
432
|
+
// whole page over a dimension that did not exist when the entry was written.
|
|
433
|
+
e911Endpoints: (d.e911Endpoints ?? []).length,
|
|
434
|
+
e911Legacy: (d.e911Legacy ?? []).length,
|
|
262
435
|
smsNumbers: d.smsNumbers.length,
|
|
263
436
|
devices: { total: 0, byModel: {} },
|
|
264
437
|
};
|
|
@@ -344,6 +517,10 @@ export function itemsFor(detail, path) {
|
|
|
344
517
|
return detail.dids;
|
|
345
518
|
if (path === 'e911Addresses')
|
|
346
519
|
return detail.e911Addresses;
|
|
520
|
+
if (path === 'e911Endpoints')
|
|
521
|
+
return detail.e911Endpoints ?? [];
|
|
522
|
+
if (path === 'e911Legacy')
|
|
523
|
+
return detail.e911Legacy ?? [];
|
|
347
524
|
if (path === 'smsNumbers')
|
|
348
525
|
return detail.smsNumbers;
|
|
349
526
|
return undefined;
|
|
@@ -358,5 +535,22 @@ export function itemLabel(item) {
|
|
|
358
535
|
return item.kind === 'tollFree' ? `${item.number} (toll-free)` : item.number;
|
|
359
536
|
if ('label' in item)
|
|
360
537
|
return item.label;
|
|
538
|
+
// An ENDPOINT is named by the number the carrier bills, then by who it announces and where it sends
|
|
539
|
+
// responders. Either half is dropped when blank rather than printed against a dangling dash, and a
|
|
540
|
+
// record with NEITHER falls back to its derived key — the same shape an address with nothing to name
|
|
541
|
+
// it by gets, except the id here is the key rather than a position, so two blank-callback endpoints
|
|
542
|
+
// stay apart. Never the empty string: this label is what a consumer writes into its acceptance
|
|
543
|
+
// history, and a row that cannot name its own item is worse than an ugly one.
|
|
544
|
+
if ('callback' in item) {
|
|
545
|
+
const who = [item.callerName, item.billingAddress].filter(Boolean).join(', ');
|
|
546
|
+
if (item.callback)
|
|
547
|
+
return who ? `${item.callback} — ${who}` : item.callback;
|
|
548
|
+
return who || `(endpoint ${item.key.slice('e911:'.length)})`;
|
|
549
|
+
}
|
|
550
|
+
// A LEGACY number says so on its own line: it looks like a DID, and nothing else on the page would
|
|
551
|
+
// tell a reader why a bare number is sitting on an E911 row. Singular is written out: a label that
|
|
552
|
+
// does not agree with itself reads as a rendering fault, and this one is frozen into history rows.
|
|
553
|
+
if ('users' in item)
|
|
554
|
+
return `${item.number} — legacy E911 (${item.users} user${item.users === 1 ? '' : 's'})`;
|
|
361
555
|
return item.number;
|
|
362
556
|
}
|