rnxsim 0.1.514 → 0.1.515

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.
Files changed (59) hide show
  1. package/cli/cloud-client.ts +18 -8
  2. package/cli/commands/inspect/get-layout.ts +2 -1
  3. package/dist-lib/agent-daemon-client.cjs +1 -1
  4. package/dist-lib/agent-events.cjs +1 -1
  5. package/dist-lib/agent-identity.cjs +1 -1
  6. package/dist-lib/agent-sessions.cjs +1 -1
  7. package/dist-lib/attached-projects.cjs +1 -1
  8. package/dist-lib/auth/shared-session.cjs +1 -1
  9. package/dist-lib/backend-origin.cjs +1 -1
  10. package/dist-lib/beta.cjs +1 -1
  11. package/dist-lib/beta.mjs +1 -1
  12. package/dist-lib/bridge-constants.cjs +1 -1
  13. package/dist-lib/bridge-contract-input.cjs +64 -1
  14. package/dist-lib/bridge-contract-input.mjs +63 -1
  15. package/dist-lib/bridge-contract.cjs +73 -3
  16. package/dist-lib/bridge-contract.mjs +69 -2
  17. package/dist-lib/capture-contract.cjs +1 -1
  18. package/dist-lib/capture-contract.mjs +1 -1
  19. package/dist-lib/cli-constants.cjs +1 -1
  20. package/dist-lib/cloud-contract.cjs +63 -22
  21. package/dist-lib/cloud-contract.mjs +57 -21
  22. package/dist-lib/cloud.cjs +56 -20
  23. package/dist-lib/cloud.mjs +56 -20
  24. package/dist-lib/config.cjs +1 -1
  25. package/dist-lib/detox/index.cjs +1 -1
  26. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  27. package/dist-lib/home-paths.cjs +1 -1
  28. package/dist-lib/host/bridge-host.cjs +45 -20
  29. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  30. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  31. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  32. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  33. package/dist-lib/host/websocket-proxy.cjs +1 -1
  34. package/dist-lib/index.cjs +62 -24
  35. package/dist-lib/jump-to-source-babel.cjs +1 -1
  36. package/dist-lib/jump-to-source-native.cjs +1 -1
  37. package/dist-lib/menu.cjs +1 -1
  38. package/dist-lib/menu.mjs +1 -1
  39. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  40. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  41. package/dist-lib/metro-production-bundle.cjs +1 -1
  42. package/dist-lib/metro-production-bundle.mjs +1 -1
  43. package/dist-lib/metro.cjs +1 -1
  44. package/dist-lib/profiles.cjs +1 -1
  45. package/dist-lib/public-brand.cjs +1 -1
  46. package/dist-lib/react-native-host-modules.cjs +1 -1
  47. package/dist-lib/react-native-host-modules.mjs +1 -1
  48. package/dist-lib/render-mode.cjs +1 -1
  49. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  50. package/dist-lib/sdk.cjs +1 -1
  51. package/dist-lib/sdk.mjs +1 -1
  52. package/dist-lib/skills.cjs +62 -24
  53. package/dist-lib/swift.cjs +1 -1
  54. package/dist-lib/vite.cjs +1 -1
  55. package/package.json +2 -1
  56. package/src/bridge-contract-input.ts +74 -0
  57. package/src/bridge-contract.ts +103 -0
  58. package/src/cloud-contract.ts +71 -19
  59. package/src/cloud.ts +10 -0
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.514 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.515 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/react-native-host-modules.ts
4
4
  var REACT_NATIVE_PASSTHROUGH_SPECIFIERS = [
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.514 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.515 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.514 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.515 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
package/dist-lib/sdk.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.514 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.515 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
package/dist-lib/sdk.mjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.514 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.515 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  var __defProp = Object.defineProperty;
3
3
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
4
4
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.514 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.515 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -27513,35 +27513,66 @@ var init_metro_fingerprint = __esm({
27513
27513
  });
27514
27514
 
27515
27515
  // src/cloud-contract.ts
27516
+ function scopeTableKeys() {
27517
+ return Object.keys(RNX_CLOUD_COMMAND_SCOPES).filter(
27518
+ (key) => Object.prototype.hasOwnProperty.call(RNX_CLOUD_COMMAND_SCOPES, key)
27519
+ );
27520
+ }
27516
27521
  function isRnxCloudCommandType(value) {
27517
27522
  return RNX_CLOUD_COMMAND_TYPE_SET.has(value);
27518
27523
  }
27519
- var RNX_CLOUD_MAX_ARTIFACT_BYTES, RNX_CLOUD_DESIGN_MAX_STORAGE_BYTES, RNX_CLOUD_COMMAND_TYPES, RNX_CLOUD_COMMAND_TYPE_SET;
27524
+ function rnxCloudCommandScope(type) {
27525
+ return RNX_CLOUD_COMMAND_SCOPES[type];
27526
+ }
27527
+ function rnxCloudScopesAllowCommand(scopes, type) {
27528
+ return scopes.includes(RNX_CLOUD_COMMAND_SCOPES[type]);
27529
+ }
27530
+ var RNX_CLOUD_MAX_ARTIFACT_BYTES, RNX_CLOUD_DESIGN_MAX_STORAGE_BYTES, RNX_CLOUD_COMMAND_SCOPES, RNX_CLOUD_COMMAND_TYPES, RNX_CLOUD_COMMAND_TYPE_SET, RNX_CLOUD_TOKEN_SCOPES, RNX_CLOUD_ALL_COMMAND_SCOPES;
27520
27531
  var init_cloud_contract = __esm({
27521
27532
  "src/cloud-contract.ts"() {
27522
27533
  "use strict";
27523
27534
  RNX_CLOUD_MAX_ARTIFACT_BYTES = 20 * 1024 * 1024;
27524
27535
  RNX_CLOUD_DESIGN_MAX_STORAGE_BYTES = 1024 * 1024;
27525
- RNX_CLOUD_COMMAND_TYPES = Object.freeze([
27526
- "state",
27527
- "query",
27528
- "resolve",
27529
- "perform",
27530
- "waitFor",
27536
+ RNX_CLOUD_COMMAND_SCOPES = {
27537
+ state: "read",
27538
+ tree: "read",
27539
+ query: "read",
27540
+ resolve: "read",
27541
+ screenshot: "read",
27542
+ capture: "read",
27543
+ captureRegions: "read",
27544
+ memory: "read",
27545
+ diagnostics: "read",
27546
+ storageSnapshot: "read",
27547
+ perform: "drive",
27548
+ waitFor: "drive",
27549
+ tap: "drive",
27550
+ longPress: "drive",
27551
+ settle: "drive",
27552
+ openUrl: "drive",
27553
+ setAppearance: "drive",
27554
+ keyboard: "drive",
27555
+ focus: "drive",
27556
+ flowStatus: "drive",
27557
+ reset: "reset",
27558
+ camera: "media",
27559
+ // string evaluation and arbitrary bridge calls are maestro's boundary: the
27560
+ // customer sdk never carries this scope, and the service grants it only to
27561
+ // the account api key caller. close rides along, since ending the page ends
27562
+ // the sim and belongs to the sim's owner rather than any driver.
27563
+ evaluate: "debug",
27564
+ call: "debug",
27565
+ close: "debug"
27566
+ };
27567
+ RNX_CLOUD_COMMAND_TYPES = Object.freeze(scopeTableKeys());
27568
+ RNX_CLOUD_COMMAND_TYPE_SET = new Set(RNX_CLOUD_COMMAND_TYPES);
27569
+ RNX_CLOUD_TOKEN_SCOPES = Object.freeze([
27570
+ "read",
27571
+ "drive",
27531
27572
  "reset",
27532
- "screenshot",
27533
- "capture",
27534
- "captureRegions",
27535
- "settle",
27536
- "setAppearance",
27537
- "openUrl",
27538
- "tree",
27539
- "tap",
27540
- "longPress",
27541
- "memory",
27542
- "diagnostics"
27573
+ "media"
27543
27574
  ]);
27544
- RNX_CLOUD_COMMAND_TYPE_SET = new Set(RNX_CLOUD_COMMAND_TYPES);
27575
+ RNX_CLOUD_ALL_COMMAND_SCOPES = Object.freeze(["read", "drive", "reset", "media", "debug"]);
27545
27576
  }
27546
27577
  });
27547
27578
 
@@ -28270,10 +28301,17 @@ function cloudCommand(command) {
28270
28301
  for (const [key, value] of Object.entries(command)) {
28271
28302
  if (key !== "id" && key !== "simId") clean[key] = value;
28272
28303
  }
28273
- if (isRnxCloudCommandType(command.type)) return clean;
28274
- throw new Error(
28275
- `a remote simulator does not support bridge command type ${command.type}`
28276
- );
28304
+ if (!isRnxCloudCommandType(command.type)) {
28305
+ throw new Error(
28306
+ `a remote simulator does not support bridge command type ${command.type}`
28307
+ );
28308
+ }
28309
+ if (!rnxCloudScopesAllowCommand(RNX_CLOUD_TOKEN_SCOPES, command.type)) {
28310
+ throw new Error(
28311
+ `a remote simulator command ${command.type} requires the ${rnxCloudCommandScope(command.type)} scope`
28312
+ );
28313
+ }
28314
+ return clean;
28277
28315
  }
28278
28316
  function createCloudBridgeForParsed(parsed) {
28279
28317
  const session = resolveCloudSessionForParsed(parsed);
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.514 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.515 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
package/dist-lib/vite.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.514 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.515 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rnxsim",
3
- "version": "0.1.514",
3
+ "version": "0.1.515",
4
4
  "description": "Vite and Metro plugins, testing drivers, and SDK exports for rnx.",
5
5
  "author": "Tamagui LLC",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -92,6 +92,7 @@
92
92
  "./bridge-contract-input": {
93
93
  "types": "./src/bridge-contract-input.ts",
94
94
  "source": "./src/bridge-contract-input.ts",
95
+ "bun": "./src/bridge-contract-input.ts",
95
96
  "import": "./dist-lib/bridge-contract-input.mjs",
96
97
  "default": "./dist-lib/bridge-contract-input.cjs"
97
98
  },
@@ -198,6 +198,46 @@ export function parsePerformStep(value: unknown, index: number): SimPerformStep
198
198
  return { type, key: stringValue(step.key, `${path}.key`) }
199
199
  case 'dismissKeyboard':
200
200
  return { type }
201
+ case 'setElementValue': {
202
+ const submit = optionalBoolean(step, 'submit', path)
203
+ return {
204
+ type,
205
+ id: stringValue(step.id, `${path}.id`),
206
+ text: stringValue(step.text, `${path}.text`),
207
+ ...(submit === undefined ? {} : { submit }),
208
+ }
209
+ }
210
+ case 'toggleKeyboard':
211
+ return { type }
212
+ case 'openUrl': {
213
+ const url = stringValue(step.url, `${path}.url`)
214
+ if (url.length === 0) validationFailure(`${path}.url`, 'must be non-empty')
215
+ return { type, url }
216
+ }
217
+ case 'keyDown':
218
+ case 'keyUp':
219
+ return { type, key: stringValue(step.key, `${path}.key`) }
220
+ case 'setOrientation': {
221
+ const orientation = stringValue(step.orientation, `${path}.orientation`)
222
+ if (orientation !== 'portrait' && orientation !== 'landscape') {
223
+ validationFailure(`${path}.orientation`, 'must be portrait or landscape')
224
+ }
225
+ return { type, orientation }
226
+ }
227
+ case 'deviceInfo':
228
+ return { type }
229
+ case 'buttonDown':
230
+ case 'buttonUp': {
231
+ const button =
232
+ step.button === undefined ? undefined : stringValue(step.button, `${path}.button`)
233
+ return { type, ...(button === undefined ? {} : { button }) }
234
+ }
235
+ case 'incrementElement':
236
+ case 'decrementElement':
237
+ return { type, id: stringValue(step.id, `${path}.id`) }
238
+ case 'discoverStoreKitConfig':
239
+ case 'clearStoreKitConfig':
240
+ return { type }
201
241
  case 'wait':
202
242
  return { type, ms: numberValue(step.ms, `${path}.ms`) }
203
243
  case 'waitFor': {
@@ -233,6 +273,40 @@ export function parsePerformStep(value: unknown, index: number): SimPerformStep
233
273
  }
234
274
  }
235
275
 
276
+ // an unclosed touchDown leaves a pointer stuck down, and the batch abort path
277
+ // would cancel it after the caller was told the batch failed. every batch is
278
+ // self-contained (pointers never survive it), so a touchDown without its
279
+ // touchUp or touchCancel in the same batch is rejected before anything runs.
280
+ export function findUnbalancedTouchDown(
281
+ steps: readonly SimPerformStep[],
282
+ ): { index: number; pointerId: number } | null {
283
+ const open = new Map<number, number>()
284
+ const pointerIdOf = (step: SimPerformStep): number | null => {
285
+ if (
286
+ step.type !== 'touchDown' &&
287
+ step.type !== 'touchMove' &&
288
+ step.type !== 'touchUp' &&
289
+ step.type !== 'touchCancel'
290
+ ) {
291
+ return null
292
+ }
293
+ const pointerId = step.pointerId ?? 999
294
+ return Number.isSafeInteger(pointerId) ? pointerId : null
295
+ }
296
+ for (const [index, step] of steps.entries()) {
297
+ if (step.type === 'touchDown') {
298
+ const pointerId = pointerIdOf(step) ?? 999
299
+ if (!open.has(pointerId)) open.set(pointerId, index)
300
+ } else if (step.type === 'touchUp' || step.type === 'touchCancel') {
301
+ const pointerId = pointerIdOf(step)
302
+ if (pointerId !== null) open.delete(pointerId)
303
+ }
304
+ }
305
+ const next = open.entries().next()
306
+ if (next.done) return null
307
+ return { index: next.value[1], pointerId: next.value[0] }
308
+ }
309
+
236
310
  export function parseStateOptions(value: unknown): SimStateOptions {
237
311
  const input = recordValue(value, 'state')
238
312
  const route = optionalBoolean(input, 'route', 'state')
@@ -163,6 +163,24 @@ export type PerformStep =
163
163
  condition?: 'present' | 'mounted' | 'absent'
164
164
  timeoutMs?: number
165
165
  }
166
+ // limrun parity steps. each maps onto an engine primitive and reports
167
+ // per step; the refused ones below parse but never run.
168
+ | { type: 'setElementValue'; id: string; text: string; submit?: boolean }
169
+ | { type: 'toggleKeyboard' }
170
+ | { type: 'openUrl'; url: string }
171
+ | { type: 'keyDown'; key: string }
172
+ | { type: 'keyUp'; key: string }
173
+ | { type: 'setOrientation'; orientation: 'portrait' | 'landscape' }
174
+ | { type: 'deviceInfo' }
175
+ // refused with a named reason, never emulated: no hardware-button model,
176
+ // no storekit simulation, no stepper primitive, and key/orientation state
177
+ // the engine does not hold.
178
+ | { type: 'buttonDown'; button?: string }
179
+ | { type: 'buttonUp'; button?: string }
180
+ | { type: 'incrementElement'; id: string }
181
+ | { type: 'decrementElement'; id: string }
182
+ | { type: 'discoverStoreKitConfig' }
183
+ | { type: 'clearStoreKitConfig' }
166
184
 
167
185
  export interface PerformOptions {
168
186
  // stop the batch at the first failing step. default true — a gesture whose
@@ -179,6 +197,91 @@ export interface PerformOptions {
179
197
  timeoutMs?: number
180
198
  }
181
199
 
200
+ // observed device identity for the `deviceInfo` step: the live device spec
201
+ // in both runtimes, so every field is measured, never a default.
202
+ export interface PerformDeviceInfo {
203
+ platform: string
204
+ model?: string
205
+ width: number
206
+ height: number
207
+ scale: number
208
+ }
209
+
210
+ // steps that parse but never run. one owner for the refusal vocabulary so
211
+ // the browser page and the cpu runtime refuse with the same named reason and
212
+ // neither plane emulates what the engine cannot do.
213
+ export const REFUSED_PERFORM_STEP_TYPES = [
214
+ 'keyDown',
215
+ 'keyUp',
216
+ 'setOrientation',
217
+ 'buttonDown',
218
+ 'buttonUp',
219
+ 'incrementElement',
220
+ 'decrementElement',
221
+ 'discoverStoreKitConfig',
222
+ 'clearStoreKitConfig',
223
+ ] as const
224
+
225
+ export type RefusedPerformStepType = (typeof REFUSED_PERFORM_STEP_TYPES)[number]
226
+
227
+ export function refusedPerformStepReason(type: RefusedPerformStepType): string {
228
+ switch (type) {
229
+ case 'keyDown':
230
+ case 'keyUp':
231
+ return `${type} is not supported: keys dispatch atomically; use the key step`
232
+ case 'setOrientation':
233
+ return 'setOrientation is not supported: the simulator viewport is fixed at boot'
234
+ case 'buttonDown':
235
+ case 'buttonUp':
236
+ return `${type} is not supported: the simulator has no hardware buttons`
237
+ case 'incrementElement':
238
+ case 'decrementElement':
239
+ return `${type} is not supported: steppers have no engine primitive; drive them with tapId`
240
+ case 'discoverStoreKitConfig':
241
+ case 'clearStoreKitConfig':
242
+ return `${type} is not supported: storekit has no simulator model`
243
+ }
244
+ }
245
+
246
+ // returns the refusal for a refused step, null when the step is runnable.
247
+ // every variant is listed so adding a step forces its classification here;
248
+ // the executors mirror the refused cases in their own switches.
249
+ export function performStepRefusal(type: PerformStep['type']): string | null {
250
+ switch (type) {
251
+ case 'keyDown':
252
+ case 'keyUp':
253
+ case 'setOrientation':
254
+ case 'buttonDown':
255
+ case 'buttonUp':
256
+ case 'incrementElement':
257
+ case 'decrementElement':
258
+ case 'discoverStoreKitConfig':
259
+ case 'clearStoreKitConfig':
260
+ return refusedPerformStepReason(type)
261
+ case 'touchDown':
262
+ case 'touchMove':
263
+ case 'touchUp':
264
+ case 'touchCancel':
265
+ case 'tap':
266
+ case 'tapId':
267
+ case 'doubleTap':
268
+ case 'longPress':
269
+ case 'drag':
270
+ case 'pinch':
271
+ case 'scroll':
272
+ case 'type':
273
+ case 'key':
274
+ case 'dismissKeyboard':
275
+ case 'setElementValue':
276
+ case 'toggleKeyboard':
277
+ case 'openUrl':
278
+ case 'deviceInfo':
279
+ case 'wait':
280
+ case 'waitFor':
281
+ return null
282
+ }
283
+ }
284
+
182
285
  export interface PerformStepResult {
183
286
  index: number
184
287
  type: PerformStep['type']
@@ -6,25 +6,51 @@ export const RNX_CLOUD_DESIGN_MAX_STORAGE_BYTES = 1024 * 1024
6
6
  export const RNX_CLOUD_TEST_FILES_MAX_COUNT = 256
7
7
  export const RNX_CLOUD_TEST_FILES_MAX_BYTES = 500_000
8
8
 
9
- export const RNX_CLOUD_COMMAND_TYPES = Object.freeze([
10
- 'state',
11
- 'query',
12
- 'resolve',
13
- 'perform',
14
- 'waitFor',
15
- 'reset',
16
- 'screenshot',
17
- 'capture',
18
- 'captureRegions',
19
- 'settle',
20
- 'setAppearance',
21
- 'openUrl',
22
- 'tree',
23
- 'tap',
24
- 'longPress',
25
- 'memory',
26
- 'diagnostics',
27
- ]) satisfies readonly BridgeCommandType[]
9
+ export type RnxCloudCommandScope = 'read' | 'drive' | 'reset' | 'media' | 'debug'
10
+
11
+ // one scope table for both remote planes: every bridge verb names the scope
12
+ // that may send it, so the nano service and the box relay admit the same
13
+ // vocabulary and differ only by caller scope. reads observe, drive moves,
14
+ // reset wipes, media injects camera fixtures, and debug evaluates.
15
+ export const RNX_CLOUD_COMMAND_SCOPES = {
16
+ state: 'read',
17
+ tree: 'read',
18
+ query: 'read',
19
+ resolve: 'read',
20
+ screenshot: 'read',
21
+ capture: 'read',
22
+ captureRegions: 'read',
23
+ memory: 'read',
24
+ diagnostics: 'read',
25
+ storageSnapshot: 'read',
26
+ perform: 'drive',
27
+ waitFor: 'drive',
28
+ tap: 'drive',
29
+ longPress: 'drive',
30
+ settle: 'drive',
31
+ openUrl: 'drive',
32
+ setAppearance: 'drive',
33
+ keyboard: 'drive',
34
+ focus: 'drive',
35
+ flowStatus: 'drive',
36
+ reset: 'reset',
37
+ camera: 'media',
38
+ // string evaluation and arbitrary bridge calls are maestro's boundary: the
39
+ // customer sdk never carries this scope, and the service grants it only to
40
+ // the account api key caller. close rides along, since ending the page ends
41
+ // the sim and belongs to the sim's owner rather than any driver.
42
+ evaluate: 'debug',
43
+ call: 'debug',
44
+ close: 'debug',
45
+ } satisfies Record<BridgeCommandType, RnxCloudCommandScope>
46
+
47
+ function scopeTableKeys(): BridgeCommandType[] {
48
+ return Object.keys(RNX_CLOUD_COMMAND_SCOPES).filter((key): key is BridgeCommandType =>
49
+ Object.prototype.hasOwnProperty.call(RNX_CLOUD_COMMAND_SCOPES, key),
50
+ )
51
+ }
52
+
53
+ export const RNX_CLOUD_COMMAND_TYPES = Object.freeze(scopeTableKeys())
28
54
 
29
55
  const RNX_CLOUD_COMMAND_TYPE_SET = new Set<string>(RNX_CLOUD_COMMAND_TYPES)
30
56
 
@@ -34,6 +60,32 @@ export function isRnxCloudCommandType(
34
60
  return RNX_CLOUD_COMMAND_TYPE_SET.has(value)
35
61
  }
36
62
 
63
+ export function rnxCloudCommandScope(type: BridgeCommandType): RnxCloudCommandScope {
64
+ return RNX_CLOUD_COMMAND_SCOPES[type]
65
+ }
66
+
67
+ // what the full per-simulator token and the box token carry until scoped
68
+ // tokens exist: every scope except debug.
69
+ export const RNX_CLOUD_TOKEN_SCOPES: readonly RnxCloudCommandScope[] = Object.freeze([
70
+ 'read',
71
+ 'drive',
72
+ 'reset',
73
+ 'media',
74
+ ])
75
+
76
+ // every scope, granted only to the account api key caller through the account
77
+ // service. the only credential the services accept for it today is the shared
78
+ // service key, which only the account service holds.
79
+ export const RNX_CLOUD_ALL_COMMAND_SCOPES: readonly RnxCloudCommandScope[] =
80
+ Object.freeze(['read', 'drive', 'reset', 'media', 'debug'])
81
+
82
+ export function rnxCloudScopesAllowCommand(
83
+ scopes: readonly RnxCloudCommandScope[],
84
+ type: BridgeCommandType,
85
+ ): boolean {
86
+ return scopes.includes(RNX_CLOUD_COMMAND_SCOPES[type])
87
+ }
88
+
37
89
  export interface RnxCloudClaim {
38
90
  id: string
39
91
  expiresAt: number
package/src/cloud.ts CHANGED
@@ -14,6 +14,9 @@ import {
14
14
  isRnxCloudCommandType,
15
15
  RNX_CLOUD_COMMAND_TYPES,
16
16
  RNX_CLOUD_MAX_ARTIFACT_BYTES,
17
+ RNX_CLOUD_TOKEN_SCOPES,
18
+ rnxCloudCommandScope,
19
+ rnxCloudScopesAllowCommand,
17
20
  type RnxCloudBoxCreateReceipt,
18
21
  type RnxCloudClaim,
19
22
  type RnxCloudCreateReceipt,
@@ -338,6 +341,13 @@ function cloudCommand(command: { type: string; [key: string]: unknown }): Remote
338
341
  if (!isRnxCloudCommandType(command.type)) {
339
342
  throw new Error(`RNX Cloud does not support bridge command type ${command.type}`)
340
343
  }
344
+ // the customer sdk never carries debug: evaluate/call/close belong to the
345
+ // account api key caller. refusing here names the scope before a request.
346
+ if (!rnxCloudScopesAllowCommand(RNX_CLOUD_TOKEN_SCOPES, command.type)) {
347
+ throw new Error(
348
+ `RNX Cloud command ${command.type} requires the ${rnxCloudCommandScope(command.type)} scope`,
349
+ )
350
+ }
341
351
  return { ...clean, type: command.type }
342
352
  }
343
353