@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.
- package/.repo-context.json +519 -0
- package/.sdd/manifest.json +335 -0
- package/AGENTS.md +181 -0
- package/dist/device.js +1 -1
- package/dist/ipNetworkDetector.js +1 -1
- package/docs/adr/index.md +28 -0
- package/docs/architecture.md +237 -0
- package/docs/getting-started.md +91 -0
- package/docs/index.md +40 -0
- package/docs/specs/README.md +80 -0
- package/package.json +4 -4
- package/schemas/repo-context.schema.json +796 -0
- package/src/docs/README.md +676 -0
|
@@ -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.
|