rnxsim 0.1.516 → 0.1.518
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/detox/index.ts +56 -31
- 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 +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- 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 +29 -1
- package/dist-lib/cloud-contract.mjs +23 -1
- package/dist-lib/cloud.cjs +114 -16
- package/dist-lib/cloud.mjs +111 -16
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +30 -14
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +1 -1
- 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 +1 -1
- 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 +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite.cjs +1 -1
- package/package.json +1 -1
- package/src/cloud-contract.ts +37 -1
- package/src/cloud.ts +178 -21
package/src/cloud.ts
CHANGED
|
@@ -9,22 +9,39 @@
|
|
|
9
9
|
// public surface.
|
|
10
10
|
|
|
11
11
|
import { isLoopbackHost } from './backend-origin'
|
|
12
|
+
import type { SimStateOptions } from './bridge-contract'
|
|
12
13
|
import { isRnxScreenCapture, type RnxScreenCapture } from './capture-contract'
|
|
13
14
|
import {
|
|
15
|
+
isRnxCloudCommandScope,
|
|
14
16
|
isRnxCloudCommandType,
|
|
15
17
|
RNX_CLOUD_COMMAND_TYPES,
|
|
16
18
|
RNX_CLOUD_MAX_ARTIFACT_BYTES,
|
|
19
|
+
RNX_CLOUD_SCOPED_TOKEN_PREFIX,
|
|
17
20
|
RNX_CLOUD_TOKEN_SCOPES,
|
|
18
21
|
rnxCloudCommandScope,
|
|
19
22
|
rnxCloudScopesAllowCommand,
|
|
20
23
|
type RnxCloudBoxCreateReceipt,
|
|
21
24
|
type RnxCloudClaim,
|
|
25
|
+
type RnxCloudCommandScope,
|
|
22
26
|
type RnxCloudCreateReceipt,
|
|
27
|
+
type RnxCloudScopedToken,
|
|
23
28
|
} from './cloud-contract'
|
|
24
29
|
import { rnxPublicBrand } from './public-brand'
|
|
25
30
|
|
|
26
|
-
export {
|
|
27
|
-
|
|
31
|
+
export {
|
|
32
|
+
isRnxCloudCommandScope,
|
|
33
|
+
isRnxCloudCommandType,
|
|
34
|
+
RNX_CLOUD_COMMAND_TYPES,
|
|
35
|
+
RNX_CLOUD_MAX_ARTIFACT_BYTES,
|
|
36
|
+
RNX_CLOUD_SCOPED_TOKEN_PREFIX,
|
|
37
|
+
}
|
|
38
|
+
export type {
|
|
39
|
+
RnxCloudClaim,
|
|
40
|
+
RnxCloudCommandScope,
|
|
41
|
+
RnxCloudCreateReceipt,
|
|
42
|
+
RnxCloudScopedToken,
|
|
43
|
+
RnxScreenCapture,
|
|
44
|
+
}
|
|
28
45
|
|
|
29
46
|
const DEFAULT_DEVICE = 'iphone-16'
|
|
30
47
|
const DEFAULT_WAIT_READY_MS = 20_000
|
|
@@ -51,6 +68,15 @@ export interface CreateRemoteSimulatorInput {
|
|
|
51
68
|
sha256?: string
|
|
52
69
|
}
|
|
53
70
|
|
|
71
|
+
// attach to a simulator another process created. the token is the full
|
|
72
|
+
// per-simulator token or a scoped `sk_rnx_sim_` token holding the scopes the
|
|
73
|
+
// verbs below need.
|
|
74
|
+
export interface ConnectRemoteSimulatorInput {
|
|
75
|
+
id: string
|
|
76
|
+
token: string
|
|
77
|
+
origin?: string
|
|
78
|
+
}
|
|
79
|
+
|
|
54
80
|
// the simulator-driving verbs. the same vocabulary the CLI drives, one for
|
|
55
81
|
// one: `rnx describe`, `rnx do tap`, `rnx wait selector`.
|
|
56
82
|
export interface RnxCommands {
|
|
@@ -68,16 +94,26 @@ export interface RnxCommands {
|
|
|
68
94
|
screenshot(options?: { layers?: 'full' | 'tenant' | 'shell' }): Promise<string>
|
|
69
95
|
capture(): Promise<RnxScreenCapture>
|
|
70
96
|
reset(): Promise<unknown>
|
|
97
|
+
// the one-call read: screenshot, tree, route, and recent errors together.
|
|
98
|
+
state(options?: SimStateOptions): Promise<unknown>
|
|
71
99
|
}
|
|
72
100
|
|
|
73
101
|
interface RemoteSimulatorSession {
|
|
74
102
|
origin: string
|
|
75
|
-
|
|
76
|
-
|
|
103
|
+
// present on the create path only. a connected simulator never learned its
|
|
104
|
+
// box, so closing one stops the simulator rather than its box.
|
|
105
|
+
boxId: string | null
|
|
106
|
+
boxToken: string | null
|
|
77
107
|
simId: string
|
|
78
108
|
simulatorToken: string
|
|
79
|
-
claim
|
|
109
|
+
// null until the simulator reports one. connect takes no claim and steals
|
|
110
|
+
// none, so an unconfirmed simulator attaches with none.
|
|
111
|
+
claim: RnxCloudClaim | null
|
|
80
112
|
artifact: RnxCloudCreateReceipt['artifact']
|
|
113
|
+
// what this token may send. the create path carries every scope except
|
|
114
|
+
// debug; a connected simulator carries what its token was minted with, and
|
|
115
|
+
// verbs outside it are refused before any request.
|
|
116
|
+
scopes: readonly RnxCloudCommandScope[]
|
|
81
117
|
}
|
|
82
118
|
|
|
83
119
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -333,7 +369,10 @@ function readBoxCreate(
|
|
|
333
369
|
}
|
|
334
370
|
}
|
|
335
371
|
|
|
336
|
-
function cloudCommand(
|
|
372
|
+
function cloudCommand(
|
|
373
|
+
command: { type: string; [key: string]: unknown },
|
|
374
|
+
scopes: readonly RnxCloudCommandScope[],
|
|
375
|
+
): RemoteCommand {
|
|
337
376
|
const clean: Record<string, unknown> = {}
|
|
338
377
|
for (const [key, value] of Object.entries(command)) {
|
|
339
378
|
if (key !== 'id' && key !== 'simId') clean[key] = value
|
|
@@ -341,9 +380,10 @@ function cloudCommand(command: { type: string; [key: string]: unknown }): Remote
|
|
|
341
380
|
if (!isRnxCloudCommandType(command.type)) {
|
|
342
381
|
throw new Error(`RNX Cloud does not support bridge command type ${command.type}`)
|
|
343
382
|
}
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
|
|
383
|
+
// a verb outside this token's scopes never leaves the sdk: the refusal
|
|
384
|
+
// names the scope before a request. the full token carries every scope
|
|
385
|
+
// except debug, so evaluate/call/close stop here for it.
|
|
386
|
+
if (!rnxCloudScopesAllowCommand(scopes, command.type)) {
|
|
347
387
|
throw new Error(
|
|
348
388
|
`RNX Cloud command ${command.type} requires the ${rnxCloudCommandScope(command.type)} scope`,
|
|
349
389
|
)
|
|
@@ -394,6 +434,10 @@ export class RemoteSimulator {
|
|
|
394
434
|
return result
|
|
395
435
|
},
|
|
396
436
|
reset: () => this.sendCommand({ type: 'reset' }),
|
|
437
|
+
state: (options) =>
|
|
438
|
+
this.sendCommand(
|
|
439
|
+
options === undefined ? { type: 'state' } : { type: 'state', stateOptions: options },
|
|
440
|
+
),
|
|
397
441
|
}
|
|
398
442
|
}
|
|
399
443
|
|
|
@@ -401,11 +445,18 @@ export class RemoteSimulator {
|
|
|
401
445
|
return this.session.simId
|
|
402
446
|
}
|
|
403
447
|
|
|
404
|
-
get claim(): RnxCloudClaim {
|
|
448
|
+
get claim(): RnxCloudClaim | null {
|
|
405
449
|
return this.session.claim
|
|
406
450
|
}
|
|
407
451
|
|
|
452
|
+
get scopes(): readonly RnxCloudCommandScope[] {
|
|
453
|
+
return this.session.scopes
|
|
454
|
+
}
|
|
455
|
+
|
|
408
456
|
get receipt(): RnxCloudCreateReceipt {
|
|
457
|
+
if (!this.session.claim) {
|
|
458
|
+
throw new Error('this simulator was attached with connect() and holds no claim')
|
|
459
|
+
}
|
|
409
460
|
return {
|
|
410
461
|
simId: this.session.simId,
|
|
411
462
|
artifact: this.session.artifact,
|
|
@@ -419,8 +470,10 @@ export class RemoteSimulator {
|
|
|
419
470
|
): Promise<unknown> {
|
|
420
471
|
const body = {
|
|
421
472
|
id: globalThis.crypto.randomUUID(),
|
|
422
|
-
|
|
423
|
-
|
|
473
|
+
// a scoped token drives without a claim: the service checks the token
|
|
474
|
+
// instead. a full token still joins the claim it names here.
|
|
475
|
+
claimId: this.session.claim?.id ?? '',
|
|
476
|
+
command: cloudCommand(command, this.session.scopes),
|
|
424
477
|
}
|
|
425
478
|
let response: Response
|
|
426
479
|
try {
|
|
@@ -462,6 +515,10 @@ export class RemoteSimulator {
|
|
|
462
515
|
}
|
|
463
516
|
|
|
464
517
|
async confirm(): Promise<RnxCloudClaim> {
|
|
518
|
+
if (!this.session.claim) {
|
|
519
|
+
throw new Error('this simulator was attached with connect() and holds no claim')
|
|
520
|
+
}
|
|
521
|
+
const claimId = this.session.claim.id
|
|
465
522
|
const response = await fetchWithCloudRetry(
|
|
466
523
|
`${this.session.origin}/v1/sims/${encodeURIComponent(this.session.simId)}/claim`,
|
|
467
524
|
{
|
|
@@ -470,7 +527,7 @@ export class RemoteSimulator {
|
|
|
470
527
|
authorization: `Bearer ${this.session.simulatorToken}`,
|
|
471
528
|
'content-type': 'application/json',
|
|
472
529
|
},
|
|
473
|
-
body: JSON.stringify({ claimId
|
|
530
|
+
body: JSON.stringify({ claimId }),
|
|
474
531
|
},
|
|
475
532
|
)
|
|
476
533
|
const text = await response.text()
|
|
@@ -482,7 +539,7 @@ export class RemoteSimulator {
|
|
|
482
539
|
throw new Error('RNX Cloud claim returned unreadable JSON')
|
|
483
540
|
}
|
|
484
541
|
const claim = readClaim(isRecord(parsed) ? parsed.claim : null)
|
|
485
|
-
if (claim.id !==
|
|
542
|
+
if (claim.id !== claimId) {
|
|
486
543
|
throw new Error('RNX Cloud confirmation replaced the initial claim')
|
|
487
544
|
}
|
|
488
545
|
this.session = { ...this.session, claim }
|
|
@@ -490,13 +547,24 @@ export class RemoteSimulator {
|
|
|
490
547
|
}
|
|
491
548
|
|
|
492
549
|
async close(): Promise<void> {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
550
|
+
// the create path tears down the box it made. a connected simulator never
|
|
551
|
+
// learned its box, so it stops its own simulator instead; a scoped token
|
|
552
|
+
// is refused there, which is the service telling the holder it may drive
|
|
553
|
+
// but never delete.
|
|
554
|
+
const target =
|
|
555
|
+
this.session.boxId !== null && this.session.boxToken !== null
|
|
556
|
+
? {
|
|
557
|
+
url: `${this.session.origin}/v1/boxes/${encodeURIComponent(this.session.boxId)}`,
|
|
558
|
+
authorization: `Bearer ${this.session.boxToken}`,
|
|
559
|
+
}
|
|
560
|
+
: {
|
|
561
|
+
url: `${this.session.origin}/v1/sims/${encodeURIComponent(this.session.simId)}`,
|
|
562
|
+
authorization: `Bearer ${this.session.simulatorToken}`,
|
|
563
|
+
}
|
|
564
|
+
const response = await fetchWithCloudRetry(target.url, {
|
|
565
|
+
method: 'DELETE',
|
|
566
|
+
headers: { authorization: target.authorization },
|
|
567
|
+
})
|
|
500
568
|
const text = await response.text()
|
|
501
569
|
if (!response.ok && response.status !== 404) {
|
|
502
570
|
throw responseError(response.status, text)
|
|
@@ -560,7 +628,96 @@ export async function createRemoteSimulator(
|
|
|
560
628
|
simulatorToken: created.simulatorToken,
|
|
561
629
|
claim: created.receipt.simulator.claim,
|
|
562
630
|
artifact: created.receipt.artifact,
|
|
631
|
+
scopes: RNX_CLOUD_TOKEN_SCOPES,
|
|
563
632
|
})
|
|
564
633
|
await simulator.confirm()
|
|
565
634
|
return simulator
|
|
566
635
|
}
|
|
636
|
+
|
|
637
|
+
function readSimArtifact(value: unknown): RnxCloudCreateReceipt['artifact'] {
|
|
638
|
+
if (
|
|
639
|
+
!isRecord(value) ||
|
|
640
|
+
typeof value.sha256 !== 'string' ||
|
|
641
|
+
!/^[a-f0-9]{64}$/.test(value.sha256) ||
|
|
642
|
+
typeof value.bytes !== 'number' ||
|
|
643
|
+
!Number.isInteger(value.bytes) ||
|
|
644
|
+
value.bytes <= 0
|
|
645
|
+
) {
|
|
646
|
+
throw new Error('RNX Cloud returned an invalid simulator artifact')
|
|
647
|
+
}
|
|
648
|
+
return { id: `sha256:${value.sha256}`, bytes: value.bytes }
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function readConnectClaim(value: unknown): RnxCloudClaim | null {
|
|
652
|
+
if (value === null) return null
|
|
653
|
+
return readClaim(value)
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function readConnectScopes(value: unknown, token: string): readonly RnxCloudCommandScope[] {
|
|
657
|
+
// the full per-simulator token carries every scope except debug, which the
|
|
658
|
+
// service already guarantees. a scoped token learns its verbs from the get
|
|
659
|
+
// it just made, so a verb outside them is refused before any request.
|
|
660
|
+
if (!token.startsWith(RNX_CLOUD_SCOPED_TOKEN_PREFIX)) return RNX_CLOUD_TOKEN_SCOPES
|
|
661
|
+
if (
|
|
662
|
+
!isRecord(value) ||
|
|
663
|
+
typeof value.id !== 'string' ||
|
|
664
|
+
!value.id ||
|
|
665
|
+
!Array.isArray(value.scopes) ||
|
|
666
|
+
value.scopes.length === 0 ||
|
|
667
|
+
!value.scopes.every(
|
|
668
|
+
(scope): scope is RnxCloudCommandScope =>
|
|
669
|
+
typeof scope === 'string' && isRnxCloudCommandScope(scope),
|
|
670
|
+
) ||
|
|
671
|
+
typeof value.expiresAt !== 'number'
|
|
672
|
+
) {
|
|
673
|
+
throw new Error('RNX Cloud returned an invalid scoped token')
|
|
674
|
+
}
|
|
675
|
+
return Object.freeze([...value.scopes])
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// attach to a simulator another process created. one get proves the token,
|
|
679
|
+
// and the simulator it returns drives with that token's scopes, takes no
|
|
680
|
+
// claim, and never steals one.
|
|
681
|
+
export async function connect(input: ConnectRemoteSimulatorInput): Promise<RemoteSimulator> {
|
|
682
|
+
if (!input.id) throw new Error('RNX Cloud connect needs a simulator id')
|
|
683
|
+
if (!input.token) throw new Error('RNX Cloud connect needs a token')
|
|
684
|
+
const origin = resolveOrigin(input.origin)
|
|
685
|
+
let response: Response
|
|
686
|
+
try {
|
|
687
|
+
response = await fetchWithCloudRetry(
|
|
688
|
+
`${origin}/v1/sims/${encodeURIComponent(input.id)}`,
|
|
689
|
+
{ headers: { authorization: `Bearer ${input.token}` } },
|
|
690
|
+
)
|
|
691
|
+
} catch (error) {
|
|
692
|
+
throw new Error(
|
|
693
|
+
`could not reach RNX Cloud at ${origin}: ${
|
|
694
|
+
error instanceof Error ? error.message : String(error)
|
|
695
|
+
}`,
|
|
696
|
+
)
|
|
697
|
+
}
|
|
698
|
+
const text = await response.text()
|
|
699
|
+
if (!response.ok) throw responseError(response.status, text)
|
|
700
|
+
let parsed: unknown
|
|
701
|
+
try {
|
|
702
|
+
parsed = JSON.parse(text)
|
|
703
|
+
} catch {
|
|
704
|
+
throw new Error('RNX Cloud connect returned unreadable JSON')
|
|
705
|
+
}
|
|
706
|
+
if (!isRecord(parsed) || !isRecord(parsed.sim)) {
|
|
707
|
+
throw new Error('RNX Cloud connect returned an invalid simulator')
|
|
708
|
+
}
|
|
709
|
+
const sim = parsed.sim
|
|
710
|
+
if (typeof sim.simId !== 'string' || !sim.simId) {
|
|
711
|
+
throw new Error('RNX Cloud connect returned an invalid simulator')
|
|
712
|
+
}
|
|
713
|
+
return new RemoteSimulator({
|
|
714
|
+
origin,
|
|
715
|
+
boxId: null,
|
|
716
|
+
boxToken: null,
|
|
717
|
+
simId: sim.simId,
|
|
718
|
+
simulatorToken: input.token,
|
|
719
|
+
claim: readConnectClaim(sim.claim ?? null),
|
|
720
|
+
artifact: readSimArtifact(sim.artifact),
|
|
721
|
+
scopes: readConnectScopes(isRecord(parsed) ? parsed.token : null, input.token),
|
|
722
|
+
})
|
|
723
|
+
}
|