peachsim 0.1.527
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/LICENSE +22 -0
- package/README.md +640 -0
- package/cli/app-config.ts +82 -0
- package/cli/app-fonts.ts +388 -0
- package/cli/app-project.ts +59 -0
- package/cli/app-splash.ts +171 -0
- package/cli/app-state-reset.ts +26 -0
- package/cli/auth.ts +184 -0
- package/cli/bin.ts +59 -0
- package/cli/bridge-diagnostics.ts +226 -0
- package/cli/bridge-flow-runner.ts +2806 -0
- package/cli/browser-backend.ts +44 -0
- package/cli/browser-evals.ts +96 -0
- package/cli/cli-update.ts +261 -0
- package/cli/cloud-client.ts +1530 -0
- package/cli/cloud-dispatch.ts +206 -0
- package/cli/cloud-session.ts +227 -0
- package/cli/command-contract.ts +221 -0
- package/cli/command-registry.ts +304 -0
- package/cli/commands/agent-wrapper.ts +1048 -0
- package/cli/commands/agent.ts +439 -0
- package/cli/commands/app-fonts.ts +99 -0
- package/cli/commands/assert.ts +542 -0
- package/cli/commands/auth.ts +71 -0
- package/cli/commands/box/bundle-plane.ts +71 -0
- package/cli/commands/box/checkout-plane.ts +256 -0
- package/cli/commands/box/plane-share.ts +208 -0
- package/cli/commands/box/rnx-command.ts +60 -0
- package/cli/commands/box/search-command.ts +157 -0
- package/cli/commands/box.ts +925 -0
- package/cli/commands/camera.ts +266 -0
- package/cli/commands/cleanup.ts +169 -0
- package/cli/commands/compat.ts +176 -0
- package/cli/commands/config.ts +34 -0
- package/cli/commands/control.ts +2154 -0
- package/cli/commands/cpu-profile.ts +941 -0
- package/cli/commands/daemon-mac-app.ts +165 -0
- package/cli/commands/daemon.ts +800 -0
- package/cli/commands/debug.ts +751 -0
- package/cli/commands/desktop.ts +39 -0
- package/cli/commands/detect.ts +201 -0
- package/cli/commands/detox.ts +481 -0
- package/cli/commands/device.ts +134 -0
- package/cli/commands/diagnose.ts +589 -0
- package/cli/commands/do-chain.ts +408 -0
- package/cli/commands/electron.ts +94 -0
- package/cli/commands/film.ts +379 -0
- package/cli/commands/flow.ts +1240 -0
- package/cli/commands/inspect/actions.ts +516 -0
- package/cli/commands/inspect/core.ts +2086 -0
- package/cli/commands/inspect/count.ts +17 -0
- package/cli/commands/inspect/describe.ts +119 -0
- package/cli/commands/inspect/env.ts +25 -0
- package/cli/commands/inspect/find.ts +169 -0
- package/cli/commands/inspect/get-layout.ts +53 -0
- package/cli/commands/inspect/keyboard.ts +54 -0
- package/cli/commands/inspect/list.ts +175 -0
- package/cli/commands/inspect/memory.ts +215 -0
- package/cli/commands/inspect/redaction.ts +39 -0
- package/cli/commands/inspect/resolve-target.ts +77 -0
- package/cli/commands/inspect/screens.ts +78 -0
- package/cli/commands/inspect/settle.ts +32 -0
- package/cli/commands/inspect/settling.ts +197 -0
- package/cli/commands/inspect/shared.ts +259 -0
- package/cli/commands/inspect/sleep.ts +15 -0
- package/cli/commands/inspect/tree.ts +23 -0
- package/cli/commands/inspect/url.ts +17 -0
- package/cli/commands/inspect/wait-event.ts +211 -0
- package/cli/commands/inspect/wait-idle.ts +25 -0
- package/cli/commands/inspect/wait-ready.ts +87 -0
- package/cli/commands/inspect/wait-selector.ts +59 -0
- package/cli/commands/inspect/wait.ts +32 -0
- package/cli/commands/inspect.ts +4738 -0
- package/cli/commands/install-cli.ts +275 -0
- package/cli/commands/install-desktop.ts +385 -0
- package/cli/commands/login.ts +357 -0
- package/cli/commands/logout.ts +24 -0
- package/cli/commands/maestro-generate.ts +375 -0
- package/cli/commands/maestro.ts +536 -0
- package/cli/commands/mode.ts +58 -0
- package/cli/commands/no-bridge-hint.ts +80 -0
- package/cli/commands/perf.ts +67 -0
- package/cli/commands/permissions.ts +203 -0
- package/cli/commands/platform.ts +410 -0
- package/cli/commands/preview.ts +103 -0
- package/cli/commands/profile.ts +106 -0
- package/cli/commands/react.ts +353 -0
- package/cli/commands/record.ts +1435 -0
- package/cli/commands/report-issue.ts +305 -0
- package/cli/commands/reset.ts +89 -0
- package/cli/commands/runtime.ts +348 -0
- package/cli/commands/screenshot-command.ts +106 -0
- package/cli/commands/screenshot-layers.ts +144 -0
- package/cli/commands/screenshot-mode.ts +38 -0
- package/cli/commands/screenshot.ts +492 -0
- package/cli/commands/screenshots-capture.ts +608 -0
- package/cli/commands/screenshots.ts +127 -0
- package/cli/commands/serve.ts +139 -0
- package/cli/commands/setup.ts +416 -0
- package/cli/commands/shell-boolean-mode.ts +82 -0
- package/cli/commands/skills.ts +468 -0
- package/cli/commands/slides.ts +357 -0
- package/cli/commands/state.ts +94 -0
- package/cli/commands/storage.ts +58 -0
- package/cli/commands/telemetry.ts +55 -0
- package/cli/commands/test.ts +535 -0
- package/cli/commands/three-mode.ts +771 -0
- package/cli/commands/timeline.ts +121 -0
- package/cli/commands/upgrade.ts +167 -0
- package/cli/commands/upload.ts +1203 -0
- package/cli/commands/version.ts +63 -0
- package/cli/commands/welcome.ts +17 -0
- package/cli/commands/what-happened.ts +328 -0
- package/cli/current-sim.ts +217 -0
- package/cli/desktop-companion.ts +315 -0
- package/cli/drivers/electron.ts +70 -0
- package/cli/drivers/index.ts +20 -0
- package/cli/drivers/playwright-provisioning.ts +184 -0
- package/cli/drivers/playwright-sim-host.ts +537 -0
- package/cli/drivers/playwright.ts +209 -0
- package/cli/drivers/registry.ts +65 -0
- package/cli/drivers/types.ts +102 -0
- package/cli/flow-export.ts +55 -0
- package/cli/flow-file.ts +142 -0
- package/cli/flow-live-status.ts +126 -0
- package/cli/flow-session.ts +140 -0
- package/cli/help-core.ts +143 -0
- package/cli/help.ts +44 -0
- package/cli/hidden-runtime-alias.ts +19 -0
- package/cli/hints.ts +214 -0
- package/cli/inspect-notice-state.ts +114 -0
- package/cli/internal-child.ts +176 -0
- package/cli/maestro-js.ts +323 -0
- package/cli/main.ts +704 -0
- package/cli/open-url.ts +8 -0
- package/cli/outbound-endpoints.ts +830 -0
- package/cli/parent-pid.ts +204 -0
- package/cli/parse-args.ts +189 -0
- package/cli/privacy.ts +53 -0
- package/cli/prompt.ts +56 -0
- package/cli/recording-access.ts +108 -0
- package/cli/registry.ts +2 -0
- package/cli/resolve-assets.ts +63 -0
- package/cli/rnx-in-process.ts +98 -0
- package/cli/run-registry.ts +226 -0
- package/cli/run-rnx-core.ts +148 -0
- package/cli/run-rnx.ts +137 -0
- package/cli/runtime-notes.ts +66 -0
- package/cli/runtime-summary.ts +25 -0
- package/cli/secure-file.ts +31 -0
- package/cli/self-invocation.ts +58 -0
- package/cli/send-to-box.ts +109 -0
- package/cli/setup-repository.ts +191 -0
- package/cli/shell-init.ts +98 -0
- package/cli/standalone.ts +4 -0
- package/cli/telemetry.ts +84 -0
- package/cli/ui.ts +68 -0
- package/cli/ws-bridge.ts +1020 -0
- package/detox/attempt-events.cjs +23 -0
- package/detox/colors.ts +54 -0
- package/detox/config-loader.ts +135 -0
- package/detox/element-types.ts +48 -0
- package/detox/expectations.ts +534 -0
- package/detox/gestures.ts +501 -0
- package/detox/index.ts +2694 -0
- package/detox/jest-preset.cjs +29 -0
- package/detox/jest-setup-after-env.cjs +22 -0
- package/detox/matchers.ts +30 -0
- package/detox/motion-change.cjs +109 -0
- package/detox/motion-change.d.cts +35 -0
- package/detox/navigation-lifecycle.ts +290 -0
- package/detox/navigation.ts +43 -0
- package/detox/proof-frame.cjs +114 -0
- package/detox/record-block-execution.cjs +78 -0
- package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
- package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
- package/detox/screenshots/color-test-rest-norngh.png +0 -0
- package/detox/screenshots/theme-blue-toggle.png +0 -0
- package/detox/screenshots/theme-blue.png +0 -0
- package/detox/screenshots/theme-red-toggle.png +0 -0
- package/detox/screenshots/theme-red.png +0 -0
- package/dist-lib/agent-daemon-client.cjs +502 -0
- package/dist-lib/agent-events.cjs +48 -0
- package/dist-lib/agent-identity.cjs +53 -0
- package/dist-lib/agent-sessions.cjs +759 -0
- package/dist-lib/attached-projects.cjs +526 -0
- package/dist-lib/auth/shared-session.cjs +208 -0
- package/dist-lib/backend-origin.cjs +88 -0
- package/dist-lib/beta.cjs +41 -0
- package/dist-lib/beta.mjs +13 -0
- package/dist-lib/bridge-constants.cjs +161 -0
- package/dist-lib/bridge-contract-input.cjs +451 -0
- package/dist-lib/bridge-contract-input.mjs +414 -0
- package/dist-lib/bridge-contract.cjs +130 -0
- package/dist-lib/bridge-contract.mjs +94 -0
- package/dist-lib/capture-contract.cjs +220 -0
- package/dist-lib/capture-contract.mjs +190 -0
- package/dist-lib/cli-constants.cjs +32 -0
- package/dist-lib/cloud-contract.cjs +194 -0
- package/dist-lib/cloud-contract.mjs +145 -0
- package/dist-lib/cloud.cjs +670 -0
- package/dist-lib/cloud.mjs +639 -0
- package/dist-lib/config.cjs +96 -0
- package/dist-lib/detox/index.cjs +3789 -0
- package/dist-lib/detox/jest-preset.cjs +29 -0
- package/dist-lib/dev-bundle-resolution.cjs +290 -0
- package/dist-lib/home-paths.cjs +638 -0
- package/dist-lib/host/bridge-host.cjs +10903 -0
- package/dist-lib/host/fetch-proxy-handler.cjs +421 -0
- package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
- package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
- package/dist-lib/host/replacement-module-handler.cjs +469 -0
- package/dist-lib/host/websocket-proxy.cjs +298 -0
- package/dist-lib/index.cjs +4268 -0
- package/dist-lib/jump-to-source-babel.cjs +93 -0
- package/dist-lib/jump-to-source-native.cjs +112 -0
- package/dist-lib/menu.cjs +646 -0
- package/dist-lib/menu.mjs +604 -0
- package/dist-lib/metro-fingerprint-registry.cjs +186 -0
- package/dist-lib/metro-fingerprint-registry.mjs +156 -0
- package/dist-lib/metro-production-bundle.cjs +329 -0
- package/dist-lib/metro-production-bundle.mjs +295 -0
- package/dist-lib/metro.cjs +904 -0
- package/dist-lib/profiles.cjs +246 -0
- package/dist-lib/public-brand.cjs +39 -0
- package/dist-lib/react-native-host-modules.cjs +51 -0
- package/dist-lib/react-native-host-modules.mjs +25 -0
- package/dist-lib/render-mode.cjs +54 -0
- package/dist-lib/scripts/dev-server-scanner.cjs +1232 -0
- package/dist-lib/sdk.cjs +2433 -0
- package/dist-lib/sdk.mjs +2336 -0
- package/dist-lib/skills.cjs +39132 -0
- package/dist-lib/swift-submit.cjs +92 -0
- package/dist-lib/swift-submit.mjs +65 -0
- package/dist-lib/swift.cjs +1646 -0
- package/dist-lib/vite.cjs +1591 -0
- package/package.json +290 -0
- package/scripts/dev-server-scanner.ts +1291 -0
- package/scripts/optional-demo-registry.ts +113 -0
- package/skills/contrast/SKILL.md +165 -0
- package/skills/rnx-debug/SKILL.md +433 -0
- package/skills/rnx-perf/SKILL.md +399 -0
- package/skills/rnx-setup/SKILL.md +172 -0
- package/skills/rnx-test/SKILL.md +321 -0
- package/skills/rnx-visual/SKILL.md +126 -0
- package/src/agent-daemon-client.ts +410 -0
- package/src/agent-events.ts +71 -0
- package/src/agent-identity.ts +23 -0
- package/src/agent-prompt.ts +71 -0
- package/src/agent-sessions.ts +580 -0
- package/src/app-url.ts +39 -0
- package/src/attached-projects.ts +539 -0
- package/src/auth/shared-session.ts +235 -0
- package/src/backend-origin.ts +70 -0
- package/src/beta.ts +19 -0
- package/src/bridge-constants.ts +175 -0
- package/src/bridge-contract-input.ts +502 -0
- package/src/bridge-contract.ts +558 -0
- package/src/browser-cache.ts +16 -0
- package/src/capture-contract.ts +587 -0
- package/src/cli-constants.ts +1 -0
- package/src/cli-version.ts +40 -0
- package/src/cloud-contract.ts +274 -0
- package/src/cloud.ts +762 -0
- package/src/codex-client.ts +234 -0
- package/src/config.ts +132 -0
- package/src/connect.ts +101 -0
- package/src/dev-bundle-resolution.ts +346 -0
- package/src/dev-server-open.ts +198 -0
- package/src/disk-cleanup.ts +540 -0
- package/src/flow-timeout-scale.ts +15 -0
- package/src/home-paths.ts +806 -0
- package/src/host/agent-host.ts +666 -0
- package/src/host/bridge-host.ts +2788 -0
- package/src/host/fetch-proxy-handler.ts +449 -0
- package/src/host/fetch-proxy-overrides.ts +51 -0
- package/src/host/open-url.ts +89 -0
- package/src/host/plane-host.ts +203 -0
- package/src/host/replacement-module-handler.ts +158 -0
- package/src/host/websocket-proxy.ts +315 -0
- package/src/index.ts +71 -0
- package/src/jump-to-source-babel.ts +135 -0
- package/src/jump-to-source-native.ts +102 -0
- package/src/menu.ts +777 -0
- package/src/metro-fingerprint-registry.ts +302 -0
- package/src/metro-plugin.ts +341 -0
- package/src/metro-production-bundle.ts +471 -0
- package/src/native-dev-bundle-url.ts +118 -0
- package/src/native-seam-manifest.ts +341 -0
- package/src/playwright-host-log.ts +16 -0
- package/src/plist.ts +27 -0
- package/src/poll-until.ts +123 -0
- package/src/profiles.ts +179 -0
- package/src/public-brand.ts +9 -0
- package/src/react-native-host-modules.ts +26 -0
- package/src/render-mode.ts +27 -0
- package/src/rolldown-plugin-swift.ts +240 -0
- package/src/runtime-assets.ts +233 -0
- package/src/runtime-delivery.ts +54 -0
- package/src/screenshots/compose.ts +422 -0
- package/src/screenshots/frame-compose.ts +514 -0
- package/src/screenshots/orchestrate.ts +256 -0
- package/src/screenshots/org-deck.ts +91 -0
- package/src/screenshots/registry.ts +58 -0
- package/src/screenshots/schema.ts +364 -0
- package/src/sdk.ts +15 -0
- package/src/settings-bundle.ts +418 -0
- package/src/shell-bridge-identity.ts +15 -0
- package/src/sim-client.ts +697 -0
- package/src/skills/builtin/a11y-review.ts +132 -0
- package/src/skills/builtin/compat-check.ts +71 -0
- package/src/skills/builtin/maestro-test.ts +121 -0
- package/src/skills/builtin/perf-profile.ts +90 -0
- package/src/skills/builtin/screenshot-all.ts +52 -0
- package/src/skills/builtin/visual-diff.ts +100 -0
- package/src/skills/registry.ts +107 -0
- package/src/skills/types.ts +41 -0
- package/src/swift-submit.ts +185 -0
- package/src/swift.ts +7 -0
- package/src/team-machine-prompt.ts +73 -0
- package/src/vite-plugin-one.ts +264 -0
- package/src/vite-plugin-swift.ts +620 -0
- package/src/vite-plugin.ts +1946 -0
- package/src/worklets-babel.ts +250 -0
package/src/cloud.ts
ADDED
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
// typed Remote Simulators client for agents. the CLI produces artifact bytes
|
|
2
|
+
// (local file or a fetched URL); this module talks to the service in those
|
|
3
|
+
// bytes only. command types and receipts are the same ones the CLI already
|
|
4
|
+
// sends. do not invent a second vocabulary.
|
|
5
|
+
//
|
|
6
|
+
// one call gives you one remote simulator. it runs on a nano box today, which
|
|
7
|
+
// is why the session below carries a box id and the create request asks for a
|
|
8
|
+
// size, but that is the service's business: nothing box-shaped reaches the
|
|
9
|
+
// public surface.
|
|
10
|
+
|
|
11
|
+
import { isLoopbackHost } from './backend-origin'
|
|
12
|
+
import { isRnxScreenCapture, type RnxScreenCapture } from './capture-contract'
|
|
13
|
+
import {
|
|
14
|
+
isRnxCloudCommandScope,
|
|
15
|
+
isRnxCloudCommandType,
|
|
16
|
+
RNX_CLOUD_COMMAND_TYPES,
|
|
17
|
+
RNX_CLOUD_MAX_ARTIFACT_BYTES,
|
|
18
|
+
RNX_CLOUD_SCOPED_TOKEN_PREFIX,
|
|
19
|
+
RNX_CLOUD_TOKEN_SCOPES,
|
|
20
|
+
rnxCloudCommandScope,
|
|
21
|
+
rnxCloudScopesAllowCommand,
|
|
22
|
+
type RnxCloudBoxCreateReceipt,
|
|
23
|
+
type RnxCloudClaim,
|
|
24
|
+
type RnxCloudCommandScope,
|
|
25
|
+
type RnxCloudCreateReceipt,
|
|
26
|
+
type RnxCloudScopedToken,
|
|
27
|
+
} from './cloud-contract'
|
|
28
|
+
import { rnxPublicBrand } from './public-brand'
|
|
29
|
+
import type { SimStateOptions } from './bridge-contract'
|
|
30
|
+
|
|
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
|
+
}
|
|
45
|
+
|
|
46
|
+
const DEFAULT_DEVICE = 'iphone-16'
|
|
47
|
+
const DEFAULT_WAIT_READY_MS = 20_000
|
|
48
|
+
|
|
49
|
+
// how many times a request goes back after a retryable refusal or a server
|
|
50
|
+
// error. the sim service passes the account's own 429 bodies through, so the
|
|
51
|
+
// code on the body is what decides: `rate_limited` carries a `retry-after`
|
|
52
|
+
// and is worth waiting for, while `concurrency_limit` refuses until the
|
|
53
|
+
// customer stops something and retrying only hammers a closed door.
|
|
54
|
+
// `early_access` and 402 never change on retry either.
|
|
55
|
+
const CLOUD_MAX_RETRIES = 2
|
|
56
|
+
const CLOUD_NEVER_RETRY_CODES = new Set(['concurrency_limit', 'early_access'])
|
|
57
|
+
|
|
58
|
+
export type RemoteCommand = {
|
|
59
|
+
type: (typeof RNX_CLOUD_COMMAND_TYPES)[number]
|
|
60
|
+
[key: string]: unknown
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface CreateRemoteSimulatorInput {
|
|
64
|
+
artifact: Uint8Array
|
|
65
|
+
authorization: string
|
|
66
|
+
origin?: string
|
|
67
|
+
device?: string
|
|
68
|
+
sha256?: string
|
|
69
|
+
}
|
|
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
|
+
|
|
80
|
+
// the simulator-driving verbs. the same vocabulary the CLI drives, one for
|
|
81
|
+
// one: `rnx describe`, `rnx do tap`, `rnx wait selector`.
|
|
82
|
+
export interface RnxCommands {
|
|
83
|
+
command(
|
|
84
|
+
command: { type: string; [key: string]: unknown },
|
|
85
|
+
options?: { timeoutMs?: number },
|
|
86
|
+
): Promise<unknown>
|
|
87
|
+
describe(): Promise<unknown>
|
|
88
|
+
do(command: { type: string; [key: string]: unknown }): Promise<unknown>
|
|
89
|
+
wait(
|
|
90
|
+
condition: 'ready' | { type: 'selector'; selector: string },
|
|
91
|
+
options?: { maxMs?: number },
|
|
92
|
+
): Promise<unknown>
|
|
93
|
+
// a `data:image/png;base64,` url, assembled by the service from the frame
|
|
94
|
+
screenshot(options?: { layers?: 'full' | 'tenant' | 'shell' }): Promise<string>
|
|
95
|
+
capture(): Promise<RnxScreenCapture>
|
|
96
|
+
// data wipes the app's stores and cold-remounts, keeping install-scoped
|
|
97
|
+
// credentials; full clears those too. from boots the wiped simulator on a
|
|
98
|
+
// wholesale live copy of another simulator's storage instead of empty
|
|
99
|
+
// stores. the bundle itself is never reinstalled: artifacts are immutable
|
|
100
|
+
// and sha-addressed, so there is no install to redo.
|
|
101
|
+
reset(options?: { strategy?: 'data' | 'full'; from?: string }): Promise<unknown>
|
|
102
|
+
// the one-call read: screenshot, tree, route, and recent errors together.
|
|
103
|
+
state(options?: SimStateOptions): Promise<unknown>
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface RemoteSimulatorSession {
|
|
107
|
+
origin: string
|
|
108
|
+
// present on the create path only. a connected simulator never learned its
|
|
109
|
+
// box, so closing one stops the simulator rather than its box.
|
|
110
|
+
boxId: string | null
|
|
111
|
+
boxToken: string | null
|
|
112
|
+
simId: string
|
|
113
|
+
simulatorToken: string
|
|
114
|
+
// null until the simulator reports one. connect takes no claim and steals
|
|
115
|
+
// none, so an unconfirmed simulator attaches with none.
|
|
116
|
+
claim: RnxCloudClaim | null
|
|
117
|
+
artifact: RnxCloudCreateReceipt['artifact']
|
|
118
|
+
// what this token may send. the create path carries every scope except
|
|
119
|
+
// debug; a connected simulator carries what its token was minted with, and
|
|
120
|
+
// verbs outside it are refused before any request.
|
|
121
|
+
scopes: readonly RnxCloudCommandScope[]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
125
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function normalizeCloudOrigin(source: string): string {
|
|
129
|
+
let parsed: URL
|
|
130
|
+
try {
|
|
131
|
+
parsed = new URL(source)
|
|
132
|
+
} catch {
|
|
133
|
+
throw new Error(`invalid RNX Cloud origin: ${source}`)
|
|
134
|
+
}
|
|
135
|
+
if (
|
|
136
|
+
(parsed.protocol !== 'https:' && parsed.protocol !== 'http:') ||
|
|
137
|
+
parsed.username ||
|
|
138
|
+
parsed.password ||
|
|
139
|
+
parsed.pathname !== '/' ||
|
|
140
|
+
parsed.search ||
|
|
141
|
+
parsed.hash
|
|
142
|
+
) {
|
|
143
|
+
throw new Error(`RNX Cloud origin must be an HTTP(S) origin: ${source}`)
|
|
144
|
+
}
|
|
145
|
+
if (parsed.protocol === 'http:' && !isLoopbackHost(parsed.hostname)) {
|
|
146
|
+
throw new Error(`RNX Cloud origin requires HTTPS outside loopback: ${source}`)
|
|
147
|
+
}
|
|
148
|
+
return parsed.origin
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function resolveOrigin(value?: string): string {
|
|
152
|
+
const configured = value?.trim() || process.env.RNX_CLOUD_ORIGIN?.trim()
|
|
153
|
+
return normalizeCloudOrigin(configured || rnxPublicBrand.origin)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function responseError(status: number, text: string): Error {
|
|
157
|
+
if (text) {
|
|
158
|
+
try {
|
|
159
|
+
const parsed: unknown = JSON.parse(text)
|
|
160
|
+
const error = isRecord(parsed) ? parsed.error : null
|
|
161
|
+
if (isRecord(error) && typeof error.message === 'string') {
|
|
162
|
+
return new Error(`RNX Cloud request failed (${status}): ${error.message}`)
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
// body was not json; fall through to the raw text
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return new Error(
|
|
169
|
+
`RNX Cloud request failed (${status})${text.trim() ? `: ${text.trim()}` : ''}`,
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function readCloudErrorCode(text: string): string | null {
|
|
174
|
+
// the account api answers `{ error: 'code', message }` and the sim service
|
|
175
|
+
// `{ error: { code, message } }`; both reach this client, so both read.
|
|
176
|
+
let parsed: unknown
|
|
177
|
+
try {
|
|
178
|
+
parsed = JSON.parse(text)
|
|
179
|
+
} catch {
|
|
180
|
+
return null
|
|
181
|
+
}
|
|
182
|
+
if (!isRecord(parsed)) return null
|
|
183
|
+
if (typeof parsed.error === 'string') return parsed.error
|
|
184
|
+
if (isRecord(parsed.error) && typeof parsed.error.code === 'string') {
|
|
185
|
+
return parsed.error.code
|
|
186
|
+
}
|
|
187
|
+
return null
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function readRetryAfterMs(response: Response, text: string): number | null {
|
|
191
|
+
const header = response.headers.get('retry-after')
|
|
192
|
+
if (header !== null) {
|
|
193
|
+
const seconds = Number(header.trim())
|
|
194
|
+
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000
|
|
195
|
+
}
|
|
196
|
+
let parsed: unknown
|
|
197
|
+
try {
|
|
198
|
+
parsed = JSON.parse(text)
|
|
199
|
+
} catch {
|
|
200
|
+
return null
|
|
201
|
+
}
|
|
202
|
+
if (
|
|
203
|
+
isRecord(parsed) &&
|
|
204
|
+
typeof parsed.retryAfterMs === 'number' &&
|
|
205
|
+
Number.isFinite(parsed.retryAfterMs) &&
|
|
206
|
+
parsed.retryAfterMs >= 0
|
|
207
|
+
) {
|
|
208
|
+
return parsed.retryAfterMs
|
|
209
|
+
}
|
|
210
|
+
return null
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function sleep(ms: number): Promise<void> {
|
|
214
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// one fetch with the cloud retry policy: `rate_limited` waits out its
|
|
218
|
+
// `retry-after` and 5xx goes again at once, each at most twice, and nothing
|
|
219
|
+
// else is retried. the body is buffered only on a retryable status, so every
|
|
220
|
+
// other refusal reaches the caller on its original response.
|
|
221
|
+
async function fetchWithCloudRetry(
|
|
222
|
+
url: string,
|
|
223
|
+
init: RequestInit,
|
|
224
|
+
options?: { timeoutMs?: number },
|
|
225
|
+
): Promise<Response> {
|
|
226
|
+
let attempt = 0
|
|
227
|
+
for (;;) {
|
|
228
|
+
const response = await fetch(
|
|
229
|
+
url,
|
|
230
|
+
options?.timeoutMs === undefined
|
|
231
|
+
? init
|
|
232
|
+
: { ...init, signal: AbortSignal.timeout(options.timeoutMs) },
|
|
233
|
+
)
|
|
234
|
+
if (response.ok) return response
|
|
235
|
+
const retryableStatus = response.status === 429 || response.status >= 500
|
|
236
|
+
if (!retryableStatus || attempt >= CLOUD_MAX_RETRIES) return response
|
|
237
|
+
const text = await response.text()
|
|
238
|
+
const code = readCloudErrorCode(text)
|
|
239
|
+
const retryable =
|
|
240
|
+
response.status >= 500
|
|
241
|
+
? !(code !== null && CLOUD_NEVER_RETRY_CODES.has(code))
|
|
242
|
+
: code === 'rate_limited'
|
|
243
|
+
if (!retryable) {
|
|
244
|
+
return new Response(text, { status: response.status, headers: response.headers })
|
|
245
|
+
}
|
|
246
|
+
if (response.status === 429) {
|
|
247
|
+
await sleep(readRetryAfterMs(response, text) ?? 1000)
|
|
248
|
+
}
|
|
249
|
+
attempt += 1
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function copyArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
|
254
|
+
const copy = new ArrayBuffer(bytes.byteLength)
|
|
255
|
+
new Uint8Array(copy).set(bytes)
|
|
256
|
+
return copy
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function sha256Hex(bytes: Uint8Array): Promise<string> {
|
|
260
|
+
const digest = await globalThis.crypto.subtle.digest('SHA-256', copyArrayBuffer(bytes))
|
|
261
|
+
const view = new Uint8Array(digest)
|
|
262
|
+
let hex = ''
|
|
263
|
+
for (const byte of view) hex += byte.toString(16).padStart(2, '0')
|
|
264
|
+
return hex
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function readJsArtifact(bytes: Uint8Array): string {
|
|
268
|
+
let source: string
|
|
269
|
+
try {
|
|
270
|
+
source = new TextDecoder('utf-8', { fatal: true }).decode(bytes)
|
|
271
|
+
} catch {
|
|
272
|
+
throw new Error('rnx Cloud artifact must be valid UTF-8 JavaScript, not bytecode')
|
|
273
|
+
}
|
|
274
|
+
if (source.includes('\0') || !source.includes('__d(') || !source.includes('__r(')) {
|
|
275
|
+
throw new Error('rnx Cloud artifact must be a Metro JavaScript bundle')
|
|
276
|
+
}
|
|
277
|
+
return source
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function readClaim(value: unknown): RnxCloudClaim {
|
|
281
|
+
if (
|
|
282
|
+
!isRecord(value) ||
|
|
283
|
+
typeof value.id !== 'string' ||
|
|
284
|
+
!value.id ||
|
|
285
|
+
typeof value.expiresAt !== 'number'
|
|
286
|
+
) {
|
|
287
|
+
throw new Error('RNX Cloud returned an invalid claim')
|
|
288
|
+
}
|
|
289
|
+
return { id: value.id, expiresAt: value.expiresAt }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function artifactDigestId(sha256: string): `sha256:${string}` {
|
|
293
|
+
return `sha256:${sha256}`
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function readArtifact(
|
|
297
|
+
value: unknown,
|
|
298
|
+
expectedId: `sha256:${string}`,
|
|
299
|
+
expectedBytes: number,
|
|
300
|
+
): RnxCloudBoxCreateReceipt['artifact'] {
|
|
301
|
+
if (
|
|
302
|
+
!isRecord(value) ||
|
|
303
|
+
value.id !== expectedId ||
|
|
304
|
+
typeof value.bytes !== 'number' ||
|
|
305
|
+
!Number.isInteger(value.bytes) ||
|
|
306
|
+
value.bytes !== expectedBytes
|
|
307
|
+
) {
|
|
308
|
+
throw new Error('RNX Cloud returned an invalid artifact receipt')
|
|
309
|
+
}
|
|
310
|
+
return { id: expectedId, bytes: expectedBytes }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function readSimulatorCreate(
|
|
314
|
+
value: unknown,
|
|
315
|
+
expectedId: `sha256:${string}`,
|
|
316
|
+
expectedBytes: number,
|
|
317
|
+
): {
|
|
318
|
+
simId: string
|
|
319
|
+
token: string
|
|
320
|
+
claim: RnxCloudClaim
|
|
321
|
+
artifact: RnxCloudBoxCreateReceipt['artifact']
|
|
322
|
+
} {
|
|
323
|
+
if (!isRecord(value)) throw new Error('RNX Cloud create returned an invalid simulator')
|
|
324
|
+
const claim = readClaim(value.claim)
|
|
325
|
+
if (
|
|
326
|
+
typeof value.simId !== 'string' ||
|
|
327
|
+
!value.simId ||
|
|
328
|
+
typeof value.token !== 'string'
|
|
329
|
+
) {
|
|
330
|
+
throw new Error('RNX Cloud create returned an invalid simulator')
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
simId: value.simId,
|
|
334
|
+
token: value.token,
|
|
335
|
+
claim,
|
|
336
|
+
artifact: readArtifact(value.artifact, expectedId, expectedBytes),
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function readBoxCreate(
|
|
341
|
+
value: unknown,
|
|
342
|
+
expectedBytes: number,
|
|
343
|
+
expectedSha256: string,
|
|
344
|
+
): {
|
|
345
|
+
receipt: RnxCloudBoxCreateReceipt
|
|
346
|
+
boxToken: string
|
|
347
|
+
simulatorToken: string
|
|
348
|
+
} {
|
|
349
|
+
const artifactId = artifactDigestId(expectedSha256)
|
|
350
|
+
if (!isRecord(value)) throw new Error('RNX Cloud create returned invalid JSON')
|
|
351
|
+
const simulator = readSimulatorCreate(value.simulator, artifactId, expectedBytes)
|
|
352
|
+
if (
|
|
353
|
+
typeof value.boxId !== 'string' ||
|
|
354
|
+
!value.boxId ||
|
|
355
|
+
value.size !== 'nano' ||
|
|
356
|
+
typeof value.token !== 'string' ||
|
|
357
|
+
!value.token
|
|
358
|
+
) {
|
|
359
|
+
throw new Error('RNX Cloud create returned an invalid box receipt')
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
receipt: {
|
|
363
|
+
boxId: value.boxId,
|
|
364
|
+
size: 'nano',
|
|
365
|
+
artifact: readArtifact(value.artifact, artifactId, expectedBytes),
|
|
366
|
+
simulator: {
|
|
367
|
+
simId: simulator.simId,
|
|
368
|
+
artifact: simulator.artifact,
|
|
369
|
+
claim: simulator.claim,
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
boxToken: value.token,
|
|
373
|
+
simulatorToken: simulator.token,
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function cloudCommand(
|
|
378
|
+
command: { type: string; [key: string]: unknown },
|
|
379
|
+
scopes: readonly RnxCloudCommandScope[],
|
|
380
|
+
): RemoteCommand {
|
|
381
|
+
const clean: Record<string, unknown> = {}
|
|
382
|
+
for (const [key, value] of Object.entries(command)) {
|
|
383
|
+
if (key !== 'id' && key !== 'simId') clean[key] = value
|
|
384
|
+
}
|
|
385
|
+
if (!isRnxCloudCommandType(command.type)) {
|
|
386
|
+
throw new Error(`RNX Cloud does not support bridge command type ${command.type}`)
|
|
387
|
+
}
|
|
388
|
+
// a verb outside this token's scopes never leaves the sdk: the refusal
|
|
389
|
+
// names the scope before a request. the full token carries every scope
|
|
390
|
+
// except debug, so evaluate/call/close stop here for it.
|
|
391
|
+
if (!rnxCloudScopesAllowCommand(scopes, command.type)) {
|
|
392
|
+
throw new Error(
|
|
393
|
+
`RNX Cloud command ${command.type} requires the ${rnxCloudCommandScope(command.type)} scope`,
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
return { ...clean, type: command.type }
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export class RemoteSimulator {
|
|
400
|
+
private session: RemoteSimulatorSession
|
|
401
|
+
|
|
402
|
+
// driving verbs live here so the client reads the way the CLI does. bound to
|
|
403
|
+
// this simulator at construction, and every verb reads the session live, so a
|
|
404
|
+
// confirmed claim reaches commands issued after it.
|
|
405
|
+
readonly rnx: RnxCommands
|
|
406
|
+
|
|
407
|
+
constructor(session: RemoteSimulatorSession) {
|
|
408
|
+
this.session = session
|
|
409
|
+
this.rnx = {
|
|
410
|
+
command: (command, options) => this.sendCommand(command, options),
|
|
411
|
+
describe: () => this.sendCommand({ type: 'tree' }),
|
|
412
|
+
do: (command) => this.sendCommand(command),
|
|
413
|
+
wait: (condition, options) => {
|
|
414
|
+
const timeoutMs = options?.maxMs ?? DEFAULT_WAIT_READY_MS
|
|
415
|
+
const waitCondition = condition === 'ready' ? { type: 'ready' } : condition
|
|
416
|
+
return this.sendCommand(
|
|
417
|
+
{
|
|
418
|
+
type: 'waitFor',
|
|
419
|
+
waitForOptions: { condition: waitCondition, timeoutMs },
|
|
420
|
+
},
|
|
421
|
+
{ timeoutMs: timeoutMs + 1_000 },
|
|
422
|
+
)
|
|
423
|
+
},
|
|
424
|
+
screenshot: async (options) => {
|
|
425
|
+
const result = await this.sendCommand({
|
|
426
|
+
type: 'screenshot',
|
|
427
|
+
layers: options?.layers ?? 'tenant',
|
|
428
|
+
})
|
|
429
|
+
if (typeof result !== 'string' || !result.startsWith('data:image/')) {
|
|
430
|
+
throw new Error('RNX Cloud screenshot did not return an image data url')
|
|
431
|
+
}
|
|
432
|
+
return result
|
|
433
|
+
},
|
|
434
|
+
capture: async () => {
|
|
435
|
+
const result = await this.sendCommand({ type: 'capture' })
|
|
436
|
+
if (!isRnxScreenCapture(result)) {
|
|
437
|
+
throw new Error('RNX Cloud capture returned an invalid screen capture')
|
|
438
|
+
}
|
|
439
|
+
return result
|
|
440
|
+
},
|
|
441
|
+
reset: (options) => {
|
|
442
|
+
if (
|
|
443
|
+
options?.strategy !== undefined &&
|
|
444
|
+
options.strategy !== 'data' &&
|
|
445
|
+
options.strategy !== 'full'
|
|
446
|
+
) {
|
|
447
|
+
throw new Error('RNX Cloud reset strategy must be data or full')
|
|
448
|
+
}
|
|
449
|
+
if (
|
|
450
|
+
options?.from !== undefined &&
|
|
451
|
+
(typeof options.from !== 'string' || options.from.length === 0)
|
|
452
|
+
) {
|
|
453
|
+
throw new Error('RNX Cloud reset from must name a simulator')
|
|
454
|
+
}
|
|
455
|
+
return this.sendCommand(
|
|
456
|
+
options === undefined
|
|
457
|
+
? { type: 'reset' }
|
|
458
|
+
: {
|
|
459
|
+
type: 'reset',
|
|
460
|
+
resetOptions: {
|
|
461
|
+
...(options.strategy === undefined
|
|
462
|
+
? {}
|
|
463
|
+
: { strategy: options.strategy }),
|
|
464
|
+
...(options.from === undefined ? {} : { from: options.from }),
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
)
|
|
468
|
+
},
|
|
469
|
+
state: (options) =>
|
|
470
|
+
this.sendCommand(
|
|
471
|
+
options === undefined
|
|
472
|
+
? { type: 'state' }
|
|
473
|
+
: { type: 'state', stateOptions: options },
|
|
474
|
+
),
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
get simId(): string {
|
|
479
|
+
return this.session.simId
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
get claim(): RnxCloudClaim | null {
|
|
483
|
+
return this.session.claim
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
get scopes(): readonly RnxCloudCommandScope[] {
|
|
487
|
+
return this.session.scopes
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
get receipt(): RnxCloudCreateReceipt {
|
|
491
|
+
if (!this.session.claim) {
|
|
492
|
+
throw new Error('this simulator was attached with connect() and holds no claim')
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
simId: this.session.simId,
|
|
496
|
+
artifact: this.session.artifact,
|
|
497
|
+
claim: this.session.claim,
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
private async sendCommand(
|
|
502
|
+
command: { type: string; [key: string]: unknown },
|
|
503
|
+
options?: { timeoutMs?: number },
|
|
504
|
+
): Promise<unknown> {
|
|
505
|
+
const body = {
|
|
506
|
+
id: globalThis.crypto.randomUUID(),
|
|
507
|
+
// a scoped token drives without a claim: the service checks the token
|
|
508
|
+
// instead. a full token still joins the claim it names here.
|
|
509
|
+
claimId: this.session.claim?.id ?? '',
|
|
510
|
+
command: cloudCommand(command, this.session.scopes),
|
|
511
|
+
}
|
|
512
|
+
let response: Response
|
|
513
|
+
try {
|
|
514
|
+
response = await fetchWithCloudRetry(
|
|
515
|
+
`${this.session.origin}/v1/sims/${encodeURIComponent(this.session.simId)}/commands`,
|
|
516
|
+
{
|
|
517
|
+
method: 'POST',
|
|
518
|
+
headers: {
|
|
519
|
+
authorization: `Bearer ${this.session.simulatorToken}`,
|
|
520
|
+
'content-type': 'application/json',
|
|
521
|
+
},
|
|
522
|
+
body: JSON.stringify(body),
|
|
523
|
+
},
|
|
524
|
+
options?.timeoutMs === undefined ? undefined : { timeoutMs: options.timeoutMs },
|
|
525
|
+
)
|
|
526
|
+
} catch (error) {
|
|
527
|
+
throw new Error(
|
|
528
|
+
`could not reach RNX Cloud at ${this.session.origin}: ${
|
|
529
|
+
error instanceof Error ? error.message : String(error)
|
|
530
|
+
}`,
|
|
531
|
+
)
|
|
532
|
+
}
|
|
533
|
+
const text = await response.text()
|
|
534
|
+
if (!response.ok) throw responseError(response.status, text)
|
|
535
|
+
let parsed: unknown
|
|
536
|
+
try {
|
|
537
|
+
parsed = JSON.parse(text)
|
|
538
|
+
} catch {
|
|
539
|
+
throw new Error('RNX Cloud command returned unreadable JSON')
|
|
540
|
+
}
|
|
541
|
+
if (!isRecord(parsed) || parsed.id !== body.id) {
|
|
542
|
+
throw new Error('RNX Cloud command returned an invalid command receipt')
|
|
543
|
+
}
|
|
544
|
+
if (typeof parsed.error === 'string' && parsed.error) throw new Error(parsed.error)
|
|
545
|
+
if (!Object.hasOwn(parsed, 'result')) {
|
|
546
|
+
throw new Error('RNX Cloud command receipt has no result')
|
|
547
|
+
}
|
|
548
|
+
return parsed.result
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async confirm(): Promise<RnxCloudClaim> {
|
|
552
|
+
if (!this.session.claim) {
|
|
553
|
+
throw new Error('this simulator was attached with connect() and holds no claim')
|
|
554
|
+
}
|
|
555
|
+
const claimId = this.session.claim.id
|
|
556
|
+
const response = await fetchWithCloudRetry(
|
|
557
|
+
`${this.session.origin}/v1/sims/${encodeURIComponent(this.session.simId)}/claim`,
|
|
558
|
+
{
|
|
559
|
+
method: 'POST',
|
|
560
|
+
headers: {
|
|
561
|
+
authorization: `Bearer ${this.session.simulatorToken}`,
|
|
562
|
+
'content-type': 'application/json',
|
|
563
|
+
},
|
|
564
|
+
body: JSON.stringify({ claimId }),
|
|
565
|
+
},
|
|
566
|
+
)
|
|
567
|
+
const text = await response.text()
|
|
568
|
+
if (!response.ok) throw responseError(response.status, text)
|
|
569
|
+
let parsed: unknown
|
|
570
|
+
try {
|
|
571
|
+
parsed = JSON.parse(text)
|
|
572
|
+
} catch {
|
|
573
|
+
throw new Error('RNX Cloud claim returned unreadable JSON')
|
|
574
|
+
}
|
|
575
|
+
const claim = readClaim(isRecord(parsed) ? parsed.claim : null)
|
|
576
|
+
if (claim.id !== claimId) {
|
|
577
|
+
throw new Error('RNX Cloud confirmation replaced the initial claim')
|
|
578
|
+
}
|
|
579
|
+
this.session = { ...this.session, claim }
|
|
580
|
+
return claim
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async close(): Promise<void> {
|
|
584
|
+
// the create path tears down the box it made. a connected simulator never
|
|
585
|
+
// learned its box, so it stops its own simulator instead; a scoped token
|
|
586
|
+
// is refused there, which is the service telling the holder it may drive
|
|
587
|
+
// but never delete.
|
|
588
|
+
const target =
|
|
589
|
+
this.session.boxId !== null && this.session.boxToken !== null
|
|
590
|
+
? {
|
|
591
|
+
url: `${this.session.origin}/v1/boxes/${encodeURIComponent(this.session.boxId)}`,
|
|
592
|
+
authorization: `Bearer ${this.session.boxToken}`,
|
|
593
|
+
}
|
|
594
|
+
: {
|
|
595
|
+
url: `${this.session.origin}/v1/sims/${encodeURIComponent(this.session.simId)}`,
|
|
596
|
+
authorization: `Bearer ${this.session.simulatorToken}`,
|
|
597
|
+
}
|
|
598
|
+
const response = await fetchWithCloudRetry(target.url, {
|
|
599
|
+
method: 'DELETE',
|
|
600
|
+
headers: { authorization: target.authorization },
|
|
601
|
+
})
|
|
602
|
+
const text = await response.text()
|
|
603
|
+
if (!response.ok && response.status !== 404) {
|
|
604
|
+
throw responseError(response.status, text)
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export async function createRemoteSimulator(
|
|
610
|
+
input: CreateRemoteSimulatorInput,
|
|
611
|
+
): Promise<RemoteSimulator> {
|
|
612
|
+
if (input.artifact.byteLength > RNX_CLOUD_MAX_ARTIFACT_BYTES) {
|
|
613
|
+
throw new Error(
|
|
614
|
+
`rnx Cloud artifact is ${input.artifact.byteLength} bytes and exceeds the ${RNX_CLOUD_MAX_ARTIFACT_BYTES}-byte limit`,
|
|
615
|
+
)
|
|
616
|
+
}
|
|
617
|
+
if (input.artifact.byteLength <= 0) {
|
|
618
|
+
throw new Error('rnx Cloud artifact is empty')
|
|
619
|
+
}
|
|
620
|
+
readJsArtifact(input.artifact)
|
|
621
|
+
const origin = resolveOrigin(input.origin)
|
|
622
|
+
const sha256 = await sha256Hex(input.artifact)
|
|
623
|
+
if (input.sha256 && input.sha256 !== sha256) {
|
|
624
|
+
throw new Error('rnx Cloud artifact sha256 does not match the provided digest')
|
|
625
|
+
}
|
|
626
|
+
const device = input.device?.trim() || DEFAULT_DEVICE
|
|
627
|
+
let response: Response
|
|
628
|
+
try {
|
|
629
|
+
response = await fetchWithCloudRetry(`${origin}/v1/boxes`, {
|
|
630
|
+
method: 'POST',
|
|
631
|
+
headers: {
|
|
632
|
+
authorization: input.authorization,
|
|
633
|
+
'content-type': 'application/javascript',
|
|
634
|
+
'x-rnx-box-size': 'nano',
|
|
635
|
+
'x-rnx-artifact-sha256': sha256,
|
|
636
|
+
'x-rnx-platform': 'ios',
|
|
637
|
+
'x-rnx-device': device,
|
|
638
|
+
},
|
|
639
|
+
body: copyArrayBuffer(input.artifact),
|
|
640
|
+
})
|
|
641
|
+
} catch (error) {
|
|
642
|
+
throw new Error(
|
|
643
|
+
`could not reach RNX Cloud at ${origin}: ${
|
|
644
|
+
error instanceof Error ? error.message : String(error)
|
|
645
|
+
}`,
|
|
646
|
+
)
|
|
647
|
+
}
|
|
648
|
+
const responseText = await response.text()
|
|
649
|
+
if (!response.ok) throw responseError(response.status, responseText)
|
|
650
|
+
let parsed: unknown
|
|
651
|
+
try {
|
|
652
|
+
parsed = JSON.parse(responseText)
|
|
653
|
+
} catch {
|
|
654
|
+
throw new Error('RNX Cloud create returned unreadable JSON')
|
|
655
|
+
}
|
|
656
|
+
const created = readBoxCreate(parsed, input.artifact.byteLength, sha256)
|
|
657
|
+
const simulator = new RemoteSimulator({
|
|
658
|
+
origin,
|
|
659
|
+
boxId: created.receipt.boxId,
|
|
660
|
+
boxToken: created.boxToken,
|
|
661
|
+
simId: created.receipt.simulator.simId,
|
|
662
|
+
simulatorToken: created.simulatorToken,
|
|
663
|
+
claim: created.receipt.simulator.claim,
|
|
664
|
+
artifact: created.receipt.artifact,
|
|
665
|
+
scopes: RNX_CLOUD_TOKEN_SCOPES,
|
|
666
|
+
})
|
|
667
|
+
await simulator.confirm()
|
|
668
|
+
return simulator
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function readSimArtifact(value: unknown): RnxCloudCreateReceipt['artifact'] {
|
|
672
|
+
if (
|
|
673
|
+
!isRecord(value) ||
|
|
674
|
+
typeof value.sha256 !== 'string' ||
|
|
675
|
+
!/^[a-f0-9]{64}$/.test(value.sha256) ||
|
|
676
|
+
typeof value.bytes !== 'number' ||
|
|
677
|
+
!Number.isInteger(value.bytes) ||
|
|
678
|
+
value.bytes <= 0
|
|
679
|
+
) {
|
|
680
|
+
throw new Error('RNX Cloud returned an invalid simulator artifact')
|
|
681
|
+
}
|
|
682
|
+
return { id: `sha256:${value.sha256}`, bytes: value.bytes }
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function readConnectClaim(value: unknown): RnxCloudClaim | null {
|
|
686
|
+
if (value === null) return null
|
|
687
|
+
return readClaim(value)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function readConnectScopes(
|
|
691
|
+
value: unknown,
|
|
692
|
+
token: string,
|
|
693
|
+
): readonly RnxCloudCommandScope[] {
|
|
694
|
+
// the full per-simulator token carries every scope except debug, which the
|
|
695
|
+
// service already guarantees. a scoped token learns its verbs from the get
|
|
696
|
+
// it just made, so a verb outside them is refused before any request.
|
|
697
|
+
if (!token.startsWith(RNX_CLOUD_SCOPED_TOKEN_PREFIX)) return RNX_CLOUD_TOKEN_SCOPES
|
|
698
|
+
if (
|
|
699
|
+
!isRecord(value) ||
|
|
700
|
+
typeof value.id !== 'string' ||
|
|
701
|
+
!value.id ||
|
|
702
|
+
!Array.isArray(value.scopes) ||
|
|
703
|
+
value.scopes.length === 0 ||
|
|
704
|
+
!value.scopes.every(
|
|
705
|
+
(scope): scope is RnxCloudCommandScope =>
|
|
706
|
+
typeof scope === 'string' && isRnxCloudCommandScope(scope),
|
|
707
|
+
) ||
|
|
708
|
+
typeof value.expiresAt !== 'number'
|
|
709
|
+
) {
|
|
710
|
+
throw new Error('RNX Cloud returned an invalid scoped token')
|
|
711
|
+
}
|
|
712
|
+
return Object.freeze([...value.scopes])
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// attach to a simulator another process created. one get proves the token,
|
|
716
|
+
// and the simulator it returns drives with that token's scopes, takes no
|
|
717
|
+
// claim, and never steals one.
|
|
718
|
+
export async function connect(
|
|
719
|
+
input: ConnectRemoteSimulatorInput,
|
|
720
|
+
): Promise<RemoteSimulator> {
|
|
721
|
+
if (!input.id) throw new Error('RNX Cloud connect needs a simulator id')
|
|
722
|
+
if (!input.token) throw new Error('RNX Cloud connect needs a token')
|
|
723
|
+
const origin = resolveOrigin(input.origin)
|
|
724
|
+
let response: Response
|
|
725
|
+
try {
|
|
726
|
+
response = await fetchWithCloudRetry(
|
|
727
|
+
`${origin}/v1/sims/${encodeURIComponent(input.id)}`,
|
|
728
|
+
{ headers: { authorization: `Bearer ${input.token}` } },
|
|
729
|
+
)
|
|
730
|
+
} catch (error) {
|
|
731
|
+
throw new Error(
|
|
732
|
+
`could not reach RNX Cloud at ${origin}: ${
|
|
733
|
+
error instanceof Error ? error.message : String(error)
|
|
734
|
+
}`,
|
|
735
|
+
)
|
|
736
|
+
}
|
|
737
|
+
const text = await response.text()
|
|
738
|
+
if (!response.ok) throw responseError(response.status, text)
|
|
739
|
+
let parsed: unknown
|
|
740
|
+
try {
|
|
741
|
+
parsed = JSON.parse(text)
|
|
742
|
+
} catch {
|
|
743
|
+
throw new Error('RNX Cloud connect returned unreadable JSON')
|
|
744
|
+
}
|
|
745
|
+
if (!isRecord(parsed) || !isRecord(parsed.sim)) {
|
|
746
|
+
throw new Error('RNX Cloud connect returned an invalid simulator')
|
|
747
|
+
}
|
|
748
|
+
const sim = parsed.sim
|
|
749
|
+
if (typeof sim.simId !== 'string' || !sim.simId) {
|
|
750
|
+
throw new Error('RNX Cloud connect returned an invalid simulator')
|
|
751
|
+
}
|
|
752
|
+
return new RemoteSimulator({
|
|
753
|
+
origin,
|
|
754
|
+
boxId: null,
|
|
755
|
+
boxToken: null,
|
|
756
|
+
simId: sim.simId,
|
|
757
|
+
simulatorToken: input.token,
|
|
758
|
+
claim: readConnectClaim(sim.claim ?? null),
|
|
759
|
+
artifact: readSimArtifact(sim.artifact),
|
|
760
|
+
scopes: readConnectScopes(isRecord(parsed) ? parsed.token : null, input.token),
|
|
761
|
+
})
|
|
762
|
+
}
|