@webex/internal-plugin-device 3.12.0-next.53 → 3.12.0-next.55

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.
@@ -0,0 +1,676 @@
1
+ ---
2
+ type: Module Spec
3
+ title: 'Device plugin specification'
4
+ description: Responsibilities, boundaries, design, invariants, and verification for the @webex/internal-plugin-device source module.
5
+ tags: [module, specification]
6
+ ---
7
+ <!-- sdd-generated-metadata
8
+ doc_kind: module-spec
9
+ generated_from: module-spec@0.3.0
10
+ generated_by: claude-code
11
+ approved_by: riag@cisco.com
12
+ updated_at: 2026-10-07T15:08:52Z
13
+ validation_status: pass
14
+ -->
15
+
16
+ # Device plugin
17
+
18
+ This source-local document at `src/docs/README.md` owns the stable
19
+ specification for the **device plugin** (`src/`). Ground every claim in repository evidence
20
+ and link to the [package architecture](../../docs/architecture.md) instead of repeating broader facts.
21
+
22
+ Related context: [documentation index](../../docs/index.md) ·
23
+ [specification registry](../../docs/specs/README.md) ·
24
+ [agent instructions](../../AGENTS.md)
25
+
26
+ ## Metadata
27
+
28
+ | Field | Value |
29
+ | ----------------- | -------------------------------------------------------------------------------------- |
30
+ | Owner | Webex JS SDK maintainers (`webex/webex-js-sdk`), confirmed by riag@cisco.com |
31
+ | Source path | `src/` |
32
+ | Resource kind | package (internal Webex SDK plugin, registered as `webex.internal.device`) |
33
+ | Status | Draft |
34
+ | Last verified | 2026-10-07 against the working tree |
35
+ | Module id | `src` |
36
+ | Parent spec | — |
37
+ | Doc kind | Module spec |
38
+ | Coverage score | 80.0% assessed 2026-10-07: 12 of 15 mandatory fields present, critical 6 of 7; no characterization baseline |
39
+ | Validation status | pass — validator `01a114a2-39d4-73f2-b1b0-e0a3d977933d`, assessed 2026-10-07; 0 findings |
40
+
41
+ ## Applicability
42
+
43
+ | Condition ID | Status | Evidence or reason | Owned section |
44
+ | ------------------------------------ | ---------- | --------------------------------------------------------------------------------------------------------------- | ----------------------------- |
45
+ | `module.has_tiers` | N/A | No tier or SLO policy is declared anywhere in `package.json` or src/ | Tier |
46
+ | `module.has_ui` | N/A | No rendering code exists under src/; the module is headless | UI use-case flow |
47
+ | `module.crosses_service_boundaries` | Applicable | `src/device.js` issues HTTP requests to the WDM service and to the intranet inactivity-check URL | Cross-boundary use-case flow |
48
+ | `module.holds_client_state` | Applicable | `src/device.js` keeps registration, session, and feature state in an Ampersand model | Client state model |
49
+ | `module.enforces_domain_rules` | Applicable | `src/device.js` and `src/interceptors/device-url.js` enforce guards listed under Business rules and invariants | Business rules and invariants |
50
+ | `module.is_concurrent_async` | Applicable | `src/device.js` uses oneFlight, waitForValue, timers, and event listeners | Concurrency and reactive flow |
51
+ | `module.owns_persistence` | N/A | Storage is owned by the webex-core package; this module only opts in with @persist in `src/device.js` and defines no schema or migration | Data, schema, and migration |
52
+ | `module.stateful_transitions` | Applicable | Registration lifecycle in `src/device.js`; detection lifecycle in `src/ipNetworkDetector.ts` | State machine |
53
+ | `module.exposes_wire_protocol` | N/A | Only JSON over HTTP via the shared request pipeline; no module-owned frame or binary format | Protocol and wire format |
54
+ | `module.ui_multi_screen` | N/A | No UI | UI flow |
55
+ | `module.large_data_model` | N/A | The WDM payload is stored as a flat property bag plus three feature collections; no relational model | Data model |
56
+ | `module.returns_caller_errors` | Applicable | Public methods reject with errors callers must handle, see Caller-visible failure modes | Caller-visible failure modes |
57
+ | `module.module_specific_conventions` | Applicable | Ampersand and decorator conventions documented in `src/device.js` comments | Module-specific rules |
58
+ | `module.published_package` | Applicable | `package.json` declares main, deploy:npm, and the public barrel `src/index.js` | Export stability |
59
+ | `module.embedded_in_host` | N/A | Consumed as a plugin of the webex-core package, not mounted into a host UI | Host integration and theming |
60
+ | `module.has_design_tradeoff` | N/A | No ADR or code comment records a consumer-facing trade-off; reviewed by the generator only | Key design trade-off |
61
+ | `module.has_submodules` | N/A | Single module for the whole package; computed from the manifest module tree | Sub-modules |
62
+
63
+ ## Evidence register
64
+
65
+ | Evidence | What it establishes |
66
+ | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- |
67
+ | `src/index.js` | Plugin registration, interceptor wiring, logout hook, and the exported surface |
68
+ | `src/device.js` | Device model, registration lifecycle, cleanup, inactivity logic, event wiring |
69
+ | `src/config.js` | Default configuration under `webex.config.device` |
70
+ | `src/constants.js` | Feature collection names, event name, header name, cleanup thresholds |
71
+ | `src/features/features-model.js` | Feature container and change propagation |
72
+ | `src/features/feature-model.js` | Feature value typing and serialization |
73
+ | `src/interceptors/device-url.js` | `cisco-device-url` header injection rules |
74
+ | `src/ipNetworkDetector.ts` | IPv4/IPv6/mDNS detection through a WebRTC peer connection |
75
+ | `package.json` | Entry points, scripts, dependencies, engines |
76
+ | `test/unit/spec/device.js` | Behavioral unit coverage of registration, refresh, cleanup, feature handling |
77
+ | `test/integration/spec/device.js` | Behavioral coverage against a real Webex test user: reachability, websocket URL, logout timer, unregister |
78
+ | Dependency specifications and source | `@webex/common`, `@webex/common-timers`, and `@webex/http-core` are taken from their module specifications; `@webex/webex-core` and `@webex/internal-plugin-metrics` have no specification and are taken from their source |
79
+ | Prior documents and history | None: the package carried only a usage README, and commit history was deliberately not used as evidence |
80
+
81
+ ## Purpose and boundary
82
+
83
+ - Responsibility: Own this client's **device registration** with the Webex WDM (web device manager) service —
84
+ register, refresh, unregister, stale-registration cleanup — and the local state derived from it: device URL,
85
+ WebSocket URL, feature toggles, org branding and policy flags, and the inactivity/network-reachability logout timer.
86
+ Also owns two small helpers that hang off the device: the `cisco-device-url` request header interceptor and the
87
+ IPv4/IPv6 network detector.
88
+ - In scope: the `device` plugin registered on `webex.internal`; its configuration namespace `device`; the
89
+ `registration:success` event; feature collections (`developer`, `entitlement`, `user`) and their change events;
90
+ debug feature-toggle overrides from `sessionStorage`; ephemeral-device refresh timing; WDM device cleanup when
91
+ the registration limit is hit.
92
+ - Out of scope: service-catalog resolution and request routing (`@webex/webex-core`), the HTTP stack
93
+ (`@webex/http-core`), metrics transport (`@webex/internal-plugin-metrics`), WebSocket connection handling
94
+ (Mercury plugin), reachability probing of media clusters (meetings plugin), and credential handling.
95
+ - Consumers: other SDK plugins reading `webex.internal.device` (`url`, `userId`, `webSocketUrl`, `features`,
96
+ `ipNetworkDetector`), `@webex/webex-core` and its test helpers, the meetings and calling packages, and
97
+ first-party Webex clients.
98
+
99
+ ## Structure and key files
100
+
101
+ | Path | Responsibility |
102
+ | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
103
+ | `src/index.js` | Registers the plugin via `registerInternalPlugin`, attaches `DeviceUrlInterceptor`, wires `onBeforeLogout` to `unregister()`, and defines the export surface |
104
+ | `src/device.js` | The `Device` Ampersand model: properties, derived `registered`, session state, registration lifecycle, inactivity timer, listeners |
105
+ | `src/config.js` | Default `device` configuration (wait duration, default registration body, ephemeral, inactivity, debug key) |
106
+ | `src/constants.js` | Feature collection names, feature value types, `registration:success`, `cisco-device-url`, cleanup limits |
107
+ | `src/types.ts` | `CatalogDetails` enum and `DeviceRegistrationOptions` type |
108
+ | `src/metrics.js` | Client-metric names for WDM registration success and failure |
109
+ | `src/features/feature-model.js` | One feature: parses the WDM string `val` into a typed `value`, serializes `lastModified` as ISO text |
110
+ | `src/features/feature-collection.js`| Collection of features keyed by `key` |
111
+ | `src/features/features-model.js` | Container of the three collections; re-emits collection changes as `change:<collection>` |
112
+ | `src/features/index.js` | Barrel for the three feature classes |
113
+ | `src/interceptors/device-url.js` | Request interceptor adding `cisco-device-url` |
114
+ | `src/ipNetworkDetector.ts` | Peer-connection based IPv4/IPv6/mDNS detector exposed as `device.ipNetworkDetector` |
115
+ | test/unit/spec/ | Unit tests: `device.js`, `ipNetworkDetector.js`, `features/`, `interceptors/`, and the `wdm-dto.json` fixture |
116
+ | test/integration/spec/ | Integration tests against Webex test users: `device.js`, `webex.js` |
117
+
118
+ ## Public surface
119
+
120
+ Exact declarations stay in the native sources; this table routes to them. Contract IDs resolve in `.sdd/manifest.json`.
121
+
122
+ | Surface | Consumer | Compatibility commitment | Source |
123
+ | ------------------------------------------------------------------------- | --------------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------ |
124
+ | `device-plugin-sdk`: default export `Device`, `config`, `constants`, `CatalogDetails`, `DeviceRegistrationOptions`, `DeviceUrlInterceptor`, `FeatureCollection`, `FeatureModel`, `FeaturesModel`, plus the `webex.internal.device` instance API and `webex.config.device` | SDK plugins and first-party clients | Internal plugin; semantic versioning is not strictly followed (see Export stability) | `src/index.js` |
125
+ | `device-registration-events`: `registration:success`, `change`, `change:features`, `change:<collection>` | Plugins listening on the device | Names are stable in practice; consumers inside the SDK rely on `registration:success`; names are declared in `src/constants.js` and emitted from `src/device.js` and `src/features/features-model.js` | `src/device.js` |
126
+ | `cisco-device-url-header`: request header carrying the registered device URL | Webex backend services | Internal; excluded for `idbroker`, `oauth`, `saml` | `src/interceptors/device-url.js` |
127
+
128
+ The repository-wide index of these contracts is `Public and consumer surfaces` in the
129
+ [package architecture](../../docs/architecture.md).
130
+
131
+ ## Dependencies
132
+
133
+ | Dependency | Why it is required | Failure behavior |
134
+ | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
135
+ | `@webex/webex-core` (source; no spec) | `WebexPlugin` base model, `persist` and `waitForValue` decorators, `registerInternalPlugin`, the request pipeline, and the `services` plugin (`waitForCatalog`, `get`, `waitForService`, `getServiceFromUrl`, `convertUrlToPriorityHostUrl`, `markFailedUrl`) | Catalog wait errors and request errors propagate to the caller unchanged |
136
+ | WDM service (external) | System of record for device registrations; reached through the `wdm` catalog service, resource `devices` | Missing catalog entry rejects `canRegister()`; HTTP errors propagate; 404 on refresh re-registers |
137
+ | `@webex/internal-plugin-metrics` (source; no spec) | `webex.internal.newMetrics` (`submitInternalEvent`, `callDiagnosticMetrics.setDeviceInfo`) and `webex.internal.metrics.submitClientMetrics`; imported for side effects in `src/index.js` | Not guarded: a missing plugin would throw at register time |
138
+ | `@webex/common` (module spec) | `oneFlight`, `deprecated`, `inBrowser`, `deviceType` (the `WEB` default registration type) | Decorator semantics as specified by that package |
139
+ | `@webex/common-timers` (module spec) | `safeSetTimeout`, so refresh and logout timers do not keep Node alive | None: pass-through to `setTimeout` |
140
+ | `@webex/http-core` (module spec) | `Interceptor` base class extended by `DeviceUrlInterceptor` | None |
141
+ | `ampersand-state`, `ampersand-collection` | Model and collection runtime for `Device` and the feature classes | None |
142
+ | `lodash`, `uuid` | `orderBy`, `defaults`, `isObject`, `set`; `uuid.v4` for stale-response request ids | None |
143
+ | `RTCPeerConnection` (browser) | Candidate gathering in `IpNetworkDetector.detect()` | `detect()` rejects when offer creation fails; absent in plain Node |
144
+
145
+ ## Requirements
146
+
147
+ | ID | WHAT | WHY | Source evidence | Test or example evidence | Assumptions or gaps | Confidence |
148
+ | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | -------------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | ---------- |
149
+ | `MOD-001` | Importing the package registers an internal plugin `device` with its config, the `DeviceUrlInterceptor`, and an `onBeforeLogout` hook that calls `unregister()` | Every SDK instance must register its device and release it on logout without per-plugin wiring | `src/index.js` | `test/integration/spec/webex.js` | Integration only; needs test users | Present |
150
+ | `MOD-002` | `canRegister()` waits for the `postauth` catalog for `canRegisterWaitDuration` seconds (default 10) and rejects with `device: cannot register, 'wdm' service is not available from the postauth catalog` when `wdm` is absent | Registering without a resolvable WDM host would send the request to the wrong place | `src/device.js` | `test/integration/spec/device.js` | Unit tests stub `canRegister`; no unit coverage of the wait | Present |
151
+ | `MOD-003` | `register()` POSTs to service `wdm`, resource `devices`, with body = `config.defaults.body` overlaid by `config.body`, headers from `config.defaults.headers` and `config.headers`, and `ttl = ephemeralDeviceTTL` when ephemeral | The WDM contract requires a typed device description; consumers override it through config | `src/device.js`, `src/config.js` | `test/unit/spec/device.js`, `test/integration/spec/device.js` | Default body values come from `src/config.js` and are not asserted by a test | Present |
152
+ | `MOD-004` | Both register and refresh send query `includeUpstreamServices=<includeDetails>` (default `all`, values from `CatalogDetails`), appending `,energyforecast` when `config.energyForecast` and `setEnergyForecastConfig(true)` are both set | Callers control payload size; energy forecast is fetched only on request to avoid over-fetching | `src/device.js`, `src/types.ts` | `test/unit/spec/device.js` | None | Present |
153
+ | `MOD-005` | `register()` on a registered device delegates to `refresh()`, and `refresh()` on an unregistered device delegates to `register()`, passing the options through | Callers never need to know registration state before calling either method | `src/device.js` | `test/unit/spec/device.js` | None | Present |
154
+ | `MOD-006` | `refresh()` PUTs to the device `url` with body = serialized device overlaid by `config.body`, and sends `If-None-Match: <etag>` when an etag is stored | Keeps the registration alive and lets WDM skip unchanged developer feature toggles | `src/device.js` | `test/unit/spec/device.js`, `test/integration/spec/device.js` | None | Present |
155
+ | `MOD-007` | A 404 on refresh clears the device and registers a fresh one with the same options | The registration expired server-side; the client must self-heal instead of failing every request | `src/device.js` | `test/integration/spec/device.js` | No unit test of the refresh-404 branch | Present |
156
+ | `MOD-008` | When registration fails with body message `User has excessive device registrations`, `register()` runs `deleteDevices()` once and retries the registration once; any other error is rethrown untouched | Users who hit the per-user device limit would otherwise be locked out until manual cleanup | `src/device.js` | `test/unit/spec/device.js` | The second failure is not retried and propagates | Present |
157
+ | `MOD-009` | `deleteDevices()` lists WDM devices (`GET wdm/devices`) of the current `deviceType`, returns without action at or below 5 devices, otherwise deletes the oldest `ceil(n/3)` by `modificationTime` best-effort, then waits for the count to fall to `n - min(5, deleted)` | Frees registration slots while never emptying a user's device list | `src/device.js`, `src/constants.js` | `test/unit/spec/device.js` | Rejects only when the initial listing fails; individual delete failures are logged | Present |
158
+ | `MOD-010` | After cleanup the module polls the device list up to 5 times at 3000 ms intervals and proceeds regardless once the target is reached, attempts run out, or a poll fails | WDM deletion is eventually consistent; the retry must not fire before the slots are actually free | `src/device.js`, `src/constants.js` | `test/unit/spec/device.js` | The wait uses plain `setTimeout`, not `safeSetTimeout` | Present |
159
+ | `MOD-011` | `unregister()` resolves immediately when unregistered; otherwise DELETEs the device `url` and clears local state on success; a 404 also clears local state but the call still rejects | Logout must not leave a stale local registration, and a missing remote device is treated as already gone | `src/device.js` | `test/unit/spec/device.js`, `test/integration/spec/device.js` | The 404 path rejects even though the outcome is the desired one | Present |
160
+ | `MOD-012` | `processRegistrationSuccess()` stores the WDM body on the device without `services` and `serviceHostMap`, stores the `etag` response header, and emits `registration:success` | Services are owned by the catalog, and listeners (WebSocket, waiters) key off the event | `src/device.js` | `test/unit/spec/device.js`, `test/integration/spec/device.js` | None | Present |
161
+ | `MOD-013` | When the response etag equals the stored etag, developer features are kept and only `user` and `entitlement` feature collections are reset from the response; otherwise all collections come from the response | WDM omits unchanged developer toggles when `If-None-Match` matches; discarding them would erase state | `src/device.js` | `test/unit/spec/device.js` | None | Present |
162
+ | `MOD-014` | With `config.debugFeatureTogglesKey` set, a changed-etag response has the toggles parsed from `sessionStorage[<key>]` appended to `features.developer` as mutable `true`/`false` strings, and the first `change:config` event unsets the etag when such toggles exist | Developers can force toggles locally; unsetting the etag guarantees developer toggles are fetched to merge into | `src/device.js` | `test/unit/spec/device.js` | Parse errors are logged and swallowed | Present |
163
+ | `MOD-015` | In ephemeral mode, a successful registration schedules `refresh()` after `ephemeralDeviceTTL / 2 + 60` seconds through `safeSetTimeout` | Ephemeral registrations expire at the TTL; refreshing at half-life plus a minute keeps them alive | `src/device.js`, `src/config.js` | `test/integration/spec/device.js` | The timer handle is never cleared by `clear()` or `unregister()` | Present |
164
+ | `MOD-016` | Feature values are typed on parse: numeric strings become `number`, `true`/`false` (any case) become `boolean`, everything else stays `string`; `lastModified` serializes as an ISO string | WDM sends every toggle as text; consumers need typed values | `src/features/feature-model.js` | `test/unit/spec/features/feature-model.js` | None | Present |
165
+ | `MOD-017` | Any add, remove, or `value` change in a feature collection raises `change:<collection>` on `features`, which the device re-emits as `change` and `change:features` | Consumers subscribe to one device event instead of three collections | `src/features/features-model.js`, `src/device.js` | `test/unit/spec/features/features-model.js`, `test/unit/spec/device.js` | None | Present |
166
+ | `MOD-018` | `getWebSocketUrl(wait)` resolves the stored `webSocketUrl` mapped to the priority host; with `wait` it first awaits registration (default 10 s); failures reject with `device: failed to get the current websocket url` or a not-registered message | Mercury must connect to the highest-priority host, and callers may ask before registration completes | `src/device.js` | `test/integration/spec/device.js` | A non-service `webSocketUrl` makes the non-waiting path throw synchronously (see Pitfalls) | Present |
167
+ | `MOD-019` | `waitForRegistration(timeout)` resolves once registered or on the next `registration:success`, and rejects after `timeout` seconds (default 10) with `device: timeout occured while waiting for registration` | Lets dependents sequence on registration without polling | `src/device.js` | `test/integration/spec/device.js` | None | Present |
168
+ | `MOD-020` | Changes to `intranetInactivityCheckUrl`, `intranetInactivityDuration`, or `inNetworkInactivityDuration` call `checkNetworkReachability()`, which probes the check URL once per device lifetime to set `isInNetwork` and then resets the logout timer | Org policy can require logging out after inactivity, with a different duration when on the intranet | `src/device.js` | `test/unit/spec/device.js`, `test/integration/spec/device.js` | None | Present |
169
+ | `MOD-021` | The `meeting started` and `meeting ended` events on `webex` set `isInMeeting` and reset the logout timer; `meetingStarted()` and `meetingEnded()` emit them; `user-activity` stamps `lastUserActivityDate`, which re-arms the timer | A user in a meeting must not be logged out for inactivity; activity restarts the countdown | `src/device.js` | `test/integration/spec/device.js`, `test/unit/spec/device.js` | None | Present |
170
+ | `MOD-022` | `DeviceUrlInterceptor` sets `cisco-device-url` to the device URL on outbound requests that resolve to a catalog service | Backend services identify the calling device from this header | `src/interceptors/device-url.js` | `test/unit/spec/interceptors/device-url.js` | None | Present |
171
+ | `MOD-023` | `ipNetworkDetector.detect(force)` gathers ICE candidates from a throwaway `RTCPeerConnection` and records first-candidate times for IPv4, IPv6, and mDNS; `supportsIpV4` and `supportsIpV6` are `true` on a candidate, `undefined` while unfinished or when only mDNS arrived, and `false` otherwise | Meetings need to know the network's IP families to pick media paths; mDNS-only results mean no permission, not no support | `src/ipNetworkDetector.ts` | `test/unit/spec/ipNetworkDetector.js` | Resolves early when both IPv4 and IPv6 are seen; browser-only | Present |
172
+ | `MOD-024` | Registration reports `internal.register.device.request` and `internal.register.device.response` internal events, sets call-diagnostic device info, and submits `JS_SDK_WDM_REGISTRATION_SUCCESSFUL` or `JS_SDK_WDM_REGISTRATION_FAILED` client metrics | Latency and failure of device registration are measured for diagnostics | `src/device.js`, `src/metrics.js` | `test/unit/spec/device.js` | Refresh emits no metrics | Present |
173
+ | `MOD-025` | `markUrlFailedAndGetNew(url)` is deprecated and resolves with the result of `services.markFailedUrl(url)` | Older callers keep working while moving to the services plugin | `src/device.js` | `test/integration/spec/device.js` | None | Present |
174
+ | `MOD-026` | `webex.config.device` declares these options: `canRegisterWaitDuration` (10 s), `defaults.body` (name from the process title, else `browser`, else `javascript`; `deviceType` `WEB`; model `web-js-sdk`; localizedModel `webex-js-sdk`; systemName `WEBEX_JS_SDK`; systemVersion `1.0.0`), `enableInactivityEnforcement` (false), `ephemeral` (false), `ephemeralDeviceTTL` (1800 s), `energyForecast` (false), `debugFeatureTogglesKey` (unset), `installationId` (unset) | Defaults are the behavior every first-party client gets without configuration; overrides are the only tuning surface | `src/config.js` | none found | No test pins the defaults; `config.body`, `config.headers`, and `config.defaults.headers` are read by `src/device.js` but not declared in the defaults; `installationId` is declared but not read inside this package | Weak |
175
+
176
+ ## Design overview
177
+
178
+ `Device` is a single Ampersand model (`WebexPlugin.extend`) that is at once the **state container** for the
179
+ registration and the **service object** that operates it. Its WDM data arrives as a flat DTO; declared props
180
+ cover the fields the SDK reads, and `extraProperties: 'allow'` lets the model absorb additional or renamed
181
+ fields so a WDM change does not break initialization. Two children hang off the model: `features`
182
+ (a `FeaturesModel` with three collections) and `ipNetworkDetector`. `registered` is derived purely from `url`.
183
+
184
+ Registration methods compose rather than duplicate: `register()` and `refresh()` each delegate to the other when
185
+ the state says the other applies, and the actual POST lives in `_registerInternal()`. Concurrency control is
186
+ declarative through decorators: `@waitForValue('@')` holds a call until persisted state has been loaded, and
187
+ `@oneFlight` makes concurrent `refresh()`, `_registerInternal()` and `unregister()` calls share one request.
188
+ Stale-response protection uses a per-call UUID stored on the model so that a logout during an in-flight request
189
+ cannot repopulate a cleared device.
190
+
191
+ The inactivity logic is event-driven rather than polled: property-change listeners on the three inactivity
192
+ settings trigger one reachability probe, and `resetLogoutTimer()` is the single place that decides whether a timer
193
+ exists. The two helpers are deliberately small and independent of the registration state: the interceptor only
194
+ reads `device.url`, and the detector only needs a peer connection.
195
+
196
+ State ownership: the model owns everything it stores; persistence mechanics belong to `@webex/webex-core`, which
197
+ writes the model through `@persist('@', decider)` unless the device is ephemeral. Evidence: `src/device.js`,
198
+ `src/index.js`, `src/features/features-model.js`, `src/interceptors/device-url.js`, `src/ipNetworkDetector.ts`.
199
+
200
+ ## Data flow and sequence coverage
201
+
202
+ The module is call-and-event driven: promise-returning methods over HTTP through `webex.request`, Ampersand
203
+ change events, and `webex`-level events. Operation groups were inventoried from the public methods, listeners,
204
+ timers, and interceptor; groups sharing actors, ordering, and failure behavior are covered by one diagram.
205
+
206
+ | Operation group | Entry and outcome | Diagram or evidence | Failure and recovery coverage |
207
+ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------ |
208
+ | Registration | `register()` posts to WDM and stores the DTO | Registration diagram; `src/device.js` | Catalog failure, request failure, excessive-registrations branch, stale response |
209
+ | Stale-registration cleanup | `deleteDevices()` removes the oldest third and confirms the count | Cleanup diagram; `src/device.js` | Listing failure, partial deletion failure, polling exhaustion |
210
+ | Refresh | `refresh()` PUTs the device with etag and stores the response | Refresh diagram; `src/device.js` | 404 re-register, stale response, other errors propagate |
211
+ | Unregister and logout | `unregister()` DELETEs the device and clears state | Unregister diagram; `src/index.js` | Not registered, 404, other failure |
212
+ | Registration result processing | `processRegistrationSuccess()` applies etag rules, features, and the ephemeral timer, then emits the event | Result-processing diagram; `src/device.js` | Etag match, debug-toggle parse error |
213
+ | WebSocket URL retrieval | `getWebSocketUrl()` returns the priority-host URL | WebSocket diagram; `src/device.js` | Not registered, wait timeout, unmappable URL |
214
+ | Inactivity enforcement | Settings change probes the intranet URL and arms or re-arms the logout timer | Inactivity diagram; `src/device.js` | Probe failure treated as off-network |
215
+ | Request decoration | The interceptor adds `cisco-device-url` | Interceptor diagram; `src/interceptors/device-url.js` | Unknown service, not-found-after-waiting swallowed, other errors rethrown |
216
+ | IP network detection | `detect()` fills IPv4/IPv6/mDNS timings | Detection diagram; `src/ipNetworkDetector.ts` | Peer-connection failure rejects; concurrent call queued |
217
+
218
+ ### Registration
219
+
220
+ ```mermaid
221
+ sequenceDiagram
222
+ participant Caller
223
+ participant Device
224
+ participant Services as webex.internal.services
225
+ participant WDM as WDM service
226
+ participant Metrics as newMetrics / metrics
227
+ Caller->>Device: register(options)
228
+ Device->>Metrics: setDeviceInfo(device)
229
+ Device->>Services: waitForCatalog('postauth', canRegisterWaitDuration)
230
+ Device->>Services: get('wdm')
231
+ alt wdm missing
232
+ Device-->>Caller: reject "cannot register, wdm service is not available"
233
+ else already registered
234
+ Device->>Device: refresh(options)
235
+ else not registered
236
+ Device->>Metrics: submitInternalEvent internal.register.device.request
237
+ Device->>WDM: POST devices (body, headers, includeUpstreamServices)
238
+ alt request fails
239
+ Device->>Metrics: submitInternalEvent internal.register.device.response
240
+ Device->>Metrics: submitClientMetrics JS_SDK_WDM_REGISTRATION_FAILED
241
+ alt message is "User has excessive device registrations"
242
+ Device->>Device: deleteDevices() then POST once more
243
+ else other error
244
+ Device-->>Caller: reject with the error
245
+ end
246
+ else request id changed during flight
247
+ Device-->>Caller: resolve, response ignored
248
+ else success
249
+ Device->>Metrics: response event and JS_SDK_WDM_REGISTRATION_SUCCESSFUL
250
+ Device->>Device: processRegistrationSuccess(response)
251
+ Device-->>Caller: resolve
252
+ end
253
+ end
254
+ ```
255
+
256
+ ### Stale-registration cleanup
257
+
258
+ ```mermaid
259
+ sequenceDiagram
260
+ participant Device
261
+ participant WDM as WDM service
262
+ Device->>WDM: GET devices
263
+ WDM-->>Device: all devices
264
+ Device->>Device: keep current deviceType, sort by modificationTime
265
+ alt 5 or fewer devices
266
+ Device->>Device: skip cleanup
267
+ else more than 5
268
+ par for each of the oldest ceil(n/3)
269
+ Device->>WDM: DELETE device url
270
+ end
271
+ Note over Device: individual failures are logged, not thrown
272
+ loop up to 5 attempts, 3000 ms apart
273
+ Device->>WDM: GET devices
274
+ alt count at or below target, or poll error
275
+ Device->>Device: stop waiting and continue
276
+ end
277
+ end
278
+ end
279
+ Note over Device: a failure of the first GET rejects deleteDevices()
280
+ ```
281
+
282
+ ### Refresh
283
+
284
+ ```mermaid
285
+ sequenceDiagram
286
+ participant Caller
287
+ participant Device
288
+ participant WDM as WDM service
289
+ Caller->>Device: refresh(options)
290
+ Device->>Device: canRegister()
291
+ alt not registered
292
+ Device->>Device: register(options)
293
+ else registered
294
+ Device->>Device: build body, drop features, mediaCluster, etag
295
+ Device->>WDM: PUT device url (If-None-Match when etag stored)
296
+ alt 404
297
+ Device->>Device: clear()
298
+ Device->>Device: register(options)
299
+ else other error
300
+ Device-->>Caller: reject
301
+ else response and request id unchanged
302
+ Device->>Device: processRegistrationSuccess(response)
303
+ else request id changed
304
+ Device-->>Caller: resolve, response ignored
305
+ end
306
+ end
307
+ ```
308
+
309
+ ### Unregister and logout
310
+
311
+ ```mermaid
312
+ sequenceDiagram
313
+ participant Webex as WebexCore
314
+ participant Device
315
+ participant WDM as WDM service
316
+ Webex->>Device: onBeforeLogout() calls unregister()
317
+ alt not registered
318
+ Device-->>Webex: resolve (warning logged)
319
+ else registered
320
+ Device->>WDM: DELETE device url
321
+ alt success
322
+ Device->>Device: clear()
323
+ Device-->>Webex: resolve
324
+ else 404
325
+ Device->>Device: clear()
326
+ Device-->>Webex: reject with the 404
327
+ else other failure
328
+ Device-->>Webex: reject, state kept
329
+ end
330
+ end
331
+ ```
332
+
333
+ ### Registration result processing
334
+
335
+ ```mermaid
336
+ sequenceDiagram
337
+ participant Device
338
+ participant Features as features
339
+ participant Storage as sessionStorage
340
+ participant Listeners
341
+ Device->>Device: copy body, drop services and serviceHostMap
342
+ alt response etag equals stored etag
343
+ Device->>Features: reset user and entitlement from response
344
+ Note over Device: developer features retained, body.features removed
345
+ else etag differs and debugFeatureTogglesKey set
346
+ Device->>Storage: getItem(key), parse toggles
347
+ Device->>Device: append toggles to developer features (errors logged)
348
+ end
349
+ Device->>Device: set(body), set(etag)
350
+ opt config.ephemeral
351
+ Device->>Device: safeSetTimeout(refresh, (ttl / 2 + 60) s)
352
+ end
353
+ Device->>Listeners: trigger registration:success
354
+ ```
355
+
356
+ ### WebSocket URL retrieval
357
+
358
+ ```mermaid
359
+ sequenceDiagram
360
+ participant Caller
361
+ participant Device
362
+ participant Services as webex.internal.services
363
+ Caller->>Device: getWebSocketUrl(wait)
364
+ alt wait is true
365
+ Device->>Device: waitForRegistration()
366
+ Device->>Services: convertUrlToPriorityHostUrl(webSocketUrl)
367
+ Note over Device: any failure becomes "failed to get the current websocket url"
368
+ else not registered
369
+ Device-->>Caller: reject "device is not registered"
370
+ else registered
371
+ Device->>Services: convertUrlToPriorityHostUrl(webSocketUrl)
372
+ alt mapped url returned
373
+ Device-->>Caller: resolve url
374
+ else falsy result
375
+ Device-->>Caller: reject "failed to get the current websocket url"
376
+ end
377
+ end
378
+ ```
379
+
380
+ ### Inactivity enforcement
381
+
382
+ ```mermaid
383
+ sequenceDiagram
384
+ participant Device
385
+ participant Probe as intranetInactivityCheckUrl
386
+ participant Webex as WebexCore
387
+ Note over Device: change of check url or either duration triggers this
388
+ Device->>Device: checkNetworkReachability()
389
+ alt already checked
390
+ Device->>Device: resetLogoutTimer()
391
+ else no check url
392
+ Device->>Device: isInNetwork = false, resetLogoutTimer()
393
+ else probe
394
+ Device->>Probe: GET (cisco-no-http-redirect, spark-user-agent, trackingid cleared)
395
+ alt reachable
396
+ Device->>Device: isInNetwork = true
397
+ else unreachable
398
+ Device->>Device: isInNetwork = false
399
+ end
400
+ Device->>Device: resetLogoutTimer()
401
+ end
402
+ opt not in meeting, enforcement on, reachability checked
403
+ Device->>Device: setLogoutTimer(inNetwork ? inNetworkInactivityDuration : intranetInactivityDuration)
404
+ Note over Device: user-activity or meeting events call resetLogoutTimer again
405
+ Device->>Webex: logout() when the timer fires
406
+ end
407
+ ```
408
+
409
+ ### Request decoration
410
+
411
+ ```mermaid
412
+ sequenceDiagram
413
+ participant Caller as http-core pipeline
414
+ participant Interceptor as DeviceUrlInterceptor
415
+ participant Services as webex.internal.services
416
+ Caller->>Interceptor: onRequest(options)
417
+ alt device has no url, or header already non-empty
418
+ Interceptor-->>Caller: options unchanged
419
+ else
420
+ Interceptor->>Services: waitForService(service, url)
421
+ alt rejected with "was not found after waiting"
422
+ Interceptor-->>Caller: options unchanged
423
+ else rejected otherwise
424
+ Interceptor-->>Caller: reject
425
+ else resolved url
426
+ Interceptor->>Services: getServiceFromUrl(url)
427
+ alt name is idbroker, oauth, saml, or unknown
428
+ Interceptor-->>Caller: options unchanged
429
+ else
430
+ Interceptor-->>Caller: options with cisco-device-url
431
+ end
432
+ end
433
+ end
434
+ ```
435
+
436
+ ### IP network detection
437
+
438
+ ```mermaid
439
+ sequenceDiagram
440
+ participant Caller
441
+ participant Detector as ipNetworkDetector
442
+ participant PC as RTCPeerConnection
443
+ Caller->>Detector: detect(force)
444
+ alt detection in progress
445
+ Detector->>Detector: remember pendingDetection (latest wins)
446
+ else not forced and results already meaningful
447
+ Detector-->>Caller: return
448
+ else
449
+ Detector->>PC: create, createDataChannel, createOffer, setLocalDescription
450
+ PC-->>Detector: icecandidate events (mDNS, IPv6, IPv4)
451
+ alt IPv4 and IPv6 both seen, or gathering complete, or null candidate
452
+ Detector-->>Caller: resolve, close the connection
453
+ else offer or setLocalDescription fails
454
+ Detector-->>Caller: reject, close the connection
455
+ end
456
+ opt pendingDetection recorded
457
+ Detector->>Detector: run detect again with the stored force flag
458
+ end
459
+ end
460
+ ```
461
+
462
+ ## Class and component relationships
463
+
464
+ ```mermaid
465
+ classDiagram
466
+ class WebexPlugin
467
+ class Interceptor
468
+ class Device {
469
+ +register()
470
+ +refresh()
471
+ +unregister()
472
+ +deleteDevices()
473
+ +getWebSocketUrl()
474
+ +waitForRegistration()
475
+ +checkNetworkReachability()
476
+ +registered
477
+ }
478
+ class FeaturesModel
479
+ class FeatureCollection
480
+ class FeatureModel
481
+ class IpNetworkDetector {
482
+ +detect()
483
+ +supportsIpV4
484
+ +supportsIpV6
485
+ }
486
+ class DeviceUrlInterceptor
487
+ WebexPlugin <|-- Device
488
+ WebexPlugin <|-- IpNetworkDetector
489
+ Interceptor <|-- DeviceUrlInterceptor
490
+ Device *-- FeaturesModel : children.features
491
+ Device *-- IpNetworkDetector : children.ipNetworkDetector
492
+ FeaturesModel *-- FeatureCollection : developer, entitlement, user
493
+ FeatureCollection o-- FeatureModel : keyed by key
494
+ DeviceUrlInterceptor ..> Device : reads url through webex.internal
495
+ ```
496
+
497
+ ## Use cases and flows
498
+
499
+ | Use case | Actor or caller | Primary steps and outcome | Failure or boundary behavior | Evidence |
500
+ | -------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
501
+ | `UC-001` | SDK client after authentication | Calls `register()`; the device is created at WDM, state stored, `registration:success` emitted, `registered` becomes true | Catalog without `wdm` rejects; WDM errors propagate | `src/device.js`, `test/unit/spec/device.js` |
502
+ | `UC-002` | Ephemeral client or any caller | Refresh keeps the registration alive on a timer or on demand; unchanged developer toggles are not re-sent | 404 re-registers; other errors reject | `src/device.js`, `test/integration/spec/device.js` |
503
+ | `UC-003` | Logging-out user | `webex.logout()` triggers `unregister()`, deleting the device and clearing local state | 404 clears and still rejects; other failures keep state | `src/index.js`, `test/integration/spec/webex.js` |
504
+ | `UC-004` | User at the registration limit | Registration fails with the excessive-registrations message, the oldest third is deleted, registration is retried | Second failure propagates; deletion failures are tolerated | `src/device.js`, `test/unit/spec/device.js` |
505
+ | `UC-005` | Feature-flag consumer | Reads typed `features.<collection>` values and subscribes to `change:features` | Unknown or non-numeric values stay strings | `src/features/feature-model.js`, `test/unit/spec/features/features-model.js` |
506
+ | `UC-006` | Mercury connection code | `getWebSocketUrl(true)` returns the priority-host WebSocket URL once registered | Timeout or mapping failure rejects with a generic message | `src/device.js`, `test/integration/spec/device.js` |
507
+ | `UC-007` | Managed-org user | Org inactivity settings arrive with registration; the client probes the intranet URL and logs out after the matching duration without activity | Probe failure counts as off-network; in a meeting no timer is set | `src/device.js`, `test/integration/spec/device.js` |
508
+ | `UC-008` | Meetings plugin | Calls `ipNetworkDetector.detect()` and later reads `supportsIpV4` / `supportsIpV6` | Peer-connection failure rejects the detect call | `src/ipNetworkDetector.ts`, `test/unit/spec/ipNetworkDetector.js` |
509
+ | `UC-009` | Developer debugging features | Sets a JSON object in `sessionStorage` under the configured key; developer toggles are overlaid at the next registration | Invalid JSON is logged and ignored | `src/device.js`, `test/unit/spec/device.js` |
510
+
511
+ ### Cross-boundary use-case flow
512
+
513
+ | Boundary | Transport and ordering | Compatibility | Timeout, retry, recovery |
514
+ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
515
+ | WDM `devices` (POST, GET, DELETE) and device URL (PUT, DELETE) | JSON over `webex.request`; registration precedes any refresh because refresh needs `url`; cleanup runs only between two register attempts | Unknown DTO fields are kept (`extraProperties: 'allow'`) | Catalog wait 10 s default; one cleanup-and-retry; 404 on refresh re-registers; no other retry in this module |
516
+ | Services catalog (`postauth`, `wdm`, priority hosts) | Promise calls on `webex.internal.services` before each registration and URL mapping | Depends on the webex-core services API | Wait timeouts propagate as rejections |
517
+ | Intranet inactivity check URL | One GET per device lifetime with correlation headers removed | URL supplied by WDM | Any failure means off-network; no retry |
518
+ | Metrics plugins | Synchronous submit calls around the registration request | Event names fixed in `src/device.js` and `src/metrics.js` | Calls are not wrapped in error handling here; a throw rejects registration |
519
+ | Browser `RTCPeerConnection` | Offer then local description then ICE events | Browser-only | Rejects on failure; no retry |
520
+
521
+ ## Client state model
522
+
523
+ | State or slice | Owner | Initial state | Transition triggers | Reset or persistence boundary |
524
+ | ----------------------------------------------------------------------------------- | -------------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
525
+ | WDM DTO props (`url`, `webSocketUrl`, `userId`, branding, policy flags, ECM, etc.) | `Device` props | Empty | `processRegistrationSuccess()` via `set(body)` | Persisted unless ephemeral; cleared by `clear()` |
526
+ | `etag` and request ids (`register-request-id`, `refresh-request-id`) | `Device` extra attributes | Unset | Registration and refresh responses; each call writes a fresh id | Cleared with the model; id change invalidates in-flight responses |
527
+ | `features.developer`, `features.entitlement`, `features.user` | `FeaturesModel` collections | Empty collections | Registration responses, debug toggles | Reset by `clear()` |
528
+ | Inactivity session (`isReachabilityChecked`, `isInNetwork`, `isInMeeting`, `lastUserActivityDate`, `logoutTimer`) | `Device` session props | Unset / `false` for reachability | Setting changes, `meeting started`/`ended`, `user-activity` | Ampersand session properties, which are not serialized with the declared props |
529
+ | `energyForecastConfig` | `Device` session prop | Unset | `setEnergyForecastConfig()` | Session only |
530
+ | `refreshTimer` | `Device` instance field | Undefined | Ephemeral registration success | Never cleared by this module |
531
+ | Detection results (`firstIpV4`, `firstIpV6`, `firstMdns`, `totalTime`, `state`, `pendingDetection`) | `IpNetworkDetector` props | `-1` times, state `initial` | `detect()` | Reset at the start of each detection |
532
+
533
+ ## Business rules and invariants
534
+
535
+ | ID | Invariant | WHY | Enforcement source | Test evidence |
536
+ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | --------------------------------------- | --------------------------------------------------- |
537
+ | `INV-001` | `registered` is true exactly when `url` is non-empty | One source of truth for registration state | `src/device.js` | `test/unit/spec/device.js` |
538
+ | `INV-002` | A register or refresh response whose stored request id no longer matches the call's id is dropped without changing state | A logout during an in-flight request must not resurrect the device | `src/device.js` | `test/unit/spec/device.js` |
539
+ | `INV-003` | `services` and `serviceHostMap` from WDM are never stored on the device | The service catalog is the single owner of service data | `src/device.js` | `test/integration/spec/device.js` |
540
+ | `INV-004` | A refresh body never contains `features`, `mediaCluster`, or `etag` | Those are server-derived or transport-level and must not be echoed back | `src/device.js` | none found |
541
+ | `INV-005` | An ephemeral device is not persisted | Ephemeral registrations must not outlive the session that created them | `src/device.js` | none found |
542
+ | `INV-006` | `cisco-device-url` is not added when the device is unregistered, the header is already non-empty, or the service is `idbroker`, `oauth`, `saml`, or unknown | Auth-related services must not receive device identity; existing values win | `src/interceptors/device-url.js` | `test/unit/spec/interceptors/device-url.js` |
543
+ | `INV-007` | The logout timer is armed only when not in a meeting, inactivity enforcement is on, reachability has been checked, and the duration is positive | Prevents logging out active meeting participants or unconfigured orgs | `src/device.js` | `test/unit/spec/device.js`, `test/integration/spec/device.js` |
544
+ | `INV-008` | Cleanup never deletes devices of another `deviceType`, and never runs at or below the minimum device count | A user's other clients must survive cleanup | `src/device.js`, `src/constants.js` | `test/unit/spec/device.js` |
545
+
546
+ ## Concurrency and reactive flow
547
+
548
+ - Execution model: single-threaded JavaScript; every public operation returns a promise; state changes
549
+ propagate through Ampersand `change` events and `webex`-level events; timers use `safeSetTimeout`, except the
550
+ cleanup polling delay which uses `setTimeout`.
551
+ - Ordering guarantees: `@waitForValue('@')` delays `register`, `refresh`, `_registerInternal` and `unregister`
552
+ until persisted state has been loaded; registration success always precedes `registration:success`; no
553
+ ordering guarantee exists between `refresh()` and `unregister()` other than the stale-response guard.
554
+ - Idempotency and retry: `@oneFlight` on `refresh`, `_registerInternal` and `unregister` returns the in-flight
555
+ promise to concurrent callers; `register()` itself is not de-duplicated but its POST is; the only automatic retries are the cleanup-and-retry
556
+ once and the 404 re-registration.
557
+ - Shared-state protection: request ids guard against responses arriving after `clear()`; the single
558
+ `pendingDetection` slot lets only the most recent queued `detect()` request survive.
559
+ - Blocking restrictions: `getDebugFeatures()` and its `JSON.parse` run synchronously during result processing;
560
+ no other work in this module blocks on I/O synchronously.
561
+
562
+ ## State machine
563
+
564
+ ```mermaid
565
+ stateDiagram-v2
566
+ [*] --> Unregistered
567
+ Unregistered --> Registering: register()
568
+ Registering --> Registered: success, registration:success
569
+ Registering --> Registering: excessive registrations, cleanup then retry once
570
+ Registering --> Unregistered: failure
571
+ Registered --> Refreshing: refresh() or ephemeral timer
572
+ Refreshing --> Registered: success
573
+ Refreshing --> Registering: 404, clear then register
574
+ Registered --> Unregistered: unregister() or logout, clear()
575
+ ```
576
+
577
+ ```mermaid
578
+ stateDiagram-v2
579
+ [*] --> initial
580
+ initial --> in_progress: detect()
581
+ idle --> in_progress: detect(force) or no meaningful result yet
582
+ in_progress --> in_progress: detect() queues pendingDetection
583
+ in_progress --> idle: gathering complete or error
584
+ idle --> in_progress: queued detection replays
585
+ ```
586
+
587
+ `in_progress` is the code value `in-progress`. A `detect(false)` call on `idle` with meaningful results returns
588
+ without a transition; `Registered` also ends when `clear()` is called directly.
589
+
590
+ ## Caller-visible failure modes
591
+
592
+ | Condition | Signal or result | Caller behavior | Retry or recovery | Evidence |
593
+ | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------- | --------------------------------------------- |
594
+ | `wdm` absent from the `postauth` catalog | Rejects with `device: cannot register, 'wdm' service is not available from the postauth catalog` | Surface or retry after the catalog loads | Caller retries | `src/device.js` |
595
+ | Catalog wait times out | Rejection from `services.waitForCatalog` | Same | Caller retries | `src/device.js` |
596
+ | WDM rejects registration other than the excessive-limit case | The request error (HTTP error with status) | Handle as a normal request error | None in module | `src/device.js` |
597
+ | Excessive registrations persist after cleanup | The second registration error | Report to the user | None in module | `test/unit/spec/device.js` |
598
+ | `deleteDevices()` cannot list devices | Rejects with the listing error | Registration retry is abandoned | Caller retries | `test/unit/spec/device.js` |
599
+ | Refresh fails with a non-404 error | The request error | Caller decides | None in module | `test/integration/spec/device.js` |
600
+ | `unregister()` returns 404 | Rejects with the 404 after clearing local state | Treat as done; local state is already cleared | None needed | `test/unit/spec/device.js` |
601
+ | `unregister()` fails otherwise | Rejects; state retained | Retry later | Caller retries | `test/unit/spec/device.js` |
602
+ | `getWebSocketUrl()` with a non-registered device | Rejects `device: cannot get websocket url, device is not registered` | Call with `wait` true or after registration | Caller retries | `test/integration/spec/device.js` |
603
+ | `getWebSocketUrl()` cannot map or wait times out | Rejects `device: failed to get the current websocket url` | Treat as no connection URL | Caller retries | `test/integration/spec/device.js` |
604
+ | `waitForRegistration()` exceeds its timeout | Rejects `device: timeout occured while waiting for registration` | Treat as not registered | Caller retries | `test/integration/spec/device.js` |
605
+ | `detect()` cannot create or apply the offer | Rejects with the underlying error | Fall back to unknown IP support | None in module | `test/unit/spec/ipNetworkDetector.js` |
606
+ | Interceptor wait rejects with anything but not-found-after-waiting | The rejection | Request fails | None | `test/unit/spec/interceptors/device-url.js` |
607
+
608
+ ## Pitfalls and constraints
609
+
610
+ - `clear()` and `unregister()` never cancel `refreshTimer` (`src/device.js`): an ephemeral device that is cleared
611
+ can still fire a scheduled `refresh()`, which will then register a new device.
612
+ - `getWebSocketUrl()` without `wait` calls `services.convertUrlToPriorityHostUrl`, which throws synchronously when
613
+ `webSocketUrl` is not a catalog URL (`src/device.js`); only the falsy-result case becomes a rejection.
614
+ - `waitForRegistration()` does not return after resolving for an already-registered device, so it still arms a
615
+ timeout timer and a one-time listener that become no-ops (`src/device.js`).
616
+ - `refresh()` builds its body from `serialize()` plus `config.body` and, unlike `register()`, does not overlay
617
+ `config.defaults.body` (`src/device.js`).
618
+ - The response-timestamp ordering in `_registerInternal` matters: nothing may run before the
619
+ `internal.register.device.response` event, or call-diagnostic latency is skewed (`src/device.js`).
620
+ - Compatibility: other packages read `webex.internal.device` property names directly, so renaming a prop or
621
+ event is a breaking change for the whole SDK (see consumers in the architecture document).
622
+ - Security: the `cisco-device-url` header must never reach authentication services (`INV-006`); credentials are
623
+ never handled here.
624
+ - Platform: `IpNetworkDetector` needs a browser `RTCPeerConnection`; in plain Node it rejects or throws.
625
+
626
+ ## Module-specific rules
627
+
628
+ - Do: write Ampersand `derived` functions as `fn() {}` methods, not arrow functions, because Ampersand binds
629
+ `this` (comment in `src/device.js`).
630
+ - Do: apply `@persist` only to `initialize` and keep `@waitForValue('@')` on any method that needs loaded
631
+ persisted state (decorator contract enforced by `@webex/webex-core`).
632
+ - Do: keep `extraProperties: 'allow'` on `Device` so unknown WDM DTO fields never fail initialization.
633
+ - Do not: add processing above the `internal.register.device.response` event in `_registerInternal`.
634
+ - Do not: store `services` or `serviceHostMap` on the device (`INV-003`).
635
+ - Do not: change `MIN_DEVICES_FOR_CLEANUP`, `MAX_DELETION_CONFIRMATION_ATTEMPTS`, or
636
+ `DELETION_CONFIRMATION_DELAY_MS` without updating the cleanup tests that depend on them.
637
+
638
+ ## Export stability
639
+
640
+ | Export or entry point | Consumer | Stability | Versioning and deprecation rule | Declaration or API report |
641
+ | -------------------------------------------------------- | --------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------- |
642
+ | Default export `Device` and the `webex.internal.device` API | SDK plugins, first-party clients | Internal; used widely across the SDK | README states semantic versioning is not strictly followed; deprecate with `@deprecated` as done for `markUrlFailedAndGetNew` | `src/index.js` |
643
+ | `config`, `constants` | Same | Internal | Same | `src/index.js` |
644
+ | `CatalogDetails`, `DeviceRegistrationOptions` | Callers of `register`/`refresh` | Internal; enum values are sent to WDM | Same | `src/types.ts` |
645
+ | `DeviceUrlInterceptor`, `FeatureCollection`, `FeatureModel`, `FeaturesModel` | SDK internals and tests | Internal | Same | `src/index.js` |
646
+
647
+ No API report or generated declaration is committed; `package.json` and the module sources are the native
648
+ artifacts.
649
+
650
+ ## Verification
651
+
652
+ | Requirement or invariant | Test level | Positive evidence | Negative or boundary evidence | Gap |
653
+ | -------------------------------------------- | ------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------- |
654
+ | `MOD-001` | Integration | `test/integration/spec/webex.js` | none found | Needs Webex test users |
655
+ | `MOD-002` | Integration | `test/integration/spec/device.js` | `test/integration/spec/device.js` | No unit test |
656
+ | `MOD-003`, `MOD-004`, `MOD-005` | Unit and integration | `test/unit/spec/device.js`, `test/integration/spec/device.js` | `test/unit/spec/device.js` | Default body values not asserted |
657
+ | `MOD-006` | Unit and integration | `test/unit/spec/device.js` | `test/unit/spec/device.js` (no etag case) | None |
658
+ | `MOD-007` | Integration | `test/integration/spec/device.js` | `test/integration/spec/device.js` | No unit test |
659
+ | `MOD-008`, `MOD-009`, `MOD-010`, `INV-008` | Unit | `test/unit/spec/device.js` | `test/unit/spec/device.js` (thresholds, partial failure, polling errors) | None |
660
+ | `MOD-011` | Unit and integration | `test/unit/spec/device.js` | `test/unit/spec/device.js` (404 and other failures) | None |
661
+ | `MOD-012`, `MOD-013`, `MOD-014` | Unit | `test/unit/spec/device.js` | `test/unit/spec/device.js` (etag mismatch, debug toggles) | None |
662
+ | `MOD-015` | Integration | `test/integration/spec/device.js` | none found | Timer never-cleared behavior untested |
663
+ | `MOD-016`, `MOD-017` | Unit | `test/unit/spec/features/feature-model.js`, `test/unit/spec/features/features-model.js` | `test/unit/spec/features/feature-model.js` | None |
664
+ | `MOD-018`, `MOD-019` | Integration | `test/integration/spec/device.js` | `test/integration/spec/device.js` | No unit test; synchronous-throw pitfall untested |
665
+ | `MOD-020`, `MOD-021`, `INV-007` | Unit and integration | `test/unit/spec/device.js`, `test/integration/spec/device.js` | `test/integration/spec/device.js` | None |
666
+ | `MOD-022`, `INV-006` | Unit | `test/unit/spec/interceptors/device-url.js` | `test/unit/spec/interceptors/device-url.js` | None |
667
+ | `MOD-023` | Unit | `test/unit/spec/ipNetworkDetector.js` | `test/unit/spec/ipNetworkDetector.js` | None |
668
+ | `MOD-024` | Unit | `test/unit/spec/device.js` | `test/unit/spec/device.js` | Refresh path emits none |
669
+ | `MOD-025` | Integration | `test/integration/spec/device.js` | none found | None |
670
+ | `MOD-026` | — | none found | none found | No test pins the defaults |
671
+ | `INV-001`, `INV-002`, `INV-003` | Unit and integration | `test/unit/spec/device.js`, `test/integration/spec/device.js` | `test/unit/spec/device.js` | None |
672
+ | `INV-004`, `INV-005` | — | none found | none found | No test asserts the refresh body exclusions or ephemeral non-persistence |
673
+
674
+ Run unit tests with `yarn workspace @webex/internal-plugin-device test:unit`; integration and browser suites need Webex test
675
+ users and a built workspace. A specification is complete only when its public surface, invariants, failure modes,
676
+ and test evidence agree with the implementation.