Roku Cloud Emulator support - #331
Conversation
Route device-info and key input to Roku Cloud Emulator instances, and add a client for the RCE management API. - RceDevice: sends ECP2 requests over the instance auth-proxy WebSocket (device-info query and key input) and resolves a device addressed by instanceUrl, id, or esn to its live instance URL - RceManagementClient: typed client for the RCE management API (devices, lifecycle, snapshots, firmware, logs, user) - getDeviceInfo and sendKeyEvent use the RCE path when the target is an RCE device - Add ws and @types/ws
- Make RokuDeploy.defaults public and add stagingDirName, username, and files - Add getStagingDir, getOutputZipPath, and getOutputPkgPath, and use them inside stage, zip, sideload, and createSignedPackage so consumers resolve the same paths the library does - Make RekeyDeviceOptions.devId optional to match rekeyDevice behavior - Replace the unused GetOutputZipFilePathOptions with the new resolver option interfaces
# Conflicts: # src/RokuDeploy.ts
…odules The device runs endpoint returns creator, ended_at, runtime, snapshot_name, firmware, and max_runtime fields the old type omitted. Also export DeviceConfig, RceDevice, and RceManagementClient from the package index.
POSTs the bodyless developer-settings-combo instance-api endpoint, which enters the dev-settings key combo on the device so the user can complete the on-screen developer setup wizard.
Negotiates a WebRTC stream from a running RCE instance's Janus gateway over the standard janus-protocol WebSocket: session create, streaming plugin attach, watch with the mountpoint id and pin, then the SDP offer out and the consumer's answer and ICE candidates back in, with keepalives and a negotiation timeout. The management api token rides the WebSocket handshake as a bearer header and the instance janus_token is sent as the apisecret field on every request. Carries no WebRTC dependency; consumers bring their own peer connection.
…d Cloud Emulator devices
… Emulator devices
…d wrappers The instance /ecp1 proxy (now working as intended) lets Cloud Emulator devices share the exact HTTP ECP transport local devices use, so ecp(device, route) owns the base-url swap and always returns the XML body parsed to JSON. Dedicated queryRegistry/queryAppState/queryRendezvous/setRendezvousTracking wrappers refine the common commands; ecp() remains the escape hatch for routes without one. Device-reported failure text (registry not keyed, limited-mode refusals) rides in the typed errors.
Nothing routes ECP over the ECP2 websocket anymore; the instance's /ecp1 proxy carries it as plain HTTP through RokuDeploy.ecp(). RceDevice keeps the instance-url resolution and instance-api actions like the developer-settings combo. Also adds 'guide' to RokuKey (the wake sequence's first key).
Limited ECP mode 403s every keypress on the /ecp1 proxy (with an empty body), but the auth-proxy WebSocket completes the ECP2 authentication challenge and keeps full key input working - the same trust level the mobile app gets. sendKeySequence presses keys in order over that socket (restoring the per-request socket pattern removed with the ECP2 stack, as a private primitive), pausing keyDelayMs between presses so on-screen navigation keeps up, and throws on the first non-2xx response with the failing key and step.
…moteKey vocabulary An RCE device's key input now tries the instance-api key route (/api/v0/ecp1/<action>/<key>) first, which keeps working while the device is in limited ECP mode - the raw /ecp1 proxy 403s every key press there. Any failure (non-2xx, or a future rename of that path) falls back to the raw proxy, and LAN devices are unaffected. Keys are exposed as a RemoteKey enum in canonical device casing, with a RemoteKeyText = keyof typeof RemoteKey type enforcing the canonical names on the public keyPress/keyUp/keyDown methods. This replaces the old lowercase RokuKey type (removed).
…pi's ports routes Instance API v0.3.0 replaced the named telnet WebSocket routes with /api/v0/ports/<port>, so the channel option (and its per-channel port defaults) is gone; callers pass the port directly and RCE devices reach it at that route. The endpoints are binary-frame-only now, which is what this socket always sent.
Instance API v0.3.0 removed the ecp2 websocket endpoints, so key sequences now POST /api/v0/ecp1/keypress/<key> per key (the same route sendKeyEvent uses), which keeps working in limited ECP mode. Removes the ECP2 auth-proxy socket stack and the createWebSocket test seam that existed only for it.
ECP commands are small and fast, so they no longer share the 150s installer timeout; ecp() and the RCE instance-api key route default to RokuDeploy.defaults.ecpTimeout (10s, still overridable per call), which also keeps the key route's fall-back-to-proxy behavior from stacking two long waits against an unreachable device. The fallback now logs at warn, and EcpResult.status is typed as possibly undefined with assertEcpStatusOk guarding that case.
waitForConsoleOutput still passed the pre-rename channel option; the factory takes a plain port number now.
sendRequest now rejects cleanly when the signaling socket is gone instead of failing with a TypeError that left an orphaned pending request, and startKeepalive clears any existing timer first.
The ECP wrappers built their error details with extractHttpDetails(undefined, body), which always returns undefined, so FailedDeviceResponseError/UnparsableDeviceResponseError/ InvalidDeviceResponseCodeError from those paths never carried httpDetails. A new buildEcpHttpDetails helper carries what ecp() retains (status code and body). getDeviceInfo also handles a missing device-info root explicitly instead of relying on the TypeError from spreading undefined, so that error carries no bogus cause. Surfaced by v4's new coverage tests after the merge.
…ss_token query param
| }); | ||
| } | ||
|
|
||
| private buildQueryString(query?: Record<string, string | number | undefined>): string { |
There was a problem hiding this comment.
Likely could be a util. Should check and see if we have one already and if not move it there. Also should verify if still needed after the change to using auth headers.
| max_runtime: number; | ||
| } | ||
|
|
||
| export interface DeviceOut { |
There was a problem hiding this comment.
Something about these interface names bother me. Part of it might be using out rather then response or something like that.
I want to revisit these with @TwitchBronBron
| creator_id: string; | ||
| created_at: string; | ||
| started_at?: string | null; | ||
| snapshot_id: number; |
There was a problem hiding this comment.
@TwitchBronBron in general what are your thoughts about the property names? Was almost thinking about making a small utility that runs every prop through a rename to camel case. Maybe overkill be let me know.
| /** | ||
| * Press a sequence of remote keys, in order, through the instance-api key route | ||
| * (`/api/v0/ecp1/keypress/<key>`). Keys must use the canonical `RemoteKey` casing ('Home', 'Up', | ||
| * 'Select', ...); the route rejects a wrong-case key with a 422. Each press waits for the | ||
| * previous press's response plus a small delay (keyDelayMs) so on-screen navigation keeps up; | ||
| * the first failed press throws with the failing key and step. | ||
| * | ||
| * This deliberately does NOT go through the raw `/ecp1` proxy RokuDeploy's `ecp()` transport | ||
| * uses: that proxy forwards unauthenticated ECP, which a device in limited ECP mode rejects with | ||
| * an empty 403. The instance-api key route carries the instance's own authentication, so key | ||
| * input works regardless of the device's ECP mode - which is what makes key macros like the | ||
| * disable-limited-ECP sequence possible in the first place. | ||
| */ |
There was a problem hiding this comment.
Some comments are a bit verbose and in particular some doc blocks are digging deeper into the underlying implementation then the consumer needs to care about. Want to do a pass on these.
| * The default timeout for ECP requests, in milliseconds. ECP commands are small and fast, | ||
| * so this is much lower than `timeout` (which must accommodate large installer uploads). | ||
| */ | ||
| ecpTimeout: 10000, |
There was a problem hiding this comment.
@TwitchBronBron added a separate timeout for ECP as I think the universal default we had is a little nuts for ECP calls. Let me know if you agree that they can be shorter then sideloading for example and if your happy with the 10s I set.
| export type RemoteKeyText = keyof typeof RemoteKey; | ||
|
|
||
| export interface SendKeyEventOptions extends BaseEcpOptions { | ||
| interface SendKeyEventOptions extends BaseEcpOptions { |
There was a problem hiding this comment.
Should we have removed the export here?
RokuDeploy's constructor options now accept an rceToken used as the default for any RCE device whose config carries no token of its own. RceDevice is gone: RceManagementClient.getInstanceUrl() resolves any RCE device config (instanceUrl directly, id/esn through the management api, with string config ids converted at the boundary now that DeviceId is a number), RokuDeploy memoizes resolved instance urls per device+token, and the two orphaned device actions move onto RokuDeploy as public methods: sendKeySequence (device-kind-agnostic, each press rides the keyPress transport) and sendDeveloperSettingsCombo (RCE-only, sends X-Authorization like the rest of the instance-api calls). TelnetSocket is renamed to RokuDeploySocket (createRokuDeploySocket, LocalSocket, RceSocket, SocketOptions) and resolves instance urls through the management client.
PR #399 review: callers pass the whole device config in; a local device comes back with its host resolved through DNS (some Rokus reject ECP requests addressed by hostname), any other device kind passes through unchanged.
… overrides
Every method takes a single options object now, and each accepts an
optional token that overrides the constructor token for that one call,
so one client can serve multi-account flows. Payloads keep their api
shape nested (createDevice({device}), startDevice({deviceId, start}),
updateSnapshot({deviceId, snapshotId, update}), ...) and send()
resolves the per-call token ?? the constructor token.
getInstanceUrl({device, token?}) threads the override through its
nested findDeviceByEsn/getRunningInstanceApiUrl calls.
Adds Roku Cloud Emulator (RCE) device support to roku-deploy, backing the extension's RCE integration (rokucommunity/vscode-brightscript-language#861).
Stacked on #323 (unified device option); also carries #330's commit until it lands.
RceManagementClient: typed client for the RCE management API (devices, lifecycle, snapshots, firmware, logs, user), withDeviceRunmatching the full device-history response shape. Every call takes an options object accepting an optional per-calltokenthat overrides the constructor token (one client can serve multi-account callers), andgetInstanceUrlresolves a device config addressed by instanceUrl, id, or esn to its live instance URLRokuDeploy(replacing the earlierRceDeviceclass): a constructor-level defaultrceTokenfor RCE device configs that carry none, memoized instance-url resolution per device+token,resolveDns(device)(resolves a local device config's host to its ip; other device kinds pass through unchanged),sendKeySequence(presses remote keys in order through the same transport askeyPress, so key macros work on local and cloud devices alike), andsendDeveloperSettingsCombo(the RCE instance-api developer-settings action)createRokuDeploySocket({ device, port }): a near-transparentnet.Socketreplacement for a device's tcp ports (the BrightScript console, the SceneGraph debug server, the debug protocol's control and io ports); a local device connects plain tcp, and an RCE device reaches the same port over the instance api's/api/v0/ports/<port>websocket routes (Instance API v0.3.0; binary frames both ways, byte parity with a raw tcp socket)DeviceConfig,RceManagementClient,RceVideoSignalingClient, andcreateRokuDeploySocketfrom the package indexkeyPress/keyUp/keyDown/sendText) prefers the RCE instance-api key route (/api/v0/ecp1/<action>/<key>), which keeps working while the device is in limited ECP mode (the rawecp1proxy 403s key presses there) and falls back to the raw proxy otherwise (live-verified in limited mode). Keys are exposed as aRemoteKeyenum in canonical device casing, with aRemoteKeyTexttype enforcing the canonical names on the public key methods; this replaces the old lowercaseRokuKeytypeecp(device, route): the single public ECP transport, addressing local devices on the HTTP ECP port and RCE devices through the instance'secp1proxy, always returning the XML body parsed to JSON. Dedicated wrappers (getDeviceInfo, key presses,launchApp,exitApp,queryApps,queryActiveApp,queryRegistry,queryAppState,queryRendezvous,setRendezvousTracking) all funnel through it, and device-reported failure text (not keyed, limited-mode refusals) rides in the typed errors. ECP requests default to a 10sRokuDeploy.defaults.ecpTimeout(overridable per call), separate from the 150s installer timeout. Live-verified against a cloud device, including in limited ECP mode