# @phystack/hub-client

## 6.13.0

### Minor Changes

- [#911](https://github.com/phystack/ps-platform/pull/911) [`3fe416c`](https://github.com/phystack/ps-platform/commit/3fe416c794bc850adfeaf6fd0b9df9fb1abe3d29) Thanks [@kamilplaczek-ombori](https://github.com/kamilplaczek-ombori)! - Web app sessions survive page reloads, tab discards, and browser kills (TECH-1468). The rotating refresh grant is persisted in localStorage, keyed by the endpoint's composite urlId, and resumed silently on boot while the server-side session deadline holds. A fresh claim code still wins over a stored session; a terminally rejected code (expired QR screenshot, stale history entry) falls back to a valid stored session; terminal failures clear the record. Every rotation is persisted write-before-use, a cheap multi-tab guard adopts a grant rotated by another tab, and a `pageshow`/visibility wake re-dials a dead socket without waiting for the heartbeat timeout. Storage failures degrade to the previous memory-only behavior.

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@6.13.0

## 6.12.0

### Minor Changes

- [#902](https://github.com/phystack/ps-platform/pull/902) [`e03a4a4`](https://github.com/phystack/ps-platform/commit/e03a4a49b6490694ed8ecef7fd0d6cd110990fcb) Thanks [@kamilplaczek-ombori](https://github.com/kamilplaczek-ombori)! - Web app session mint moved to the flat body-keyed route `POST {sessionBaseUrl}/api/v1/web-sessions` with `{ urlId, code | refreshToken }` — the urlId is now the tenant-scoped composite path `{tenantSlug}/{name}` published in `boot.json` (TECH-1469). Requires a phyhub with the matching route; the old `POST /api/v1/web-endpoints/{urlId}/session` route is gone.

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@6.12.0

## 6.11.0

### Minor Changes

- [#896](https://github.com/phystack/ps-platform/pull/896) [`5f8c651`](https://github.com/phystack/ps-platform/commit/5f8c6512a0ebdfb9c6238ce518dc56e6264d5c3f) Thanks [@kamilplaczek-ombori](https://github.com/kamilplaczek-ombori)! - Add the device-side web-session claim-code issuer for the web-app QR flow. `client.createWebSessionCode()` mints one claim code for a Web endpoint, identified by exactly one of `twinId` (the Web twin `_id` a settings twin picker stores) or `endpointId` (the Web twin `deviceId` shown in Console/CLI). `client.subscribeWebSessionCode()` keeps a continuously displayed QR fresh: it delivers the initial code and every rotation through a single listener, renews at a fraction of the code TTL with downward jitter, re-mints after socket reconnects, retries failures with capped exponential backoff, and reports explicit `active`/`unavailable` states so apps never render a dead code. Device sessions only — web and cloud sessions get a clear synchronous error.

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@6.11.0

## 6.10.0

### Minor Changes

- [#893](https://github.com/phystack/ps-platform/pull/893) [`6465a13`](https://github.com/phystack/ps-platform/commit/6465a134777bc8f701829517adec4dc5b35936a0) Thanks [@kamilplaczek-ombori](https://github.com/kamilplaczek-ombori)! - Add the device-side web-session claim-code issuer for the web-app QR flow. `client.createWebSessionCode()` mints one claim code for a Web endpoint, identified by exactly one of `twinId` (the Web twin `_id` a settings twin picker stores) or `endpointId` (the Web twin `deviceId` shown in Console/CLI). `client.subscribeWebSessionCode()` keeps a continuously displayed QR fresh: it delivers the initial code and every rotation through a single listener, renews at a fraction of the code TTL with downward jitter, re-mints after socket reconnects, retries failures with capped exponential backoff, and reports explicit `active`/`unavailable` states so apps never render a dead code. Device sessions only — web and cloud sessions get a clear synchronous error.

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@6.10.0

## 6.9.0

### Minor Changes

- [#831](https://github.com/phystack/ps-platform/pull/831) [`b5bf6ce`](https://github.com/phystack/ps-platform/commit/b5bf6ce81464cbf314ff262ed186820ca4b091f8) Thanks [@kamilplaczek-ombori](https://github.com/kamilplaczek-ombori)! - Add the web-app connection branch: `PhyHubClient.connect({ webApp: { urlId, sessionBaseUrl, phyhubUrl, code } })` establishes a phyhub session from a QR claim code via `POST {sessionBaseUrl}/api/v1/web-endpoints/{urlId}/session` (phyhub's public mint route, reached through the API gateway's regional passthrough), with rotating refresh grants, a four-layer token refresh (foreground timer, visibilitychange, handshake-time auth callback, connect_error fallback), twin-subscription replay after the routine token-expiry reconnects, and a `sessionTerminated` signal when the session requires a new QR scan. Web apps are auto-detected: `connectPhyClient()` with no arguments probes `./boot.json` (published by the platform next to every web bundle) when the page has no `#instanceId`, and configures the web session from it — zero-config, like a screen app. The claim code is scrubbed from the URL fragment after successful redemption, `unsubscribeTwin()` drops a twin from the replay registry, and the whole session lifecycle logs verbosely to the console by default (pass `logger` to silence). Also exports `exchangeWebAppSession`, `WebAppSessionError`, `WebAppConnection`, `readWebSessionCodeFromLocation`, `clearWebSessionCodeFromLocation`, `loadWebAppBoot`, and the `WebAppBoot` type.

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@6.9.0

## 6.8.0

### Minor Changes

- [#858](https://github.com/phystack/ps-platform/pull/858) [`ea35e0a`](https://github.com/phystack/ps-platform/commit/ea35e0a7894a1235cff1a8a589e7e7f7b484029d) Thanks [@kamilplaczek-ombori](https://github.com/kamilplaczek-ombori)! - Peripheral identity under the tenant-wide hardwareId key (TECH-1394):

  - `instance.generateScopedHardwareId(id, scope?)` mints hardwareIds that carry their uniqueness scope: `g:<id>` (global, default), `s-<spaceId>:<id>` (space — survives device moves within the space), `d-<deviceId-without-dashes>:<id>` (device-pinned). The user part has a flat 64-char budget and charset `[a-zA-Z0-9._-]` (colon is reserved as the prefix separator); violations and unavailable scope ids throw — the helper never silently falls back to an unscoped id. The pure formatter is exported as `formatScopedHardwareId`.
  - Ownership-loss revocation: when another device takes over a peripheral this app registered (same hardwareId, tenant-wide upsert), the client revokes the local `PeripheralInstance` — `emit`/`updateReported` reject with an explicit `ownership lost` error instead of silently double-driving hardware, the twin-room subscription is dropped (including from the reconnect re-subscribe set), and the new `onOwnershipLost(callback)` hook fires once so the app can release the hardware. Triggered by the takeover `twinUpdated` notification and, as a fallback, by a report rejected with `PeripheralOwnershipError`.
  - `instance.createPeripheralTwinDetailed(...)` resolves the full `{twin, created, handover}` ack; `createPeripheralTwin` is unchanged (resolves the twin) but now logs a warning whenever the registration took the twin over from another device or instance.

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@6.8.0

## 6.7.0

### Minor Changes

- [#856](https://github.com/phystack/ps-platform/pull/856) [`3ea5143`](https://github.com/phystack/ps-platform/commit/3ea51433a73ef93e582262b72f6d4c70231ebb67) Thanks [@kamilplaczek-ombori](https://github.com/kamilplaczek-ombori)! - Fix socket listeners accumulating on the shared singleton socket (TECH-1334), and add listener lifecycle APIs.

  - `setupSocketListeners()` and the cloud-app listener block are now idempotent per socket object: re-entering `initializeConnection()` after a disconnect no longer stacks a duplicate handler set. This removes duplicate twin-event delivery (N reconnect cycles no longer produce N+1 deliveries), the PONG keepalive amplification, and the multiplied re-subscribe bursts.
  - `createTwinMessaging.on()`, `Instance.on()`, `PeripheralTwinInstance.on()`, `onUpdateReported()` and `onUpdateDesired()` now return an unsubscribe function (previously listeners could never be removed). `createTwinMessaging`, `Instance` and `PeripheralInstance` also gain `off(type, callback)` (on `PeripheralInstance` it removes by the caller's original callback identity even when advisory validation wraps the listener).
  - Arming a replacement socket object now first detaches this client's tracked handlers from the abandoned socket (the connection singleton is replaced on `connect_error`, and the old socket can still be alive and delivering duplicates).
  - `disconnect()` now works for device/screen connections: it detaches exactly the handlers this client armed on the shared socket (the page-wide singleton itself is left connected for other consumers), clears listener registries, and leaves the client reusable. On both branches, `disconnect()` now also clears the twin listener registries, cached instances, and the emit queue — cloud-app listeners registered before a `disconnect()` no longer survive into a later reconnect.
  - Per-request `'error'` listeners no longer leak: `getDeviceStatus()`, `getDeviceInstance()`, `getDeviceNetworks()`, `setScreenInstanceReportedProperties()`, `request()`, `subscribeTwin()`, `getTwinById()`, `updateReportedProperties()`, `getPeripheralTwins()` and `createPeripheralTwin()` each armed a bare `socket.on('error', ...)` per call and never removed it. They now detach on every settle path, and `disconnect()` drops the listeners of requests still in flight.
  - The await-connect step of `initializeConnection()` no longer strands a `once('connect_error')` listener on every successful connect (and vice versa) — the counterpart listener is detached when either fires.

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@6.7.0

## 6.6.0

### Minor Changes

- [#846](https://github.com/phystack/ps-platform/pull/846) [`c50aa4c`](https://github.com/phystack/ps-platform/commit/c50aa4cc489a0cd51ad9347d05ac3dc06d7766e3) Thanks [@kamilplaczek-ombori](https://github.com/kamilplaczek-ombori)! - Support audio tracks in WebRTC media streams. `MediaStreamOptions.kinds` lets a receiving consumer negotiate `['audio']`, `['video']`, or both at once (one transceiver per kind); the default `['video']` preserves the previous behaviour for every existing caller. When `kinds` is passed explicitly, a requested kind that produces no remote track within `connectionTimeout` rejects the connection with the new `MissingMediaTrackError` (exported, with `missingKinds`) instead of resolving half-connected. `ensureWebRTCGlobals()` now also polyfills `RTCAudioSource` and `RTCAudioSink` alongside `RTCVideoSource`, so Node consumers no longer import `@roamhq/wrtc` directly for audio. Also exports the `MediaTrackKind` type.

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@6.6.0

## 6.5.1

### Patch Changes

- [#716](https://github.com/phystack/ps-platform/pull/716) [`d985b7d`](https://github.com/phystack/ps-platform/commit/d985b7d217340dc18b54805d2ccd338a1ddc4aa3) Thanks [@kamilplaczek-ombori](https://github.com/kamilplaczek-ombori)! - createPeripheralTwin: reject the returned promise on an error ack instead of throwing inside the socket.io callback. The throw escaped as an uncaughtException (crashing the app) while the awaited promise stayed pending forever; the rejection now carries the server's rejection reason from the ack `message` so device apps can log why the create was refused.

- [#716](https://github.com/phystack/ps-platform/pull/716) [`d985b7d`](https://github.com/phystack/ps-platform/commit/d985b7d217340dc18b54805d2ccd338a1ddc4aa3) Thanks [@kamilplaczek-ombori](https://github.com/kamilplaczek-ombori)! - createPeripheralTwin no longer pre-lists twins and throws "already exists" — the server's create-or-update (keyed by tenant+device+hardwareId) is the uniqueness authority, and re-calling create is the supported way to re-register: refresh the peripheral name, re-point the owner instance, update descriptor pins. Requires a phyhub with the peripheral upsert (TECH-408); against older phyhub versions an unconditional create can duplicate twins, so deploy phyhub first.

- [#716](https://github.com/phystack/ps-platform/pull/716) [`d985b7d`](https://github.com/phystack/ps-platform/commit/d985b7d217340dc18b54805d2ccd338a1ddc4aa3) Thanks [@kamilplaczek-ombori](https://github.com/kamilplaczek-ombori)! - getPeripheralTwins now rejects on an error ack instead of resolving an empty array — a failed lookup was indistinguishable from "no peripherals exist", which drove find-or-create logic into duplicate registrations. updateReportedProperties rejections now carry the server's reason from the ack message. BREAKING-ish for apps: getPeripheralTwins can now reject, so callers polling in setInterval must catch (an unhandled rejection crashes the process on Node 15+).

- Updated dependencies []:
  - @phystack/socket.io-proxy@6.5.1

## 6.5.0

### Minor Changes

- [#694](https://github.com/phystack/ps-platform/pull/694) [`2ce5ab5`](https://github.com/phystack/ps-platform/commit/2ce5ab5c280cc37178e06ce3772d7020876eb57a) Thanks [@kamilplaczek-ombori](https://github.com/kamilplaczek-ombori)! - Add a one-shot (HTTP-signaled) WebRTC answer API for camera stream viewing (WHEP). A viewer can now deliver a single offer SDP out-of-band (e.g. an HTTP request body, with no socket transport of its own) and receive one non-trickle answer SDP that embeds the publisher's relay candidates. New public surface: `PeripheralTwinInstance.answerMediaOffer(offerSdp, { viewerId, channelName? })`, `WebRTCManager.answerMediaOffer(targetTwinId, offerSdp, { viewerId, channelName? })`, `ResponderFanout.ingestExternalOffer(peerId, offerSdp)`, an optional `signalingSink` on the media/peer-connection layers, and the typed errors `NoMediaResponderError` / `PeerCapReachedError`. Purely additive — the existing socket-signaled offer/answer/ICE behaviour is unchanged, and the offer is answered on the same already-armed media responder fan-out (`onMediaStream`), so peer-cap and connect-timeout eviction apply identically.

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@6.5.0

## 6.4.0

### Minor Changes

- [#678](https://github.com/phystack/ps-platform/pull/678) [`1f5da2c`](https://github.com/phystack/ps-platform/commit/1f5da2cb990f6c570fd2033493916049b9ccb5c6) Thanks [@kamilplaczek-ombori](https://github.com/kamilplaczek-ombori)! - Add optional structured `data` field to `TwinMessageResult` — the canonical carrier for a request-response action's returns payload (matching the descriptor's `actions.<name>.returns` schema). `message` stays human-readable text. Additive: responders that JSON-encode returns into `message` compile and behave exactly as before; phyhub's advisory returns-validation only engages when `data` is present.

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@6.4.0

## 6.3.7

### Patch Changes

- [#652](https://github.com/phystack/ps-platform/pull/652) [`01a6c64`](https://github.com/phystack/ps-platform/commit/01a6c6423834f15361c33af75bf0fc1d4e0a42a2) Thanks [@zahid-hsn](https://github.com/zahid-hsn)! - Fix WebRTC re-subscribe on the same peripheral. `WebRTCManager` cached each `MediaStreamHandler` under `${twinId}:${channelName}` but never evicted it on `stream.close()`, so a second `getMediaStream()` on the same twin+channel returned the closed handler and `connect()` threw `"MediaStreamHandler has been closed"` — a consumer that closed a stream and re-opened it (e.g. toggling a camera view off/on) got no video and no way to recover. `createMediaStream` now ignores a closed cached handler and drops the stale entry, building a fresh peer connection instead. (The data-channel path in `createDataChannel` has the same latent pattern; left unchanged here.)

- Updated dependencies []:
  - @phystack/socket.io-proxy@6.3.7

## 6.3.6

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@6.3.6

## 6.3.5

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@6.3.5

## 6.3.4

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@6.3.4

## 6.3.3

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@6.3.3

## 6.3.2

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@6.3.2

## 6.3.1

### Patch Changes

- [#601](https://github.com/phystack/ps-platform/pull/601) [`b29a265`](https://github.com/phystack/ps-platform/commit/b29a265ae059965e9218003dd3dc9f0a18ea6c3e) Thanks [@zahid-hsn](https://github.com/zahid-hsn)! - fix(webrtc): peripheral-attached WebRTC now gets TURN credentials

  `PeripheralTwinInstance.getWebRTCManager()` constructed the WebRTC manager with no
  options, so it never received an `iceServersProvider`. Every peripheral-attached
  connection — camera/sensor media (`onMediaStream`/`getMediaStream`) and data
  channels (`onDataChannel`/`getDataChannel`) — silently fell back to static STUN
  and could not traverse a symmetric/CGNAT path (cross-network relay failed). The
  provider is now injected, mirroring `PhyHubClient.getWebRTCManager()`, fetching
  short-lived TURN credentials via `getIceServers('media')`.

- Updated dependencies []:
  - @phystack/socket.io-proxy@6.3.1

## 6.3.0

### Minor Changes

- [#540](https://github.com/phystack/ps-platform/pull/540) [`29ee9f6`](https://github.com/phystack/ps-platform/commit/29ee9f669ec14173276a5ba350de0e512559cd71) Thanks [@hassellof](https://github.com/hassellof)! - Add optional, opt-in, advisory descriptor validation of the facets an edge app emits (reported properties, event payloads, action returns). The app supplies precompiled validators built at build time from its own compiled descriptor schema (build-time embed) via `peripheralInstance.enableAdvisoryValidation(validator)` or the new optional `getPeripheralInstance(twinId, validator)` argument. Validation is advisory and routing-independent: a mismatch logs a warning but the message is still sent/stored, and the validation path is fail-open (no validator or a thrown validator → skip). Apps that don't opt in get byte-identical existing behavior. hub-client adds no JSON-schema runtime to the device bundle — the validator shape is the Ajv-compiled `(data) => boolean` + `.errors` function the app already produces at build time.

- [#591](https://github.com/phystack/ps-platform/pull/591) [`c67a623`](https://github.com/phystack/ps-platform/commit/c67a62390c02b8d9d8c23e0dee2f7516d6913f20) Thanks [@zahid-hsn](https://github.com/zahid-hsn)! - Add `iceServersProvider` to `WebRTCManagerOptions` so the full ICE server list (STUN + TURN with credentials) can be supplied asynchronously at connect time, and stop forcing relay-only when TURN is configured.

  This unblocks cross-network (NAT-traversal) WebRTC. Previously TURN could only be configured via the static `PHYSTACK_TURN_URL`/`PHYSTACK_TURN_USERNAME`/`PHYSTACK_TURN_CREDENTIAL` env vars, read once at module load — unsuitable for the short-lived (≈5 min) HMAC credentials a coturn relay issues, and with no way to refresh them. Callers can now pass `iceServersProvider: () => fetch(...)` to fetch ephemeral credentials from a server endpoint (e.g. phyhub's `/v1/webrtc/ice-servers`) so the shared secret never reaches the client. The provider is resolved once per connection lifecycle and cached across rebuilds; a throw or empty result falls back to the static stun/turn config, so a transient fetch failure degrades to STUN-only rather than failing the connection. On the no-STUN retry the provider's TURN entries are retained so relay fallback still works.

  Also fixes the ICE transport policy: setting a TURN URL no longer forces `iceTransportPolicy: 'relay'` (TURN-only, which disabled host/srflx candidates and routed all same-network traffic through the relay). The default is now always `'all'` — TURN is a relay fallback, not the only transport. Callers that genuinely want relay-only (e.g. a cross-NAT verification harness) can still pass `iceTransportPolicy: 'relay'` explicitly.

  Fully backward-compatible: with no provider set and no TURN env vars, behaviour is unchanged (Google STUN, direct connections).

- [#586](https://github.com/phystack/ps-platform/pull/586) [`47c76b5`](https://github.com/phystack/ps-platform/commit/47c76b5f2606696ee7d31185aa91f2b436f119d4) Thanks [@zahid-hsn](https://github.com/zahid-hsn)! - Add one-to-many (fan-out) WebRTC support so a single source — e.g. one camera, or a data-channel responder — can serve many simultaneous peers at once, each on its own peer connection.

  Previously a responder served exactly one peer per channel: peers sharing a peripheral channel all sign their signaling with the same `sourceTwinId` (the peripheral id), so the responder could not tell two peers apart and a second peer would hijack/kill the first. Apps worked around this with a pool of channels (camera1..cameraN).

  The library now mints a per-session `peerId` on the initiator side, stamps it on every offer/answer/ice, and ignores signals bearing a different id. A new `ResponderFanout` dispatcher owns the single subscription, demultiplexes by `peerId`, and runs one responder session per peer — so a single source fans out to N peers with no interference (N=1 is just the single-peer case).

  API (one unified responder API; there were no existing consumers, so the prior single-fire path was removed rather than kept alongside):

  - `PeripheralTwinInstance.onMediaStream(callback, options?)` / `Instance.onMediaStream(...)` — media responder; the callback fires once per connected peer with `(stream, peerId, info)` and the method returns a `stop()`. Supply `options.createLocalStream` to mint a fresh track per peer.
  - `PeripheralTwinInstance.onDataChannel(callback, options?)` / `Instance.onDataChannel(...)` — data-channel responder; same shape.
  - `WebRTCManager.acceptMediaStream` / `acceptDataChannel` are the underlying primitives; both accept an optional `maxPeers` cap and return `stop()`.
  - The initiator side (`getMediaStream` / `getDataChannel`) is unchanged.
  - `PhygridMediaStream.getPeerId()` / `PhygridDataChannel.getPeerId()` expose the per-session id.

  The responder callback also receives a `PeerInfo` argument (`{ deviceId? }`) — the remote's `sourceDeviceId` from the signaling envelope — so the host can tell which physical device each peer is on (`peerId` distinguishes sessions; `deviceId` distinguishes devices).

  A peer that offers but never reaches `connected` within a deadline is evicted so a stuck offerer can't hold a slot.

  Sending (`sendonly`) responder peers also get a liveness backstop: such a peer receives no media (so the frame monitor never runs) and `@roamhq/wrtc` does not reliably transition `connectionState` when a remote viewer silently disappears (page reload/crash), which would otherwise leak the viewer's slot and eventually exhaust a `maxPeers` fan-out. The handler now watches inbound transport activity (STUN consent + RTCP) via `getStats` and, once a connection has proven live, tears the peer down when inbound goes silent past a timeout — freeing the slot. It only acts after observing real inbound activity, so a stats backend that reports nothing never causes a false teardown.

### Patch Changes

- [#581](https://github.com/phystack/ps-platform/pull/581) [`55c1907`](https://github.com/phystack/ps-platform/commit/55c19073ce5a9386748aca0ed0a48d127a8c5657) Thanks [@zahid-hsn](https://github.com/zahid-hsn)! - Fix WebRTC responder never recovering after the remote peer reloads/reconnects. A responder created via `onDataChannel`/`onMediaStream` was single-use: when the initiator reloaded, its fresh offer was renegotiated onto the dead peer connection (or dropped), so the app's channel/stream stayed bound to a dead session and froze until the process restarted. Recovery is now automatic and transparent — the app keeps the same `PhygridDataChannel`/`PhygridMediaStream` object and its listeners; the library swaps the dead peer connection underneath.

  - `peer-connection-manager.ts`: a fresh offer is recognised as a new session by a changed ICE ufrag (a reloaded peer presents a new ufrag); such an offer on an already-established or dead responder pc triggers a clean pc rebuild that answers the carried offer (never lost in the offer-race window). Duplicate/in-session re-offers (same ufrag) are ignored, which prevents offer/answer glare and the teardown churn a naive "ever connected" check would cause.
  - `data-channel-handler.ts`: added a pc-state-based liveness backstop (data channels have no frame heartbeat) that forces a reconnect when the pc stays non-`connected`/the channel is gone past a 20s deadline — catching the silent-death case where no state-change event fires. Idle data channels are never torn down (state-based, not traffic-based).
  - `media-stream-handler.ts`: `ontrack` now keeps a single stable `remoteStream` across reconnects (adding the new track, dropping the ended one) so an app holding `getStream()` as a `<video>.srcObject` keeps working after recovery.

  - `peripheral-twin.ts`: `updateReported` now defaults the twin type to `Peripheral` when the cached twin response has it unset (the device-local hub can return a reused peripheral twin without `type`), fixing a spurious `unsupported twin type "undefined"` failure when reporting peripheral properties.

  No app changes required; the initiator path and happy paths are unchanged.

- Updated dependencies []:
  - @phystack/socket.io-proxy@6.3.0

## 6.2.2

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@6.2.2

## 6.2.1

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@6.2.1

## 6.2.0

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@6.2.0

## 6.1.2

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@6.1.2

## 6.1.1

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@6.1.1

## 6.1.0

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@6.1.0

## 6.0.0

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@6.0.0

## 5.2.1

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@5.2.1

## 5.2.0

### Minor Changes

- [#285](https://github.com/phystack/ps-platform/pull/285) [`b21582b`](https://github.com/phystack/ps-platform/commit/b21582be38dbf3581c237fc313a9809f9e9044de) Thanks [@hassellof](https://github.com/hassellof)! - Add cloud-app constructor branch to `PhyHubClient`. The same SDK that runs on edge/screen/peripheral apps now also powers cloud apps — only the constructor variant differs:

  ```ts
  const client = new PhyHubClient({
    cloudApp: {
      appId,
      appSecret,
      coreApiUrl, // gateway, e.g. http://localhost:14080
      phyhubUrl, // phyhub, e.g. http://localhost:14400
    },
  });
  ```

  The SDK exchanges `{ appId, appSecret }` for a short-lived JWT via `POST /api/2026-03/cloud-apps/:appId/token`, opens one Socket.IO connection per Gridapp with the JWT as handshake auth, and auto-refreshes the JWT 60 seconds before expiry. The existing device-path constructor (`{ instanceId, moduleName, dataResidency }`) is unchanged — fully backward compatible.

  New types: `CloudTwinDesiredProperties` extended with `appDefinitionId`, `spaceId`, `settings`, `$version`. `CloudTwinLifecycleEvent` for the `{ eventId, $version, twin }` payload phyhub emits to the room.

  Replaces the now-deleted `@phystack/hub-cloud-app` placeholder package — cloud apps use one shared client SDK with the rest of the platform.

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@5.2.0

## 5.1.1

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@5.1.1

## 5.1.0

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@5.1.0

## 5.0.3

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@5.0.3

## 5.0.2

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@5.0.2

## 5.0.1

### Patch Changes

- Updated dependencies []:
  - @phystack/socket.io-proxy@5.0.1
