rnxsim 0.1.513 → 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.
- package/cli/cloud-client.ts +18 -8
- package/cli/commands/inspect/get-layout.ts +2 -1
- package/cli/outbound-endpoints.ts +1 -7
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +64 -1
- package/dist-lib/bridge-contract-input.mjs +63 -1
- package/dist-lib/bridge-contract.cjs +73 -3
- package/dist-lib/bridge-contract.mjs +69 -2
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +117 -22
- package/dist-lib/cloud-contract.mjs +104 -21
- package/dist-lib/cloud.cjs +121 -27
- package/dist-lib/cloud.mjs +121 -27
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +45 -20
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +62 -24
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +62 -24
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite.cjs +1 -1
- package/package.json +2 -1
- package/src/bridge-contract-input.ts +74 -0
- package/src/bridge-contract.ts +103 -0
- package/src/cloud-contract.ts +191 -19
- package/src/cloud.ts +104 -7
package/dist-lib/skills.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
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
|
-
|
|
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
|
-
|
|
27526
|
-
|
|
27527
|
-
"
|
|
27528
|
-
"
|
|
27529
|
-
"
|
|
27530
|
-
"
|
|
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
|
-
"
|
|
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
|
-
|
|
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))
|
|
28274
|
-
|
|
28275
|
-
|
|
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);
|
package/dist-lib/swift.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
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.
|
|
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.
|
|
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')
|
package/src/bridge-contract.ts
CHANGED
|
@@ -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']
|
package/src/cloud-contract.ts
CHANGED
|
@@ -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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
|
|
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
|
|
@@ -58,3 +110,123 @@ export interface RnxCloudBoxCreateReceipt {
|
|
|
58
110
|
}
|
|
59
111
|
simulator: RnxCloudCreateReceipt
|
|
60
112
|
}
|
|
113
|
+
|
|
114
|
+
// what a cloud simulator reports. the stored lifecycle values are
|
|
115
|
+
// `creating`/`open`/`hibernated`/`deleting`; `deleted` is only ever a response,
|
|
116
|
+
// answered by a delete, because a deleted simulator has no row left to read.
|
|
117
|
+
export type RnxCloudSimStatus =
|
|
118
|
+
| 'creating'
|
|
119
|
+
| 'open'
|
|
120
|
+
| 'hibernated'
|
|
121
|
+
| 'deleting'
|
|
122
|
+
| 'deleted'
|
|
123
|
+
|
|
124
|
+
// one simulator as the instance list reports it: the lifecycle view plus the
|
|
125
|
+
// labels it was created with.
|
|
126
|
+
export interface RnxCloudSimView {
|
|
127
|
+
simId: string
|
|
128
|
+
status: RnxCloudSimStatus
|
|
129
|
+
labels: Record<string, string>
|
|
130
|
+
artifact: { sha256: string; bytes: number }
|
|
131
|
+
claim: RnxCloudClaim | null
|
|
132
|
+
createdAt: number
|
|
133
|
+
lastActiveAt: number
|
|
134
|
+
lastHibernatedAt?: number
|
|
135
|
+
storageFrom?: string
|
|
136
|
+
/** the idle window the creator set, or null for the service default. */
|
|
137
|
+
inactivityTimeoutMs: number | null
|
|
138
|
+
/** the instant the creator's hard timeout stops this simulator, or null. */
|
|
139
|
+
hardTimeoutAt: number | null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// one simulator as create and get report it, with the watch URL a browser
|
|
143
|
+
// opens to see it.
|
|
144
|
+
export interface RnxCloudSimInstance extends RnxCloudSimView {
|
|
145
|
+
/** a watch URL with its grant in the fragment, or null when none can be
|
|
146
|
+
* minted yet: an unconfirmed simulator is not watchable. */
|
|
147
|
+
streamUrl: string | null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// the create/reuse answer: the receipt the box path also returns, plus what
|
|
151
|
+
// only the instance API knows.
|
|
152
|
+
export interface RnxCloudSimCreateResponse extends RnxCloudCreateReceipt {
|
|
153
|
+
token: string
|
|
154
|
+
status: RnxCloudSimStatus
|
|
155
|
+
labels: Record<string, string>
|
|
156
|
+
streamUrl: string | null
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// one grammar for the create map and the list selector: `key=value,key2=value2`.
|
|
160
|
+
// a value may be empty; a key may not; a repeated key is the caller's mistake
|
|
161
|
+
// rather than a silent last-one-wins. commas and control characters never
|
|
162
|
+
// belong in a key or value, and 63 characters is the ceiling for either.
|
|
163
|
+
export const RNX_CLOUD_LABELS_MAX_COUNT = 16
|
|
164
|
+
export const RNX_CLOUD_LABEL_MAX_LENGTH = 63
|
|
165
|
+
|
|
166
|
+
const RNX_CLOUD_LABEL_CONTROL = /[\u0000-\u001f\u007f]/
|
|
167
|
+
|
|
168
|
+
export function isValidRnxCloudLabel(key: string, value: string): boolean {
|
|
169
|
+
return (
|
|
170
|
+
key.length > 0 &&
|
|
171
|
+
key.length <= RNX_CLOUD_LABEL_MAX_LENGTH &&
|
|
172
|
+
value.length <= RNX_CLOUD_LABEL_MAX_LENGTH &&
|
|
173
|
+
!key.includes(',') &&
|
|
174
|
+
!key.includes('=') &&
|
|
175
|
+
!value.includes(',') &&
|
|
176
|
+
!RNX_CLOUD_LABEL_CONTROL.test(key) &&
|
|
177
|
+
!RNX_CLOUD_LABEL_CONTROL.test(value)
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** the wire form, or null when it is not one. `''` is the empty map. */
|
|
182
|
+
export function parseRnxCloudLabels(value: string): Record<string, string> | null {
|
|
183
|
+
if (value === '') return {}
|
|
184
|
+
const entries = value.split(',')
|
|
185
|
+
if (entries.length > RNX_CLOUD_LABELS_MAX_COUNT) return null
|
|
186
|
+
const labels: Record<string, string> = {}
|
|
187
|
+
for (const entry of entries) {
|
|
188
|
+
const separator = entry.indexOf('=')
|
|
189
|
+
if (separator < 1) return null
|
|
190
|
+
const key = entry.slice(0, separator).trim()
|
|
191
|
+
const label = entry.slice(separator + 1).trim()
|
|
192
|
+
if (!isValidRnxCloudLabel(key, label)) return null
|
|
193
|
+
if (Object.hasOwn(labels, key)) return null
|
|
194
|
+
labels[key] = label
|
|
195
|
+
}
|
|
196
|
+
return labels
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** the same grammar with the map already split apart, as a json body carries
|
|
200
|
+
* it. anything the wire form would refuse is refused here too. */
|
|
201
|
+
export function readRnxCloudLabels(value: unknown): Record<string, string> | null {
|
|
202
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return null
|
|
203
|
+
const entries = Object.entries(value)
|
|
204
|
+
if (entries.length > RNX_CLOUD_LABELS_MAX_COUNT) return null
|
|
205
|
+
const labels: Record<string, string> = {}
|
|
206
|
+
for (const [key, label] of entries) {
|
|
207
|
+
if (typeof label !== 'string') return null
|
|
208
|
+
if (!isValidRnxCloudLabel(key, label)) return null
|
|
209
|
+
labels[key] = label
|
|
210
|
+
}
|
|
211
|
+
return labels
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** every selector entry must be present with the same value. an empty
|
|
215
|
+
* selector selects everything. */
|
|
216
|
+
export function matchesRnxCloudLabels(
|
|
217
|
+
labels: Record<string, string>,
|
|
218
|
+
selector: Record<string, string>,
|
|
219
|
+
): boolean {
|
|
220
|
+
return Object.entries(selector).every(([key, value]) => labels[key] === value)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** the selector an instance list filters by, given the statuses it accepts. */
|
|
224
|
+
export function isRnxCloudSimStatus(value: string): value is RnxCloudSimStatus {
|
|
225
|
+
return (
|
|
226
|
+
value === 'creating' ||
|
|
227
|
+
value === 'open' ||
|
|
228
|
+
value === 'hibernated' ||
|
|
229
|
+
value === 'deleting' ||
|
|
230
|
+
value === 'deleted'
|
|
231
|
+
)
|
|
232
|
+
}
|