@dszp/netsapiens-lib 0.6.0 → 0.7.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 CHANGED
@@ -79,7 +79,7 @@ genuine zero from a read that never completed rather than silently undercounting
79
79
 
80
80
  ### Counting a domain: `countDomainInventory`
81
81
 
82
- `countDomainInventory(snapshot)` is pure — it fetches nothing, and turns a `Snapshot` (from
82
+ `countDomainInventory(snapshot, opts?)` is pure — it fetches nothing, and turns a `Snapshot` (from
83
83
  `fetchDomainSnapshot`, a backup, or a fixture) into a fixed tree of numeric leaves along the dimensions a
84
84
  VoIP operator actually sells on:
85
85
 
@@ -92,7 +92,8 @@ VoIP operator actually sells on:
92
92
  - `transcriptionEnabled` — extensions with voicemail transcription on.
93
93
  - `teamsConnected` — extensions with a Microsoft Teams connector device (SIP `aor` local part
94
94
  `<ext>t`). That connector is excluded from `devices`/`deviceCount`: it is a connector, not a handset.
95
- - `dids` — phone numbers, `total` / `tollFree` / `local`.
95
+ - `dids` — phone numbers: `total` / `tollFree` / `local`, all three **excluding fax lines**, plus
96
+ `fax` and `all` (everything, `total + fax === all`). See [Fax lines](#fax-lines).
96
97
  - `e911Addresses`, `smsNumbers` — record counts.
97
98
  - `devices` — `total` and `byModel`, real extensions only (a system user's device is not a seat).
98
99
 
@@ -116,9 +117,33 @@ inventory.dids.tollFree; // e.g. 3
116
117
  inventory.devices.byModel; // e.g. { "Yealink T54W": 12, "(unknown)": 1 }
117
118
  ```
118
119
 
120
+ #### Fax lines
121
+
122
+ On the portal's **Fax Server** treatment a fax line is an ordinary phone number whose dial rule hands it
123
+ to a fax server: `dial-rule-application: to-connection` and `dial-rule-translation-destination-host` set
124
+ to that server's host. The API has no fax-account endpoint and the ATA is not a device on any user, so
125
+ that host is the only thing that says "fax line" — and nothing in the API distinguishes an analog fax
126
+ from a digital one.
127
+
128
+ The host belongs to your deployment, so you supply it; **with no hosts, nothing is a fax line** and the
129
+ counts are exactly what they were before 0.7.0:
130
+
131
+ ```ts
132
+ const inventory = countDomainInventory(snapshot, { faxServerHosts: ['203.0.113.7'] });
133
+ inventory.dids.fax; // 2 — billed as fax lines
134
+ inventory.dids.total; // the DIDs, those two NOT among them
135
+ inventory.dids.all; // total + fax
136
+ ```
137
+
138
+ Matching is on the trimmed host, case-insensitively, and on nothing else — never the
139
+ `dial-rule-description`, which is a note the portal writes and an operator can edit. A fax line's
140
+ `NumberItem.fax` is `true`, its `destination` reads `to fax server` (the host is deliberately not shown),
141
+ and it keeps its `kind`: a fax number is local or toll-free like any other, it is simply not counted as a
142
+ DID.
143
+
119
144
  #### Listing a domain: `listDomainInventory`
120
145
 
121
- `countDomainInventory` is a fold over `listDomainInventory(snapshot)`, which returns the per-item lists
146
+ `countDomainInventory` is a fold over `listDomainInventory(snapshot, opts?)` (same options), which returns the per-item lists
122
147
  behind those counts — for anything that shows an operator *which* extension or number a count refers to,
123
148
  not just how many. Same allowlist discipline as the counts: a device's MAC, SIP credentials and email
124
149
  never appear, though names and sites now do (that's the point of a list). Each item carries a stable
@@ -131,8 +156,9 @@ never appear, though names and sites now do (that's the point of a list). Each i
131
156
  | `e911Addresses` | `addr:<emergency-address-id>` |
132
157
  | `smsNumbers` | `sms:<number>` |
133
158
 
134
- A number carries `destination` — where it routes, in words (`to user 100 — Ann Lee`, `to queue 701 —
135
- Sales`), built by the exported `destinationOf(p, userByExt)` over `usersByExt(users)` and `description`
159
+ A number carries `fax` (see [Fax lines](#fax-lines)) and `destination` — where it routes, in words
160
+ (`to user 100 Ann Lee`, `to queue 701 — Sales`, `to fax server`), built by the exported
161
+ `destinationOf(p, userByExt, faxServerHosts?)` over `usersByExt(users)` — and `description`
136
162
  (`dial-rule-description`, trimmed); an extension carries `devices`, an `{ name, model, teams }` per
137
163
  device it has (handset and Teams connector alike), in record order — still never the MAC, SIP password
138
164
  or email.
@@ -150,7 +176,9 @@ no separate lookup table:
150
176
  | `extensions.withAnyDevice` / `extensions.withNoDevice` | extensions with / without any device (handset or Teams connector) |
151
177
  | `transcriptionEnabled` | extensions with transcription on |
152
178
  | `teamsConnected` | extensions with a Teams connector |
153
- | `dids.total` / `dids.tollFree` / `dids.local` | phone numbers |
179
+ | `dids.total` / `dids.tollFree` / `dids.local` | phone numbers, **fax lines excluded** |
180
+ | `dids.fax` | fax lines |
181
+ | `dids.all` | every phone number, fax lines included |
154
182
  | `e911Addresses` | E911 addresses |
155
183
  | `smsNumbers` | SMS numbers |
156
184
 
package/dist/index.d.ts CHANGED
@@ -16,7 +16,7 @@ export { THEMES, DEFAULT_LIGHT_THEME, DEFAULT_DARK_THEME, NODE_LIGHT, NODE_DARK,
16
16
  export { renderGalleryHtml, renderFlowCards, renderFlowCard, mermaidBootstrap, flowAnchorId, type GalleryOptions, type CardOptions, } from './html.js';
17
17
  export { resolveSvgSize, rasterizerScript } from './raster.js';
18
18
  export { NsClient, NsApiError, assertBareServer, fetchDomainSnapshot, listDomains, asArray, type NsClientConfig, type FetchSnapshotOptions } from './nsClient.js';
19
- export { countDomainInventory, countInventoryDetail, listDomainInventory, itemsFor, itemLabel, destinationOf, usersByExt, type DomainInventory, type DomainInventoryDetail, type ExtensionItem, type NumberItem, type AddressItem, type SmsItem, type InventoryItem } from './inventory.js';
19
+ export { countDomainInventory, countInventoryDetail, listDomainInventory, itemsFor, itemLabel, destinationOf, usersByExt, type DomainInventory, type DomainInventoryDetail, type InventoryOptions, type ExtensionItem, type NumberItem, type AddressItem, type SmsItem, type InventoryItem } from './inventory.js';
20
20
  export { attributeDomainInventory, type DomainAttribution, type ItemAttribution } from './attribution.js';
21
21
  export { NsWriteClient, type NsWriteClientConfig } from './nsWriteClient.js';
22
22
  export { supportsSynchronous, SYNCHRONOUS_OPERATIONS, type SynchronousMethod, type SynchronousOperation, } from './nsSynchronous.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAkB,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAE,SAAS,EAAuC,MAAM,cAAc,CAAC;AAC9E,OAAO,EACL,MAAM,EACN,mBAAmB,EACnB,kBAAkB,EAClB,UAAU,EACV,SAAS,EACT,UAAU,EACV,SAAS,GAKV,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,YAAY,GAGb,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,WAAW,EAAE,OAAO,EAAkD,MAAM,eAAe,CAAC;AAClK,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,QAAQ,EAAE,SAAS,EAAE,aAAa,EAAE,UAAU,EAA6I,MAAM,gBAAgB,CAAC;AAC5R,OAAO,EAAE,wBAAwB,EAAgD,MAAM,kBAAkB,CAAC;AAC1G,OAAO,EAAE,aAAa,EAA4B,MAAM,oBAAoB,CAAC;AAC7E,OAAO,EACL,mBAAmB,EACnB,sBAAsB,GAGvB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,YAAY,GAIb,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,WAAW,EAAiD,MAAM,mBAAmB,CAAC;AAC7G,OAAO,EACL,qBAAqB,EACrB,2BAA2B,EAC3B,mBAAmB,EACnB,mBAAmB,EACnB,UAAU,EACV,eAAe,EACf,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,GAUlB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,MAAM,EACN,iBAAiB,EACjB,cAAc,EACd,YAAY,EACZ,oBAAoB,EACpB,cAAc,EACd,QAAQ,EACR,kBAAkB,GAOnB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAwB,cAAc,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAC1F,OAAO,EACL,WAAW,EACX,aAAa,EACb,eAAe,EACf,YAAY,GAIb,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,WAAW,EACX,SAAS,EACT,GAAG,GAIJ,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,mBAAmB,GAOpB,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAkB,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAE,SAAS,EAAuC,MAAM,cAAc,CAAC;AAC9E,OAAO,EACL,MAAM,EACN,mBAAmB,EACnB,kBAAkB,EAClB,UAAU,EACV,SAAS,EACT,UAAU,EACV,SAAS,GAKV,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,YAAY,GAGb,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,WAAW,EAAE,OAAO,EAAkD,MAAM,eAAe,CAAC;AAClK,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,QAAQ,EAAE,SAAS,EAAE,aAAa,EAAE,UAAU,EAAoK,MAAM,gBAAgB,CAAC;AACnT,OAAO,EAAE,wBAAwB,EAAgD,MAAM,kBAAkB,CAAC;AAC1G,OAAO,EAAE,aAAa,EAA4B,MAAM,oBAAoB,CAAC;AAC7E,OAAO,EACL,mBAAmB,EACnB,sBAAsB,GAGvB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,YAAY,GAIb,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,WAAW,EAAiD,MAAM,mBAAmB,CAAC;AAC7G,OAAO,EACL,qBAAqB,EACrB,2BAA2B,EAC3B,mBAAmB,EACnB,mBAAmB,EACnB,UAAU,EACV,eAAe,EACf,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,GAUlB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,MAAM,EACN,iBAAiB,EACjB,cAAc,EACd,YAAY,EACZ,oBAAoB,EACpB,cAAc,EACd,QAAQ,EACR,kBAAkB,GAOnB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAwB,cAAc,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAC1F,OAAO,EACL,WAAW,EACX,aAAa,EACb,eAAe,EACf,YAAY,GAIb,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,WAAW,EACX,SAAS,EACT,GAAG,GAIJ,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,mBAAmB,GAOpB,MAAM,kBAAkB,CAAC"}
@@ -9,9 +9,9 @@
9
9
  * A device record from NetSapiens carries the SIP registration password. `listDomainInventory` builds
10
10
  * per-item lists — an extension's name and site, a number's kind and routing destination, an address's
11
11
  * label — from an allowlist of named fields, and `countDomainInventory` is a fold over those same
12
- * lists. The allowlist now includes a device's NAME (its `aor` local part, e.g. `101b` the same
13
- * short id the portal shows) alongside its model; a device's MAC, SIP password and email never appear,
14
- * and nothing here should be added that carries one.
12
+ * lists. The allowlist now includes a device's NAME (the local part of its `device` SIP URI, falling back
13
+ * to `aor` — e.g. `101b`, the same short id the portal shows) alongside its model; a device's MAC, SIP
14
+ * password and email never appear, and nothing here should be added that carries one.
15
15
  *
16
16
  * ## Every countable dimension is a numeric leaf
17
17
  *
@@ -24,6 +24,20 @@
24
24
  * A user whose `service-code` is empty or does not begin with `system-`. NetSapiens models auto
25
25
  * attendants, queues and time-of-day routers as users, and counting them as seats would overstate
26
26
  * every domain that has any. They are counted separately, as information, and never compared.
27
+ *
28
+ * ## What counts as a fax line
29
+ *
30
+ * On the portal's "Fax Server" treatment a number is an ordinary phone number whose dial rule hands it
31
+ * to a fax server host — `dial-rule-application: to-connection` with
32
+ * `dial-rule-translation-destination-host` set to that host. There is no fax-account endpoint and the
33
+ * ATA is not a device on any user, so the host is the only thing in the API that says "this is a fax
34
+ * line", and nothing in the API can tell an analog fax from a digital one.
35
+ *
36
+ * The host is therefore the whole test, and it is the CALLER's: pass `{ faxServerHosts }` to
37
+ * {@link listDomainInventory} or {@link countDomainInventory}. Matching is on the trimmed host,
38
+ * case-insensitively, and on nothing else — not the `dial-rule-description`, which is a portal-written
39
+ * note an operator can edit. **With no hosts supplied nothing is a fax line**, because a library that
40
+ * hardcoded one deployment's fax server would be wrong everywhere else.
27
41
  */
28
42
  import type { Rec, Snapshot } from './model.js';
29
43
  export interface DomainInventory {
@@ -49,16 +63,28 @@ export interface DomainInventory {
49
63
  /** Extensions whose `voicemail-transcription-enabled` is anything but empty or `no`. */
50
64
  transcriptionEnabled: number;
51
65
  /**
52
- * Extensions with a Microsoft Teams connector device — one whose SIP `aor` local part is the
53
- * extension number followed by `t` (`1000t`), which is how the TeamMate connector registers.
66
+ * Extensions with a Microsoft Teams connector device — one whose device NAME is the extension number
67
+ * followed by `t` (`1000t`), which is how the TeamMate connector registers. See {@link deviceName} for
68
+ * which field that name is read from, and why reading the wrong one silently miscounted this.
54
69
  * That device is NOT counted under `devices`: it is a connector, not a handset.
55
70
  */
56
71
  teamsConnected: number;
57
- /** Phone numbers on the domain, split by NANP toll-free prefix. */
72
+ /**
73
+ * Phone numbers on the domain, split by NANP toll-free prefix — **fax lines excluded**.
74
+ *
75
+ * A number handed to the fax server is billed as a fax line, not as a DID, so `total`, `tollFree`
76
+ * and `local` all leave it out and `fax` counts it instead. `all` is every phone number the domain
77
+ * holds, fax lines included: `total + fax === all`.
78
+ *
79
+ * With no `faxServerHosts` supplied nothing is a fax line, `fax` is 0 and `total === all` — the
80
+ * numbers this returned before 0.7.0, unchanged.
81
+ */
58
82
  dids: {
59
83
  total: number;
60
84
  tollFree: number;
61
85
  local: number;
86
+ fax: number;
87
+ all: number;
62
88
  };
63
89
  /** E911 address records on the domain. */
64
90
  e911Addresses: number;
@@ -99,8 +125,9 @@ export interface ExtensionItem {
99
125
  /** `device-models-model` per handset, `(unknown)` when blank. Never the MAC. */
100
126
  deviceModels: string[];
101
127
  /**
102
- * Every device on this extension, in record order, connector included: `name` is the `aor` local
103
- * part (`sip:101b@acme.example` → `101b`) — the device NAME as the portal shows it — `model` is
128
+ * Every device on this extension, in record order, connector included: `name` is the local part of its
129
+ * `device` SIP URI, or of `aor` when that is all the record has (`sip:101b@acme.example` → `101b`) —
130
+ * the device NAME as the portal shows it — `model` is
104
131
  * `device-models-model` (`(unknown)` when blank on a handset, `''` for the Teams connector, which
105
132
  * has no model), and `teams` marks the connector entry itself. `deviceCount`/`deviceModels`/`teams`
106
133
  * above stay handset-only; this list is the one place a connector's own row shows up. Never the MAC
@@ -118,6 +145,14 @@ export interface NumberItem {
118
145
  key: string;
119
146
  number: string;
120
147
  kind: 'local' | 'tollFree';
148
+ /**
149
+ * This number is handed to a fax server — see the module doc. `false` whenever the caller supplied no
150
+ * `faxServerHosts`, since without a host list nothing here can tell a fax line from any other number.
151
+ *
152
+ * `kind` is still set on a fax line (a fax number is local or toll-free like any other), but the
153
+ * COUNTS exclude it from `dids.total`/`local`/`tollFree` and count it under `dids.fax` instead.
154
+ */
155
+ fax: boolean;
121
156
  /** Where the number routes, for a person: see {@link destinationOf}. `''` when the record says nothing. */
122
157
  destination: string;
123
158
  /** `dial-rule-description` trimmed — the note the portal writes ("Portal Created: User - 1001"); `''` when blank. */
@@ -141,6 +176,19 @@ export interface DomainInventoryDetail {
141
176
  e911Addresses: AddressItem[];
142
177
  smsNumbers: SmsItem[];
143
178
  }
179
+ /**
180
+ * What the caller has to tell the counter that the API cannot. Optional in full: every option absent is
181
+ * the pre-0.7.0 behaviour, and no option changes what a snapshot has to contain.
182
+ */
183
+ export interface InventoryOptions {
184
+ /**
185
+ * The hosts a fax line is handed to — an IP or a hostname, as it appears in
186
+ * `dial-rule-translation-destination-host`. Compared trimmed and case-insensitively; blanks are
187
+ * ignored. Absent or empty means NO number is a fax line. See the module doc for why this is the
188
+ * caller's to supply.
189
+ */
190
+ faxServerHosts?: readonly string[];
191
+ }
144
192
  export declare const str: (v: unknown) => string;
145
193
  /** Is this user one of NetSapiens' internal routing objects rather than a seat? */
146
194
  export declare function isSystemUser(user: Rec): boolean;
@@ -170,8 +218,13 @@ export declare function usersByExt(users: Rec[]): Map<string, Rec>;
170
218
  * - No destination but an application is set → `to <application>` (`to-connection` → `to connection`,
171
219
  * `to-voicemail` → `to voicemail`).
172
220
  * - Neither is set → `''`.
221
+ *
222
+ * A FAX LINE — a number whose destination host is one of `faxServerHosts` — short-circuits all of that
223
+ * and reads `to fax server`, host omitted. Otherwise it would render as `to connection` (which names
224
+ * plumbing, not a destination) or, on a rule that also carries a destination user, as a bare IP address
225
+ * beside a customer's phone number. Nobody reading this line needs the fax server's address.
173
226
  */
174
- export declare function destinationOf(p: Rec, userByExt: Map<string, Rec>): string;
227
+ export declare function destinationOf(p: Rec, userByExt: Map<string, Rec>, faxServerHosts?: readonly string[]): string;
175
228
  /**
176
229
  * The items behind every count. Pure. Fields are copied by name from an allowlist; no record passes
177
230
  * through, so a device's MAC or SIP password cannot reach a consumer by accident.
@@ -189,9 +242,9 @@ export declare function destinationOf(p: Rec, userByExt: Map<string, Rec>): stri
189
242
  * countable thing, and one derived row is more honest than two that shuffle. A blank id is a
190
243
  * provisioning fault to fix; the fallback only keeps the distinguishable ones apart until it is.
191
244
  */
192
- export declare function listDomainInventory(snapshot: Snapshot): DomainInventoryDetail;
245
+ export declare function listDomainInventory(snapshot: Snapshot, opts?: InventoryOptions): DomainInventoryDetail;
193
246
  /** The counts, as a fold over {@link listDomainInventory} so the two can never disagree. */
194
- export declare function countDomainInventory(snapshot: Snapshot): DomainInventory;
247
+ export declare function countDomainInventory(snapshot: Snapshot, opts?: InventoryOptions): DomainInventory;
195
248
  /**
196
249
  * Count an item list. Exposed separately so a consumer that has FILTERED the lists — to one site, to
197
250
  * one billing account — gets counts that agree with what it kept, rather than re-counting the snapshot.
@@ -1 +1 @@
1
- {"version":3,"file":"inventory.d.ts","sourceRoot":"","sources":["../src/inventory.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"inventory.d.ts","sourceRoot":"","sources":["../src/inventory.ts"],"names":[],"mappings":"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"}
package/dist/inventory.js CHANGED
@@ -43,17 +43,48 @@ function isTollFree(raw) {
43
43
  const nanp = digits.length === 11 && digits.startsWith('1') ? digits.slice(1) : digits;
44
44
  return nanp.length === 10 && TOLL_FREE.has(nanp.slice(0, 3));
45
45
  }
46
- /** The local part of a device's `aor` (`sip:103t@acme.example` `103t`), read only to test for Teams. */
47
- function aorLocal(device) {
48
- const a = str(device.aor).replace(/^sip:/i, '');
46
+ /** The fax-server hosts, trimmed, lower-cased and with blanks dropped the shape {@link isFaxLine} tests against. */
47
+ function faxHosts(hosts) {
48
+ const out = new Set();
49
+ for (const h of hosts ?? []) {
50
+ const v = str(h).toLowerCase();
51
+ if (v)
52
+ out.add(v);
53
+ }
54
+ return out;
55
+ }
56
+ /**
57
+ * Is this number handed to a fax server? The `dial-rule-translation-destination-host` alone, matched
58
+ * against the caller's list — never the `dial-rule-description`, which is a note the portal writes and
59
+ * an operator can edit. An empty host set answers `false` for everything, which is the point: this
60
+ * library knows no fax server of its own.
61
+ */
62
+ function isFaxLine(p, hosts) {
63
+ if (!hosts.size)
64
+ return false;
65
+ return hosts.has(str(p['dial-rule-translation-destination-host']).toLowerCase());
66
+ }
67
+ /**
68
+ * A device's NAME — the local part of its SIP URI (`sip:103t@acme.example` → `103t`), which is the short
69
+ * id the portal shows and the string the Teams test matches against.
70
+ *
71
+ * **`device` first, `aor` second.** A live `/users/<ext>/devices` record names the device in `device` and
72
+ * frequently carries no `aor` at all; reading `aor` alone therefore returned `''` on live data, which
73
+ * blanked every device name on the page AND broke the `<ext>t` Teams test — so a Teams connector read as
74
+ * `teams: false` and was counted as a handset in `deviceCount` and `devices.total`. Some records carry
75
+ * both, and then `device` wins, being the field the system actually names the device by. Neither, and the
76
+ * name is `''` rather than a guess.
77
+ */
78
+ function deviceName(device) {
79
+ const a = (str(device.device) || str(device.aor)).replace(/^sip:/i, '');
49
80
  const at = a.indexOf('@');
50
81
  return at === -1 ? a : a.slice(0, at);
51
82
  }
52
83
  function extensionItem(u, devices) {
53
84
  const ext = str(u.user);
54
- // The Teams test is `<ext>t`, so a blank ext would read every device whose aor local part is a
55
- // bare `t` as a connector. No extension number, no Teams claim.
56
- const handsets = ext ? devices.filter((d) => aorLocal(d) !== `${ext}t`) : devices;
85
+ // The Teams test is `<ext>t`, so a blank ext would read every device NAMED a bare `t` as a connector.
86
+ // No extension number, no Teams claim.
87
+ const handsets = ext ? devices.filter((d) => deviceName(d) !== `${ext}t`) : devices;
57
88
  const transcription = str(u['voicemail-transcription-enabled']).toLowerCase();
58
89
  const teams = handsets.length !== devices.length;
59
90
  const name = `${str(u['name-first-name'])} ${str(u['name-last-name'])}`.trim();
@@ -74,8 +105,9 @@ function extensionItem(u, devices) {
74
105
  deviceModels: handsets.map((d) => str(d['device-models-model']) || '(unknown)'),
75
106
  // Every device, including the Teams connector — this is a display list, not a seat count.
76
107
  devices: devices.map((d) => {
77
- const isTeams = ext ? aorLocal(d) === `${ext}t` : false;
78
- return { name: aorLocal(d), model: isTeams ? '' : str(d['device-models-model']) || '(unknown)', teams: isTeams };
108
+ const name = deviceName(d);
109
+ const isTeams = ext ? name === `${ext}t` : false;
110
+ return { name, model: isTeams ? '' : str(d['device-models-model']) || '(unknown)', teams: isTeams };
79
111
  }),
80
112
  anyDevice: handsets.length > 0 || teams,
81
113
  };
@@ -114,8 +146,15 @@ export function usersByExt(users) {
114
146
  * - No destination but an application is set → `to <application>` (`to-connection` → `to connection`,
115
147
  * `to-voicemail` → `to voicemail`).
116
148
  * - Neither is set → `''`.
149
+ *
150
+ * A FAX LINE — a number whose destination host is one of `faxServerHosts` — short-circuits all of that
151
+ * and reads `to fax server`, host omitted. Otherwise it would render as `to connection` (which names
152
+ * plumbing, not a destination) or, on a rule that also carries a destination user, as a bare IP address
153
+ * beside a customer's phone number. Nobody reading this line needs the fax server's address.
117
154
  */
118
- export function destinationOf(p, userByExt) {
155
+ export function destinationOf(p, userByExt, faxServerHosts) {
156
+ if (isFaxLine(p, faxHosts(faxServerHosts)))
157
+ return 'to fax server';
119
158
  const dest = str(p['dial-rule-translation-destination-user']);
120
159
  const app = str(p['dial-rule-application']).replace(/^to-/i, '');
121
160
  const host = str(p['dial-rule-translation-destination-host']);
@@ -151,7 +190,7 @@ export function destinationOf(p, userByExt) {
151
190
  * countable thing, and one derived row is more honest than two that shuffle. A blank id is a
152
191
  * provisioning fault to fix; the fallback only keeps the distinguishable ones apart until it is.
153
192
  */
154
- export function listDomainInventory(snapshot) {
193
+ export function listDomainInventory(snapshot, opts) {
155
194
  const users = Array.isArray(snapshot.users) ? snapshot.users : [];
156
195
  const devicesByUser = (snapshot.devicesByUser ?? {});
157
196
  const phonenumbers = Array.isArray(snapshot.phonenumbers) ? snapshot.phonenumbers : [];
@@ -172,12 +211,17 @@ export function listDomainInventory(snapshot) {
172
211
  (isSystemUser(u) ? systemUsers : extensions).push(item);
173
212
  }
174
213
  const userByExt = usersByExt(users);
214
+ // Normalised once, not per number: the host list is the caller's and does not change mid-fold.
215
+ const hosts = faxHosts(opts?.faxServerHosts);
175
216
  const dids = phonenumbers.map((p) => {
176
217
  const number = str(p.phonenumber);
177
218
  const kind = isTollFree(number) ? 'tollFree' : 'local';
178
- const destination = destinationOf(p, userByExt);
219
+ const fax = isFaxLine(p, hosts);
220
+ // The KEY does not carry `fax`. It is a fact about how the number is routed today, and routing a
221
+ // number to the fax server must not orphan every decision a consumer recorded against it.
222
+ const destination = fax ? 'to fax server' : destinationOf(p, userByExt);
179
223
  const description = str(p['dial-rule-description']);
180
- return { key: identityKey('did', number, JSON.stringify({ number, kind })), number, kind, destination, description };
224
+ return { key: identityKey('did', number, JSON.stringify({ number, kind })), number, kind, fax, destination, description };
181
225
  });
182
226
  const e911Addresses = addresses.map((a, i) => {
183
227
  const id = str(a['emergency-address-id']);
@@ -200,8 +244,8 @@ export function listDomainInventory(snapshot) {
200
244
  return { extensions, systemUsers, dids, e911Addresses, smsNumbers };
201
245
  }
202
246
  /** The counts, as a fold over {@link listDomainInventory} so the two can never disagree. */
203
- export function countDomainInventory(snapshot) {
204
- return countInventoryDetail(listDomainInventory(snapshot));
247
+ export function countDomainInventory(snapshot, opts) {
248
+ return countInventoryDetail(listDomainInventory(snapshot, opts));
205
249
  }
206
250
  /**
207
251
  * Count an item list. Exposed separately so a consumer that has FILTERED the lists — to one site, to
@@ -213,7 +257,7 @@ export function countInventoryDetail(d) {
213
257
  systemUsers: { total: d.systemUsers.length, byServiceCode: {} },
214
258
  transcriptionEnabled: 0,
215
259
  teamsConnected: 0,
216
- dids: { total: d.dids.length, tollFree: 0, local: 0 },
260
+ dids: { total: 0, tollFree: 0, local: 0, fax: 0, all: d.dids.length },
217
261
  e911Addresses: d.e911Addresses.length,
218
262
  smsNumbers: d.smsNumbers.length,
219
263
  devices: { total: 0, byModel: {} },
@@ -239,7 +283,16 @@ export function countInventoryDetail(d) {
239
283
  for (const m of x.deviceModels)
240
284
  bump(inv.devices.byModel, m);
241
285
  }
286
+ // A fax line is billed as a fax line, so it lands in `fax` and in NEITHER of the two DID buckets —
287
+ // counting it as both would bill one number twice on a rulebook that has a rule for each. `fax` is
288
+ // read off the item rather than recomputed: an item list a consumer FILTERED still carries it, and a
289
+ // list built by a pre-0.7.0 lib has no `fax` at all, which reads as false and counts as it always did.
242
290
  for (const n of d.dids) {
291
+ if (n.fax) {
292
+ inv.dids.fax++;
293
+ continue;
294
+ }
295
+ inv.dids.total++;
243
296
  if (n.kind === 'tollFree')
244
297
  inv.dids.tollFree++;
245
298
  else
@@ -276,12 +329,19 @@ export function itemsFor(detail, path) {
276
329
  return ex.filter((x) => x.transcription);
277
330
  if (path === 'teamsConnected')
278
331
  return ex.filter((x) => x.teams);
332
+ // The three DID paths exclude fax lines, exactly as the counts do — a `counts: "dids.total"` rule
333
+ // whose observed number left the fax lines out but whose item list showed them would offer an
334
+ // operator rows to accept that the number above them does not count.
279
335
  if (path === 'dids.total')
280
- return detail.dids;
336
+ return detail.dids.filter((n) => !n.fax);
281
337
  if (path === 'dids.tollFree')
282
- return detail.dids.filter((n) => n.kind === 'tollFree');
338
+ return detail.dids.filter((n) => !n.fax && n.kind === 'tollFree');
283
339
  if (path === 'dids.local')
284
- return detail.dids.filter((n) => n.kind === 'local');
340
+ return detail.dids.filter((n) => !n.fax && n.kind === 'local');
341
+ if (path === 'dids.fax')
342
+ return detail.dids.filter((n) => n.fax);
343
+ if (path === 'dids.all')
344
+ return detail.dids;
285
345
  if (path === 'e911Addresses')
286
346
  return detail.e911Addresses;
287
347
  if (path === 'smsNumbers')
@@ -1 +1 @@
1
- {"version":3,"file":"inventory.js","sourceRoot":"","sources":["../src/inventory.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"inventory.js","sourceRoot":"","sources":["../src/inventory.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dszp/netsapiens-lib",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Portable, Node-free NetSapiens toolkit: split read/write API clients, JWT (ns_t) validation, and a snapshot -> FlowGraph -> Mermaid call-flow resolver/renderer. Runs unchanged in a Cloudflare Worker, Node, or the browser.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -37,7 +37,7 @@ export {
37
37
  } from './html.js';
38
38
  export { resolveSvgSize, rasterizerScript } from './raster.js';
39
39
  export { NsClient, NsApiError, assertBareServer, fetchDomainSnapshot, listDomains, asArray, type NsClientConfig, type FetchSnapshotOptions } from './nsClient.js';
40
- export { countDomainInventory, countInventoryDetail, listDomainInventory, itemsFor, itemLabel, destinationOf, usersByExt, type DomainInventory, type DomainInventoryDetail, type ExtensionItem, type NumberItem, type AddressItem, type SmsItem, type InventoryItem } from './inventory.js';
40
+ export { countDomainInventory, countInventoryDetail, listDomainInventory, itemsFor, itemLabel, destinationOf, usersByExt, type DomainInventory, type DomainInventoryDetail, type InventoryOptions, type ExtensionItem, type NumberItem, type AddressItem, type SmsItem, type InventoryItem } from './inventory.js';
41
41
  export { attributeDomainInventory, type DomainAttribution, type ItemAttribution } from './attribution.js';
42
42
  export { NsWriteClient, type NsWriteClientConfig } from './nsWriteClient.js';
43
43
  export {
@@ -60,6 +60,7 @@ ok(inv.transcriptionEnabled === 2, 'yes and a provider name both count; no and a
60
60
  ok(inv.dids.total === 4, 'four phone numbers');
61
61
  ok(inv.dids.tollFree === 2, '800 and 833 are toll-free');
62
62
  ok(inv.dids.local === 2, 'and the rest are local');
63
+ ok(inv.dids.fax === 0 && inv.dids.all === 4, 'no fax-server hosts were supplied, so nothing is a fax line and all equals total');
63
64
  ok(inv.e911Addresses === 2, 'two address records');
64
65
  ok(inv.smsNumbers === 1, 'one SMS number');
65
66
  ok(inv.devices.total === 5, 'five devices across all extensions');
@@ -71,6 +72,7 @@ ok(inv.devices.byModel['(unknown)'] === 1, 'a device with no model is counted un
71
72
  const empty = countDomainInventory({ meta: { domain: 'empty.example' } } as Snapshot);
72
73
  ok(empty.extensions.total === 0, 'empty snapshot: no extensions');
73
74
  ok(empty.dids.total === 0 && empty.dids.tollFree === 0 && empty.dids.local === 0, 'empty snapshot: no numbers');
75
+ ok(empty.dids.fax === 0 && empty.dids.all === 0, 'empty snapshot: no fax lines either');
74
76
  ok(empty.devices.total === 0, 'empty snapshot: no devices');
75
77
  ok(empty.smsNumbers === 0 && empty.e911Addresses === 0, 'empty snapshot: no addresses and no SMS numbers');
76
78
 
@@ -141,6 +143,7 @@ ok(sysDev.devices.total === 0, 'a system user device is not counted');
141
143
  ok(c.extensions.withAnyDevice + c.extensions.withNoDevice === c.extensions.total, '[fold] the two presence leaves partition the total');
142
144
  ok(c.dids.total === d.dids.length, '[fold] dids.total equals the number list length');
143
145
  ok(c.dids.tollFree === d.dids.filter((n) => n.kind === 'tollFree').length, '[fold] dids.tollFree equals the toll-free items');
146
+ ok(c.dids.all === d.dids.length && c.dids.fax === d.dids.filter((n) => n.fax).length, '[fold] dids.all is the whole list and dids.fax the fax lines in it');
144
147
  ok(c.e911Addresses === d.e911Addresses.length && c.smsNumbers === d.smsNumbers.length, '[fold] address and SMS counts equal the lists');
145
148
  }
146
149
 
@@ -173,6 +176,129 @@ ok(sysDev.devices.total === 0, 'a system user device is not counted');
173
176
  ok(itemLabel(d.smsNumbers[0]!) === '13175550100', '[label] SMS is its number');
174
177
  }
175
178
 
179
+ // ── a device is named by `device`, and only falls back to `aor` ──────────────────────────────────────
180
+ // A LIVE /users/<ext>/devices record names the device in `device` and frequently carries no `aor` at all.
181
+ // Reading `aor` alone blanked every device name and — worse — broke the `<ext>t` Teams test, so a Teams
182
+ // connector read as a handset and inflated deviceCount, devices.total and byDeviceCount while
183
+ // teamsConnected read zero. That is the shape this block pins.
184
+ {
185
+ const live = {
186
+ meta: { domain: 'live.example' },
187
+ users: [
188
+ { user: '1001', 'user-scope': 'Basic User', 'service-code': '' },
189
+ { user: '1002', 'user-scope': 'Basic User', 'service-code': '' },
190
+ { user: '1003', 'user-scope': 'Basic User', 'service-code': '' },
191
+ ],
192
+ devicesByUser: {
193
+ // `device` only — the live shape.
194
+ '1001': [
195
+ { device: 'sip:1001a@live.example', 'device-models-model': 'Yealink T54W' },
196
+ { device: 'sip:1001t@live.example', 'device-models-model': 'Teams' },
197
+ ],
198
+ // BOTH, disagreeing: `device` wins, being the field the system names the device by.
199
+ '1002': [{ device: 'sip:1002a@live.example', aor: 'sip:wrong@live.example', 'device-models-model': 'Yealink T31P' }],
200
+ // NEITHER: the name is empty rather than guessed, and an empty name is not a `t`.
201
+ '1003': [{ 'device-models-model': 'Yealink T31P' }],
202
+ },
203
+ } as Snapshot;
204
+
205
+ const d = listDomainInventory(live);
206
+ const e1 = d.extensions.find((x) => x.ext === '1001')!;
207
+ ok(e1.devices[0]!.name === '1001a', '[device] a record with `device` and no `aor` is still named');
208
+ ok(e1.teams === true, '[device] and its <ext>t connector is detected — reading `aor` alone made this false on every live domain');
209
+ ok(e1.devices[1]!.teams === true && e1.devices[1]!.model === '', '[device] the connector row is marked and prints no model');
210
+ ok(e1.deviceCount === 1 && JSON.stringify(e1.deviceModels) === JSON.stringify(['Yealink T54W']),
211
+ '[device] so the connector is excluded from the handset count rather than inflating it');
212
+
213
+ const e2 = d.extensions.find((x) => x.ext === '1002')!;
214
+ ok(e2.devices[0]!.name === '1002a', '[device] where a record carries both, `device` wins');
215
+
216
+ const e3 = d.extensions.find((x) => x.ext === '1003')!;
217
+ ok(e3.devices[0]!.name === '', '[device] a record with neither field has an empty name, not "undefined"');
218
+ ok(e3.teams === false && e3.deviceCount === 1, '[device] and an unnamed device is a handset, never a connector');
219
+
220
+ const c = countDomainInventory(live);
221
+ ok(c.teamsConnected === 1, '[device] teamsConnected counts the one connector');
222
+ ok(c.devices.total === 3, '[device] and devices.total is the three handsets, the connector excluded');
223
+
224
+ // The `aor`-only fixture at the top of this file still behaves exactly as it did — the fallback is a
225
+ // fallback, not a replacement.
226
+ ok(listDomainInventory(snap).extensions.find((x) => x.ext === '103')!.teams === true,
227
+ '[device] a record carrying only `aor` is unchanged');
228
+ }
229
+
230
+ // ── fax lines ────────────────────────────────────────────────────────────────────────────────────────
231
+ // The portal's "Fax Server" treatment is an ordinary phone number whose dial rule hands it to a host:
232
+ // to-connection, plus dial-rule-translation-destination-host. There is no fax endpoint in the API and
233
+ // the ATA is not a device on any user, so the host is the only thing that says "fax line" — and the host
234
+ // belongs to the deployment, not to this library, which is why it arrives as an option.
235
+ {
236
+ const faxSnap = {
237
+ meta: { domain: 'fax.example' },
238
+ users: [{ user: '100', 'user-scope': 'Basic User', 'service-code': '', 'name-first-name': 'Ann', 'name-last-name': 'Lee' }],
239
+ phonenumbers: [
240
+ { phonenumber: '13175550100', 'dial-rule-translation-destination-user': '100' },
241
+ {
242
+ phonenumber: '13175550199',
243
+ 'dial-rule-application': 'to-connection',
244
+ 'dial-rule-translation-destination-host': '203.0.113.7',
245
+ 'dial-rule-description': 'Portal Created: Phonenumber -> FaxServer',
246
+ },
247
+ ],
248
+ } as Snapshot;
249
+ const HOSTS = { faxServerHosts: ['203.0.113.7'] };
250
+
251
+ const d = listDomainInventory(faxSnap, HOSTS);
252
+ const fx = d.dids[1]!;
253
+ ok(fx.fax === true, '[fax] a number whose destination host is the fax server is a fax line');
254
+ ok(d.dids[0]!.fax === false, '[fax] and the one routed to a user is not');
255
+ ok(fx.kind === 'local', '[fax] a fax line still has a kind — it is a local or toll-free number like any other');
256
+ ok(fx.destination === 'to fax server', '[fax] its destination reads "to fax server" — never the bare host');
257
+ ok(!fx.destination.includes('203.0.113.7'), '[fax] the fax server address does not reach a reader');
258
+ ok(fx.description === 'Portal Created: Phonenumber -> FaxServer', '[fax] the portal note is carried as written; it is not what the test reads');
259
+
260
+ const c = countDomainInventory(faxSnap, HOSTS);
261
+ ok(c.dids.fax === 1, '[fax] dids.fax counts it');
262
+ ok(c.dids.total === 1 && c.dids.local === 1 && c.dids.tollFree === 0, '[fax] and dids.total/local/tollFree leave it out — it is billed as a fax line, not as a DID');
263
+ ok(c.dids.all === 2, '[fax] dids.all is every number, fax lines included');
264
+ ok(c.dids.total + c.dids.fax === c.dids.all, '[fax] total + fax partitions all');
265
+
266
+ const keys = (p: string) => (itemsFor(d, p) ?? []).map((x) => x.key).join(',');
267
+ ok(keys('dids.fax') === 'did:13175550199', '[fax] itemsFor dids.fax is the fax lines');
268
+ ok(keys('dids.total') === 'did:13175550100', '[fax] itemsFor dids.total excludes them, exactly as the count does');
269
+ ok(keys('dids.local') === 'did:13175550100', '[fax] and so does dids.local');
270
+ ok(keys('dids.all') === 'did:13175550100,did:13175550199', '[fax] dids.all is everything');
271
+ ok(itemLabel(fx) === '13175550199', '[fax] the label is unchanged — a fax line is named by its number like any other');
272
+
273
+ // The SAME snapshot with no hosts supplied. A library that guessed a fax server would be wrong on
274
+ // every deployment but the one it was written against, so it guesses nothing.
275
+ const bare = listDomainInventory(faxSnap);
276
+ ok(bare.dids[1]!.fax === false, '[fax] no hosts supplied, nothing is a fax line');
277
+ ok(bare.dids[1]!.destination === 'to connection', '[fax] and the destination falls back to the application');
278
+ const bareCounts = countDomainInventory(faxSnap);
279
+ ok(bareCounts.dids.fax === 0 && bareCounts.dids.total === 2 && bareCounts.dids.local === 2,
280
+ '[fax] the fax line is counted as the local DID it otherwise looks like');
281
+ ok(bareCounts.dids.all === bareCounts.dids.total, '[fax] all equals total when nothing is a fax line');
282
+ ok(countDomainInventory(faxSnap, { faxServerHosts: [] }).dids.fax === 0, '[fax] an empty host list is the same as none');
283
+ ok(countDomainInventory(faxSnap, { faxServerHosts: ['', ' '] }).dids.fax === 0, '[fax] and so is a list of blanks — a blank host must not match a number with no host');
284
+
285
+ // Host matching is trimmed and case-insensitive on BOTH sides: a hostname is not case-sensitive, and
286
+ // a comma-separated setting arrives with spaces around its entries.
287
+ const named = {
288
+ meta: { domain: 'fax2.example' },
289
+ phonenumbers: [{ phonenumber: '13175550198', 'dial-rule-application': 'to-connection', 'dial-rule-translation-destination-host': ' Fax.Example.COM ' }],
290
+ } as Snapshot;
291
+ ok(countDomainInventory(named, { faxServerHosts: [' fax.example.com '] }).dids.fax === 1, '[fax] host matching trims and lower-cases both sides');
292
+ ok(countDomainInventory(named, { faxServerHosts: ['other.example.com'] }).dids.fax === 0, '[fax] a host that is not on the list is not a fax line');
293
+
294
+ // The description is a note the portal writes and an operator can edit, so it is never the test.
295
+ const noteOnly = {
296
+ meta: { domain: 'fax3.example' },
297
+ phonenumbers: [{ phonenumber: '13175550197', 'dial-rule-application': 'to-connection', 'dial-rule-description': 'Portal Created: Phonenumber -> FaxServer' }],
298
+ } as Snapshot;
299
+ ok(countDomainInventory(noteOnly, { faxServerHosts: ['203.0.113.7'] }).dids.fax === 0, '[fax] the "-> FaxServer" description alone does not make a fax line');
300
+ }
301
+
176
302
  // ── blank identity fields never collide onto one key ─────────────────────────────────────────────────
177
303
  {
178
304
  const blank = {
@@ -251,6 +377,13 @@ ok(sysDev.devices.total === 0, 'a system user device is not counted');
251
377
  );
252
378
  ok(destinationOf({ 'dial-rule-application': 'to-connection' }, byExt) === 'to connection', '[destinationOf] no destination, application only — to- stripped so it does not read "to to-connection"');
253
379
  ok(destinationOf({}, byExt) === '', '[destinationOf] neither field set is empty, not "to undefined"');
380
+ const faxRule: Rec = { 'dial-rule-application': 'to-connection', 'dial-rule-translation-destination-host': '203.0.113.7' };
381
+ ok(destinationOf(faxRule, byExt) === 'to connection', '[destinationOf] with no host list a fax rule is just a connection');
382
+ ok(destinationOf(faxRule, byExt, ['203.0.113.7']) === 'to fax server', '[destinationOf] with the host on the list it reads "to fax server"');
383
+ ok(
384
+ destinationOf({ ...faxRule, 'dial-rule-translation-destination-user': '100' }, byExt, ['203.0.113.7']) === 'to fax server',
385
+ '[destinationOf] the fax test wins over a destination user, so no reader is shown a bare IP',
386
+ );
254
387
  }
255
388
 
256
389
  console.log(`\n${pass} passed, ${fail} failed`);
package/src/inventory.ts CHANGED
@@ -9,9 +9,9 @@
9
9
  * A device record from NetSapiens carries the SIP registration password. `listDomainInventory` builds
10
10
  * per-item lists — an extension's name and site, a number's kind and routing destination, an address's
11
11
  * label — from an allowlist of named fields, and `countDomainInventory` is a fold over those same
12
- * lists. The allowlist now includes a device's NAME (its `aor` local part, e.g. `101b` the same
13
- * short id the portal shows) alongside its model; a device's MAC, SIP password and email never appear,
14
- * and nothing here should be added that carries one.
12
+ * lists. The allowlist now includes a device's NAME (the local part of its `device` SIP URI, falling back
13
+ * to `aor` — e.g. `101b`, the same short id the portal shows) alongside its model; a device's MAC, SIP
14
+ * password and email never appear, and nothing here should be added that carries one.
15
15
  *
16
16
  * ## Every countable dimension is a numeric leaf
17
17
  *
@@ -24,6 +24,20 @@
24
24
  * A user whose `service-code` is empty or does not begin with `system-`. NetSapiens models auto
25
25
  * attendants, queues and time-of-day routers as users, and counting them as seats would overstate
26
26
  * every domain that has any. They are counted separately, as information, and never compared.
27
+ *
28
+ * ## What counts as a fax line
29
+ *
30
+ * On the portal's "Fax Server" treatment a number is an ordinary phone number whose dial rule hands it
31
+ * to a fax server host — `dial-rule-application: to-connection` with
32
+ * `dial-rule-translation-destination-host` set to that host. There is no fax-account endpoint and the
33
+ * ATA is not a device on any user, so the host is the only thing in the API that says "this is a fax
34
+ * line", and nothing in the API can tell an analog fax from a digital one.
35
+ *
36
+ * The host is therefore the whole test, and it is the CALLER's: pass `{ faxServerHosts }` to
37
+ * {@link listDomainInventory} or {@link countDomainInventory}. Matching is on the trimmed host,
38
+ * case-insensitively, and on nothing else — not the `dial-rule-description`, which is a portal-written
39
+ * note an operator can edit. **With no hosts supplied nothing is a fax line**, because a library that
40
+ * hardcoded one deployment's fax server would be wrong everywhere else.
27
41
  */
28
42
  import type { Rec, Snapshot } from './model.js';
29
43
 
@@ -47,13 +61,23 @@ export interface DomainInventory {
47
61
  /** Extensions whose `voicemail-transcription-enabled` is anything but empty or `no`. */
48
62
  transcriptionEnabled: number;
49
63
  /**
50
- * Extensions with a Microsoft Teams connector device — one whose SIP `aor` local part is the
51
- * extension number followed by `t` (`1000t`), which is how the TeamMate connector registers.
64
+ * Extensions with a Microsoft Teams connector device — one whose device NAME is the extension number
65
+ * followed by `t` (`1000t`), which is how the TeamMate connector registers. See {@link deviceName} for
66
+ * which field that name is read from, and why reading the wrong one silently miscounted this.
52
67
  * That device is NOT counted under `devices`: it is a connector, not a handset.
53
68
  */
54
69
  teamsConnected: number;
55
- /** Phone numbers on the domain, split by NANP toll-free prefix. */
56
- dids: { total: number; tollFree: number; local: number };
70
+ /**
71
+ * Phone numbers on the domain, split by NANP toll-free prefix — **fax lines excluded**.
72
+ *
73
+ * A number handed to the fax server is billed as a fax line, not as a DID, so `total`, `tollFree`
74
+ * and `local` all leave it out and `fax` counts it instead. `all` is every phone number the domain
75
+ * holds, fax lines included: `total + fax === all`.
76
+ *
77
+ * With no `faxServerHosts` supplied nothing is a fax line, `fax` is 0 and `total === all` — the
78
+ * numbers this returned before 0.7.0, unchanged.
79
+ */
80
+ dids: { total: number; tollFree: number; local: number; fax: number; all: number };
57
81
  /** E911 address records on the domain. */
58
82
  e911Addresses: number;
59
83
  /** SMS-enabled numbers on the domain. */
@@ -91,8 +115,9 @@ export interface ExtensionItem {
91
115
  /** `device-models-model` per handset, `(unknown)` when blank. Never the MAC. */
92
116
  deviceModels: string[];
93
117
  /**
94
- * Every device on this extension, in record order, connector included: `name` is the `aor` local
95
- * part (`sip:101b@acme.example` → `101b`) — the device NAME as the portal shows it — `model` is
118
+ * Every device on this extension, in record order, connector included: `name` is the local part of its
119
+ * `device` SIP URI, or of `aor` when that is all the record has (`sip:101b@acme.example` → `101b`) —
120
+ * the device NAME as the portal shows it — `model` is
96
121
  * `device-models-model` (`(unknown)` when blank on a handset, `''` for the Teams connector, which
97
122
  * has no model), and `teams` marks the connector entry itself. `deviceCount`/`deviceModels`/`teams`
98
123
  * above stay handset-only; this list is the one place a connector's own row shows up. Never the MAC
@@ -106,6 +131,14 @@ export interface NumberItem {
106
131
  key: string /* did:<phonenumber>, or did:~<hash> when the number is blank */;
107
132
  number: string;
108
133
  kind: 'local' | 'tollFree';
134
+ /**
135
+ * This number is handed to a fax server — see the module doc. `false` whenever the caller supplied no
136
+ * `faxServerHosts`, since without a host list nothing here can tell a fax line from any other number.
137
+ *
138
+ * `kind` is still set on a fax line (a fax number is local or toll-free like any other), but the
139
+ * COUNTS exclude it from `dids.total`/`local`/`tollFree` and count it under `dids.fax` instead.
140
+ */
141
+ fax: boolean;
109
142
  /** Where the number routes, for a person: see {@link destinationOf}. `''` when the record says nothing. */
110
143
  destination: string;
111
144
  /** `dial-rule-description` trimmed — the note the portal writes ("Portal Created: User - 1001"); `''` when blank. */
@@ -125,6 +158,20 @@ export interface DomainInventoryDetail {
125
158
  smsNumbers: SmsItem[];
126
159
  }
127
160
 
161
+ /**
162
+ * What the caller has to tell the counter that the API cannot. Optional in full: every option absent is
163
+ * the pre-0.7.0 behaviour, and no option changes what a snapshot has to contain.
164
+ */
165
+ export interface InventoryOptions {
166
+ /**
167
+ * The hosts a fax line is handed to — an IP or a hostname, as it appears in
168
+ * `dial-rule-translation-destination-host`. Compared trimmed and case-insensitively; blanks are
169
+ * ignored. Absent or empty means NO number is a fax line. See the module doc for why this is the
170
+ * caller's to supply.
171
+ */
172
+ faxServerHosts?: readonly string[];
173
+ }
174
+
128
175
  /** NANP toll-free area codes, 800 through 888. A number outside this set is counted local. */
129
176
  const TOLL_FREE = new Set(['800', '833', '844', '855', '866', '877', '888']);
130
177
 
@@ -176,18 +223,46 @@ function isTollFree(raw: string): boolean {
176
223
  return nanp.length === 10 && TOLL_FREE.has(nanp.slice(0, 3));
177
224
  }
178
225
 
179
- /** The local part of a device's `aor` (`sip:103t@acme.example` `103t`), read only to test for Teams. */
180
- function aorLocal(device: Rec): string {
181
- const a = str(device.aor).replace(/^sip:/i, '');
226
+ /** The fax-server hosts, trimmed, lower-cased and with blanks dropped the shape {@link isFaxLine} tests against. */
227
+ function faxHosts(hosts: readonly string[] | undefined): Set<string> {
228
+ const out = new Set<string>();
229
+ for (const h of hosts ?? []) { const v = str(h).toLowerCase(); if (v) out.add(v); }
230
+ return out;
231
+ }
232
+
233
+ /**
234
+ * Is this number handed to a fax server? The `dial-rule-translation-destination-host` alone, matched
235
+ * against the caller's list — never the `dial-rule-description`, which is a note the portal writes and
236
+ * an operator can edit. An empty host set answers `false` for everything, which is the point: this
237
+ * library knows no fax server of its own.
238
+ */
239
+ function isFaxLine(p: Rec, hosts: Set<string>): boolean {
240
+ if (!hosts.size) return false;
241
+ return hosts.has(str(p['dial-rule-translation-destination-host']).toLowerCase());
242
+ }
243
+
244
+ /**
245
+ * A device's NAME — the local part of its SIP URI (`sip:103t@acme.example` → `103t`), which is the short
246
+ * id the portal shows and the string the Teams test matches against.
247
+ *
248
+ * **`device` first, `aor` second.** A live `/users/<ext>/devices` record names the device in `device` and
249
+ * frequently carries no `aor` at all; reading `aor` alone therefore returned `''` on live data, which
250
+ * blanked every device name on the page AND broke the `<ext>t` Teams test — so a Teams connector read as
251
+ * `teams: false` and was counted as a handset in `deviceCount` and `devices.total`. Some records carry
252
+ * both, and then `device` wins, being the field the system actually names the device by. Neither, and the
253
+ * name is `''` rather than a guess.
254
+ */
255
+ function deviceName(device: Rec): string {
256
+ const a = (str(device.device) || str(device.aor)).replace(/^sip:/i, '');
182
257
  const at = a.indexOf('@');
183
258
  return at === -1 ? a : a.slice(0, at);
184
259
  }
185
260
 
186
261
  function extensionItem(u: Rec, devices: Rec[]): ExtensionItem {
187
262
  const ext = str(u.user);
188
- // The Teams test is `<ext>t`, so a blank ext would read every device whose aor local part is a
189
- // bare `t` as a connector. No extension number, no Teams claim.
190
- const handsets = ext ? devices.filter((d) => aorLocal(d) !== `${ext}t`) : devices;
263
+ // The Teams test is `<ext>t`, so a blank ext would read every device NAMED a bare `t` as a connector.
264
+ // No extension number, no Teams claim.
265
+ const handsets = ext ? devices.filter((d) => deviceName(d) !== `${ext}t`) : devices;
191
266
  const transcription = str(u['voicemail-transcription-enabled']).toLowerCase();
192
267
  const teams = handsets.length !== devices.length;
193
268
  const name = `${str(u['name-first-name'])} ${str(u['name-last-name'])}`.trim();
@@ -208,8 +283,9 @@ function extensionItem(u: Rec, devices: Rec[]): ExtensionItem {
208
283
  deviceModels: handsets.map((d) => str(d['device-models-model']) || '(unknown)'),
209
284
  // Every device, including the Teams connector — this is a display list, not a seat count.
210
285
  devices: devices.map((d) => {
211
- const isTeams = ext ? aorLocal(d) === `${ext}t` : false;
212
- return { name: aorLocal(d), model: isTeams ? '' : str(d['device-models-model']) || '(unknown)', teams: isTeams };
286
+ const name = deviceName(d);
287
+ const isTeams = ext ? name === `${ext}t` : false;
288
+ return { name, model: isTeams ? '' : str(d['device-models-model']) || '(unknown)', teams: isTeams };
213
289
  }),
214
290
  anyDevice: handsets.length > 0 || teams,
215
291
  };
@@ -249,8 +325,14 @@ export function usersByExt(users: Rec[]): Map<string, Rec> {
249
325
  * - No destination but an application is set → `to <application>` (`to-connection` → `to connection`,
250
326
  * `to-voicemail` → `to voicemail`).
251
327
  * - Neither is set → `''`.
328
+ *
329
+ * A FAX LINE — a number whose destination host is one of `faxServerHosts` — short-circuits all of that
330
+ * and reads `to fax server`, host omitted. Otherwise it would render as `to connection` (which names
331
+ * plumbing, not a destination) or, on a rule that also carries a destination user, as a bare IP address
332
+ * beside a customer's phone number. Nobody reading this line needs the fax server's address.
252
333
  */
253
- export function destinationOf(p: Rec, userByExt: Map<string, Rec>): string {
334
+ export function destinationOf(p: Rec, userByExt: Map<string, Rec>, faxServerHosts?: readonly string[]): string {
335
+ if (isFaxLine(p, faxHosts(faxServerHosts))) return 'to fax server';
254
336
  const dest = str(p['dial-rule-translation-destination-user']);
255
337
  const app = str(p['dial-rule-application']).replace(/^to-/i, '');
256
338
  const host = str(p['dial-rule-translation-destination-host']);
@@ -288,7 +370,7 @@ export function destinationOf(p: Rec, userByExt: Map<string, Rec>): string {
288
370
  * countable thing, and one derived row is more honest than two that shuffle. A blank id is a
289
371
  * provisioning fault to fix; the fallback only keeps the distinguishable ones apart until it is.
290
372
  */
291
- export function listDomainInventory(snapshot: Snapshot): DomainInventoryDetail {
373
+ export function listDomainInventory(snapshot: Snapshot, opts?: InventoryOptions): DomainInventoryDetail {
292
374
  const users: Rec[] = Array.isArray(snapshot.users) ? snapshot.users : [];
293
375
  const devicesByUser: Record<string, Rec[]> = (snapshot.devicesByUser ?? {}) as Record<string, Rec[]>;
294
376
  const phonenumbers: Rec[] = Array.isArray(snapshot.phonenumbers) ? snapshot.phonenumbers : [];
@@ -310,12 +392,17 @@ export function listDomainInventory(snapshot: Snapshot): DomainInventoryDetail {
310
392
  (isSystemUser(u) ? systemUsers : extensions).push(item);
311
393
  }
312
394
  const userByExt = usersByExt(users);
395
+ // Normalised once, not per number: the host list is the caller's and does not change mid-fold.
396
+ const hosts = faxHosts(opts?.faxServerHosts);
313
397
  const dids: NumberItem[] = phonenumbers.map((p) => {
314
398
  const number = str(p.phonenumber);
315
399
  const kind: 'local' | 'tollFree' = isTollFree(number) ? 'tollFree' : 'local';
316
- const destination = destinationOf(p, userByExt);
400
+ const fax = isFaxLine(p, hosts);
401
+ // The KEY does not carry `fax`. It is a fact about how the number is routed today, and routing a
402
+ // number to the fax server must not orphan every decision a consumer recorded against it.
403
+ const destination = fax ? 'to fax server' : destinationOf(p, userByExt);
317
404
  const description = str(p['dial-rule-description']);
318
- return { key: identityKey('did', number, JSON.stringify({ number, kind })), number, kind, destination, description };
405
+ return { key: identityKey('did', number, JSON.stringify({ number, kind })), number, kind, fax, destination, description };
319
406
  });
320
407
  const e911Addresses: AddressItem[] = addresses.map((a, i) => {
321
408
  const id = str(a['emergency-address-id']);
@@ -339,8 +426,8 @@ export function listDomainInventory(snapshot: Snapshot): DomainInventoryDetail {
339
426
  }
340
427
 
341
428
  /** The counts, as a fold over {@link listDomainInventory} so the two can never disagree. */
342
- export function countDomainInventory(snapshot: Snapshot): DomainInventory {
343
- return countInventoryDetail(listDomainInventory(snapshot));
429
+ export function countDomainInventory(snapshot: Snapshot, opts?: InventoryOptions): DomainInventory {
430
+ return countInventoryDetail(listDomainInventory(snapshot, opts));
344
431
  }
345
432
 
346
433
  /**
@@ -353,7 +440,7 @@ export function countInventoryDetail(d: DomainInventoryDetail): DomainInventory
353
440
  systemUsers: { total: d.systemUsers.length, byServiceCode: {} },
354
441
  transcriptionEnabled: 0,
355
442
  teamsConnected: 0,
356
- dids: { total: d.dids.length, tollFree: 0, local: 0 },
443
+ dids: { total: 0, tollFree: 0, local: 0, fax: 0, all: d.dids.length },
357
444
  e911Addresses: d.e911Addresses.length,
358
445
  smsNumbers: d.smsNumbers.length,
359
446
  devices: { total: 0, byModel: {} },
@@ -371,7 +458,15 @@ export function countInventoryDetail(d: DomainInventoryDetail): DomainInventory
371
458
  inv.devices.total += x.deviceCount;
372
459
  for (const m of x.deviceModels) bump(inv.devices.byModel, m);
373
460
  }
374
- for (const n of d.dids) { if (n.kind === 'tollFree') inv.dids.tollFree++; else inv.dids.local++; }
461
+ // A fax line is billed as a fax line, so it lands in `fax` and in NEITHER of the two DID buckets —
462
+ // counting it as both would bill one number twice on a rulebook that has a rule for each. `fax` is
463
+ // read off the item rather than recomputed: an item list a consumer FILTERED still carries it, and a
464
+ // list built by a pre-0.7.0 lib has no `fax` at all, which reads as false and counts as it always did.
465
+ for (const n of d.dids) {
466
+ if (n.fax) { inv.dids.fax++; continue; }
467
+ inv.dids.total++;
468
+ if (n.kind === 'tollFree') inv.dids.tollFree++; else inv.dids.local++;
469
+ }
375
470
  return inv;
376
471
  }
377
472
 
@@ -393,9 +488,14 @@ export function itemsFor(detail: DomainInventoryDetail, path: string): Inventory
393
488
  if (path === 'extensions.withNoDevice') return ex.filter((x) => !x.anyDevice);
394
489
  if (path === 'transcriptionEnabled') return ex.filter((x) => x.transcription);
395
490
  if (path === 'teamsConnected') return ex.filter((x) => x.teams);
396
- if (path === 'dids.total') return detail.dids;
397
- if (path === 'dids.tollFree') return detail.dids.filter((n) => n.kind === 'tollFree');
398
- if (path === 'dids.local') return detail.dids.filter((n) => n.kind === 'local');
491
+ // The three DID paths exclude fax lines, exactly as the counts do — a `counts: "dids.total"` rule
492
+ // whose observed number left the fax lines out but whose item list showed them would offer an
493
+ // operator rows to accept that the number above them does not count.
494
+ if (path === 'dids.total') return detail.dids.filter((n) => !n.fax);
495
+ if (path === 'dids.tollFree') return detail.dids.filter((n) => !n.fax && n.kind === 'tollFree');
496
+ if (path === 'dids.local') return detail.dids.filter((n) => !n.fax && n.kind === 'local');
497
+ if (path === 'dids.fax') return detail.dids.filter((n) => n.fax);
498
+ if (path === 'dids.all') return detail.dids;
399
499
  if (path === 'e911Addresses') return detail.e911Addresses;
400
500
  if (path === 'smsNumbers') return detail.smsNumbers;
401
501
  return undefined;