@stim-cli/server 1.23.1 → 1.24.0
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/README.md +29 -6
- package/dist/{protocol-BKpAsa9G.mjs → protocol-B8Ztv1K4.mjs} +56 -0
- package/dist/protocol.mjs +1 -1
- package/dist/protocol.schema.json +90 -0
- package/dist/{stim-server-Z2tFBWaG.mjs → stim-server-Z40-_Git.mjs} +88 -9
- package/dist/stim-server.mjs +1 -1
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -547,8 +547,11 @@ are not carried over: phones pair again, and remote machines are approved
|
|
|
547
547
|
again with a new setup ticket.
|
|
548
548
|
|
|
549
549
|
Stim Desktop and other clients of the server's own user on the same machine do
|
|
550
|
-
not pair. They connect to the Unix socket `$STIM_HOME/server/local.sock
|
|
551
|
-
|
|
550
|
+
not pair. They connect to the Unix socket `$STIM_HOME/server/local.sock`. If
|
|
551
|
+
that path exceeds the platform's limit (103 UTF-8 bytes on macOS, 107 on Linux), the socket instead lives at
|
|
552
|
+
`/tmp/stim-sockets-<uid>/<sha256-of-normalized-socket-path>.sock`, so long hosted
|
|
553
|
+
session homes remain usable and distinct homes keep distinct sockets. The
|
|
554
|
+
socket is mode 0600 in a mode 0700 directory owned by this user and needs no handshake.
|
|
552
555
|
That connection gets read and control. The Desktop-only methods (`route.setup`,
|
|
553
556
|
`machines.update.*`, `device-host.sessions` and `device-host.sessions.stop`)
|
|
554
557
|
work only there; on a server Desktop started, only after the private Desktop
|
|
@@ -556,8 +559,13 @@ bootstrap proof (see [Private Desktop bootstrap](#private-desktop-bootstrap)).
|
|
|
556
559
|
Windows has no socket.
|
|
557
560
|
|
|
558
561
|
The network address, `tailscale whois` and the `X-Forwarded-For` header that
|
|
559
|
-
`tailscale serve` adds never authenticate. They
|
|
560
|
-
`stim-server devices`) and key the rate limits below.
|
|
562
|
+
`tailscale serve` adds never authenticate. They label a record (`from` in
|
|
563
|
+
`stim-server devices`) and key the rate limits below. Tailscale rewrites the
|
|
564
|
+
header to the real tailnet source, so remote clients cannot forge it. A local
|
|
565
|
+
process on the host can connect to the loopback port and forge it to lock one
|
|
566
|
+
named peer, or the shared local bucket, out for the lockout window. It
|
|
567
|
+
authenticates nothing and reads nothing. This is accepted because it needs
|
|
568
|
+
local code execution on the host.
|
|
561
569
|
|
|
562
570
|
A connection must finish the handshake and send `hello` within 5 seconds of
|
|
563
571
|
each step. Five failed attempts from the same peer address within a minute
|
|
@@ -1143,6 +1151,12 @@ stream over WireGuard with socket backpressure; no public tunnel, Funnel or
|
|
|
1143
1151
|
Tailscale serve configuration change is needed. The 64 KiB server message
|
|
1144
1152
|
limit remains unchanged.
|
|
1145
1153
|
|
|
1154
|
+
An approved client calls `device-host.metro.ports` with `{ports}`, at most 256
|
|
1155
|
+
of them, before it starts Metro for an iOS Expo dev client, and the host
|
|
1156
|
+
answers `{busy}`: the listed ports that already accept loopback connections,
|
|
1157
|
+
apart from the client's own dev-client bridges. The feature
|
|
1158
|
+
`hosted-metro-ports` advertises it.
|
|
1159
|
+
|
|
1146
1160
|
Replaying the same open request keeps the port. Client disconnection leaves
|
|
1147
1161
|
the bridge available for the same session while its server owner lives. Call
|
|
1148
1162
|
`device-host.metro.close` to replace a gateway, then reopen on the same port;
|
|
@@ -1700,7 +1714,17 @@ connected since stim-server started keeps its directory when revoked.
|
|
|
1700
1714
|
`ccache` or `none`) and `stimBuild`, after the whole manifest of `repo` was
|
|
1701
1715
|
synced on this connection. A peer-cache-capable React Native caller can set
|
|
1702
1716
|
`cacheArtifact: true` to retain a completed development archive when its cache
|
|
1703
|
-
write and remote policies allow it.
|
|
1717
|
+
write and remote policies allow it. With the `fingerprint-sources` feature, a
|
|
1718
|
+
caller can send `fingerprintSources`, the `id` and `hash` of each source of
|
|
1719
|
+
`fingerprint`, and a `fingerprint-mismatch` failure names the sources that
|
|
1720
|
+
differ and the synced files prebuild or `pod install` rewrote. With the
|
|
1721
|
+
`fingerprint-ignored` feature, it can send `fingerprintIgnored`, the paths
|
|
1722
|
+
relative to `project` that its git ignores for `fingerprint` through
|
|
1723
|
+
`.git/info/exclude` or a global excludes file, which the machine's synced
|
|
1724
|
+
checkout has no copy of. With the `fingerprint-tracked` feature, it can
|
|
1725
|
+
send `fingerprintTracked`, the top-level entry names of `project` that its
|
|
1726
|
+
git tracks although an ignore pattern matches them, which the machine's
|
|
1727
|
+
empty index would skip. It returns `{ "job" }`. It fails with
|
|
1704
1728
|
`build-busy` while the offer would be declined, naming the reason, or while
|
|
1705
1729
|
another build of the same client and repository runs, or while every one
|
|
1706
1730
|
of this machine's `concurrency.maxBuilds` slots is held, and with
|
|
@@ -2497,7 +2521,6 @@ silent only when every notification it sums up is. An older server ignores
|
|
|
2497
2521
|
`levels` and alerts for every event but `started`.
|
|
2498
2522
|
|
|
2499
2523
|
The server and phone import the same pure rules from `@stim-cli/core/oversight`.
|
|
2500
|
-
The phone formats home attention messages locally from the facts the shared rule selects.
|
|
2501
2524
|
`__tests__/oversight-agreement.test.ts` checks their categories, workspace names
|
|
2502
2525
|
and needs-attention vectors. Notification delivery remains local to each.
|
|
2503
2526
|
|
|
@@ -2275,6 +2275,20 @@ function protocolJsonSchema() {
|
|
|
2275
2275
|
pattern: "^[a-f0-9]{64}$"
|
|
2276
2276
|
}
|
|
2277
2277
|
}, ["gatewayPort", "secret"])),
|
|
2278
|
+
request("device-host.metro.ports", {
|
|
2279
|
+
type: "object",
|
|
2280
|
+
required: ["ports"],
|
|
2281
|
+
additionalProperties: false,
|
|
2282
|
+
properties: { ports: {
|
|
2283
|
+
type: "array",
|
|
2284
|
+
maxItems: 256,
|
|
2285
|
+
items: {
|
|
2286
|
+
type: "integer",
|
|
2287
|
+
minimum: 1,
|
|
2288
|
+
maximum: 65535
|
|
2289
|
+
}
|
|
2290
|
+
} }
|
|
2291
|
+
}),
|
|
2278
2292
|
request("device-host.metro.close", session({})),
|
|
2279
2293
|
request("device-host.frames.subscribe", session({
|
|
2280
2294
|
fps: {
|
|
@@ -3106,6 +3120,48 @@ function protocolJsonSchema() {
|
|
|
3106
3120
|
type: ["string", "null"],
|
|
3107
3121
|
minLength: 1
|
|
3108
3122
|
},
|
|
3123
|
+
fingerprintSources: {
|
|
3124
|
+
type: "array",
|
|
3125
|
+
maxItems: 2e4,
|
|
3126
|
+
description: "The id and hash of each source of fingerprint, to name the sources that differ.",
|
|
3127
|
+
items: {
|
|
3128
|
+
type: "object",
|
|
3129
|
+
required: ["id", "hash"],
|
|
3130
|
+
additionalProperties: false,
|
|
3131
|
+
properties: {
|
|
3132
|
+
id: {
|
|
3133
|
+
type: "string",
|
|
3134
|
+
minLength: 1,
|
|
3135
|
+
maxLength: 1024
|
|
3136
|
+
},
|
|
3137
|
+
hash: {
|
|
3138
|
+
type: ["string", "null"],
|
|
3139
|
+
pattern: "^[0-9a-f]{1,128}$"
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
},
|
|
3144
|
+
fingerprintIgnored: {
|
|
3145
|
+
type: "array",
|
|
3146
|
+
maxItems: 2e4,
|
|
3147
|
+
description: "Paths relative to project that git ignores for fingerprint on the caller, such as through .git/info/exclude.",
|
|
3148
|
+
items: {
|
|
3149
|
+
type: "string",
|
|
3150
|
+
minLength: 1,
|
|
3151
|
+
maxLength: 1024
|
|
3152
|
+
}
|
|
3153
|
+
},
|
|
3154
|
+
fingerprintTracked: {
|
|
3155
|
+
type: "array",
|
|
3156
|
+
maxItems: 2e4,
|
|
3157
|
+
description: "Top-level entries of project that the caller git tracks for fingerprint although an ignore pattern matches them.",
|
|
3158
|
+
items: {
|
|
3159
|
+
type: "string",
|
|
3160
|
+
minLength: 1,
|
|
3161
|
+
maxLength: 255,
|
|
3162
|
+
pattern: "^[^/\\u0000]+$"
|
|
3163
|
+
}
|
|
3164
|
+
},
|
|
3109
3165
|
native: { oneOf: [{
|
|
3110
3166
|
type: "object",
|
|
3111
3167
|
required: [
|
package/dist/protocol.mjs
CHANGED
|
@@ -1271,10 +1271,14 @@
|
|
|
1271
1271
|
"native-gradle-artifact-cache",
|
|
1272
1272
|
"native-gradle-peer-cache",
|
|
1273
1273
|
"react-native-peer-cache",
|
|
1274
|
+
"fingerprint-sources",
|
|
1275
|
+
"fingerprint-ignored",
|
|
1276
|
+
"fingerprint-tracked",
|
|
1274
1277
|
"swiftpm-peer-cache",
|
|
1275
1278
|
"build-dependency-cache",
|
|
1276
1279
|
"hosted-android-data",
|
|
1277
1280
|
"hosted-android-process",
|
|
1281
|
+
"hosted-metro-ports",
|
|
1278
1282
|
"server-update",
|
|
1279
1283
|
"status-views",
|
|
1280
1284
|
"desktop-reports",
|
|
@@ -3183,6 +3187,44 @@
|
|
|
3183
3187
|
}
|
|
3184
3188
|
}
|
|
3185
3189
|
},
|
|
3190
|
+
{
|
|
3191
|
+
"type": "object",
|
|
3192
|
+
"required": [
|
|
3193
|
+
"id",
|
|
3194
|
+
"method",
|
|
3195
|
+
"params"
|
|
3196
|
+
],
|
|
3197
|
+
"additionalProperties": false,
|
|
3198
|
+
"properties": {
|
|
3199
|
+
"id": {
|
|
3200
|
+
"type": [
|
|
3201
|
+
"integer",
|
|
3202
|
+
"string"
|
|
3203
|
+
]
|
|
3204
|
+
},
|
|
3205
|
+
"method": {
|
|
3206
|
+
"const": "device-host.metro.ports"
|
|
3207
|
+
},
|
|
3208
|
+
"params": {
|
|
3209
|
+
"type": "object",
|
|
3210
|
+
"required": [
|
|
3211
|
+
"ports"
|
|
3212
|
+
],
|
|
3213
|
+
"additionalProperties": false,
|
|
3214
|
+
"properties": {
|
|
3215
|
+
"ports": {
|
|
3216
|
+
"type": "array",
|
|
3217
|
+
"maxItems": 256,
|
|
3218
|
+
"items": {
|
|
3219
|
+
"type": "integer",
|
|
3220
|
+
"minimum": 1,
|
|
3221
|
+
"maximum": 65535
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
},
|
|
3186
3228
|
{
|
|
3187
3229
|
"type": "object",
|
|
3188
3230
|
"required": [
|
|
@@ -7303,6 +7345,54 @@
|
|
|
7303
7345
|
],
|
|
7304
7346
|
"minLength": 1
|
|
7305
7347
|
},
|
|
7348
|
+
"fingerprintSources": {
|
|
7349
|
+
"type": "array",
|
|
7350
|
+
"maxItems": 20000,
|
|
7351
|
+
"description": "The id and hash of each source of fingerprint, to name the sources that differ.",
|
|
7352
|
+
"items": {
|
|
7353
|
+
"type": "object",
|
|
7354
|
+
"required": [
|
|
7355
|
+
"id",
|
|
7356
|
+
"hash"
|
|
7357
|
+
],
|
|
7358
|
+
"additionalProperties": false,
|
|
7359
|
+
"properties": {
|
|
7360
|
+
"id": {
|
|
7361
|
+
"type": "string",
|
|
7362
|
+
"minLength": 1,
|
|
7363
|
+
"maxLength": 1024
|
|
7364
|
+
},
|
|
7365
|
+
"hash": {
|
|
7366
|
+
"type": [
|
|
7367
|
+
"string",
|
|
7368
|
+
"null"
|
|
7369
|
+
],
|
|
7370
|
+
"pattern": "^[0-9a-f]{1,128}$"
|
|
7371
|
+
}
|
|
7372
|
+
}
|
|
7373
|
+
}
|
|
7374
|
+
},
|
|
7375
|
+
"fingerprintIgnored": {
|
|
7376
|
+
"type": "array",
|
|
7377
|
+
"maxItems": 20000,
|
|
7378
|
+
"description": "Paths relative to project that git ignores for fingerprint on the caller, such as through .git/info/exclude.",
|
|
7379
|
+
"items": {
|
|
7380
|
+
"type": "string",
|
|
7381
|
+
"minLength": 1,
|
|
7382
|
+
"maxLength": 1024
|
|
7383
|
+
}
|
|
7384
|
+
},
|
|
7385
|
+
"fingerprintTracked": {
|
|
7386
|
+
"type": "array",
|
|
7387
|
+
"maxItems": 20000,
|
|
7388
|
+
"description": "Top-level entries of project that the caller git tracks for fingerprint although an ignore pattern matches them.",
|
|
7389
|
+
"items": {
|
|
7390
|
+
"type": "string",
|
|
7391
|
+
"minLength": 1,
|
|
7392
|
+
"maxLength": 255,
|
|
7393
|
+
"pattern": "^[^/\\u0000]+$"
|
|
7394
|
+
}
|
|
7395
|
+
},
|
|
7306
7396
|
"native": {
|
|
7307
7397
|
"oneOf": [
|
|
7308
7398
|
{
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as protocol_exports } from "./protocol-
|
|
1
|
+
import { n as protocol_exports } from "./protocol-B8Ztv1K4.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
4
4
|
import { appendFileSync, chmodSync, closeSync, constants, copyFileSync, cpSync, createReadStream, existsSync, futimesSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, statfsSync, watch, writeFileSync, writeSync } from "node:fs";
|
|
@@ -3226,6 +3226,32 @@ function validFile(file) {
|
|
|
3226
3226
|
const validRepo = (repo) => typeof repo === "string" && new RegExp(protocol_exports.BUILD_REPO_PATTERN).test(repo);
|
|
3227
3227
|
const gb = (bytes) => (bytes / 1024 ** 3).toFixed(1);
|
|
3228
3228
|
const optional = (value) => typeof value === "string" ? value : null;
|
|
3229
|
+
const MAX_FINGERPRINT_SOURCES = 2e4;
|
|
3230
|
+
function fingerprintSources(value) {
|
|
3231
|
+
if (value === void 0) return void 0;
|
|
3232
|
+
if (!Array.isArray(value) || value.length > MAX_FINGERPRINT_SOURCES) return null;
|
|
3233
|
+
return value.every((source) => isJsonObject(source) && Object.keys(source).every((key) => key === "id" || key === "hash") && typeof source.id === "string" && source.id.length > 0 && source.id.length <= MAX_PATH_CHARS && (source.hash === null || typeof source.hash === "string" && /^[0-9a-f]{1,128}$/.test(source.hash))) ? value : null;
|
|
3234
|
+
}
|
|
3235
|
+
function fingerprintIgnored(value) {
|
|
3236
|
+
if (value === void 0) return void 0;
|
|
3237
|
+
if (!Array.isArray(value) || value.length > MAX_FINGERPRINT_SOURCES) return null;
|
|
3238
|
+
const paths = [];
|
|
3239
|
+
for (const path of value) {
|
|
3240
|
+
if (typeof path !== "string" || path.length === 0 || path.length > MAX_PATH_CHARS) return null;
|
|
3241
|
+
paths.push(path);
|
|
3242
|
+
}
|
|
3243
|
+
return paths;
|
|
3244
|
+
}
|
|
3245
|
+
function fingerprintTracked(value) {
|
|
3246
|
+
if (value === void 0) return void 0;
|
|
3247
|
+
if (!Array.isArray(value) || value.length > MAX_FINGERPRINT_SOURCES) return null;
|
|
3248
|
+
const names = [];
|
|
3249
|
+
for (const name of value) {
|
|
3250
|
+
if (!(typeof name === "string" && name.length > 0 && name.length <= 255 && name !== "." && name !== ".." && !/[/\0]/.test(name))) return null;
|
|
3251
|
+
names.push(name);
|
|
3252
|
+
}
|
|
3253
|
+
return names;
|
|
3254
|
+
}
|
|
3229
3255
|
const GRADLE_NAME = /^[A-Za-z0-9_-]{1,100}$/;
|
|
3230
3256
|
const GRADLE_ROOT = /^[A-Za-z0-9._/ -]{1,200}$/;
|
|
3231
3257
|
const GRADLE_MODULE = /^(:(?!\.\.?(?::|$))[A-Za-z0-9._-]+){1,20}$/;
|
|
@@ -3522,7 +3548,7 @@ var BuildHost = class {
|
|
|
3522
3548
|
},
|
|
3523
3549
|
warm: {
|
|
3524
3550
|
checkout: existsSync(join(area, "src")),
|
|
3525
|
-
dependencies: typeof params.lockfile === "string" && dependencies !== void 0 && reusableDependencies(area, dependencies.project,
|
|
3551
|
+
dependencies: typeof params.lockfile === "string" && dependencies !== void 0 && reusableDependencies(area, dependencies.project, params.lockfile, this.options.env),
|
|
3526
3552
|
build: false
|
|
3527
3553
|
},
|
|
3528
3554
|
...artifact !== void 0 ? { artifact } : {}
|
|
@@ -4305,7 +4331,13 @@ var BuildSession = class {
|
|
|
4305
4331
|
native = input;
|
|
4306
4332
|
}
|
|
4307
4333
|
if (native?.provider !== "gradle" && (typeof params.fingerprint !== "string" || !params.fingerprint)) return refusal("bad-request", "build.start needs params.fingerprint.");
|
|
4334
|
+
const expectedSources = fingerprintSources(params.fingerprintSources);
|
|
4335
|
+
if (expectedSources === null) return refusal("bad-request", `params.fingerprintSources must be at most ${MAX_FINGERPRINT_SOURCES} sources, each an id and a hex hash or null.`);
|
|
4308
4336
|
if (!native && [...this.files.values()].some((file) => file.kind === "directory")) return refusal("bad-request", "Directory entries require a native build request.");
|
|
4337
|
+
const expectedIgnored = fingerprintIgnored(params.fingerprintIgnored);
|
|
4338
|
+
if (expectedIgnored === null) return refusal("bad-request", `params.fingerprintIgnored must be at most ${MAX_FINGERPRINT_SOURCES} paths of at most ${MAX_PATH_CHARS} characters.`);
|
|
4339
|
+
const expectedTracked = fingerprintTracked(params.fingerprintTracked);
|
|
4340
|
+
if (expectedTracked === null) return refusal("bad-request", `params.fingerprintTracked must be at most ${MAX_FINGERPRINT_SOURCES} top-level entry names.`);
|
|
4309
4341
|
const android = platform === "android" ? androidOptions(params.android) : null;
|
|
4310
4342
|
if (platform === "android" && !android) return refusal("bad-request", "An android build needs params.android: variant and abi (string or null), gradleBuildCache, pch and compilerCache.");
|
|
4311
4343
|
let macos = null;
|
|
@@ -4333,7 +4365,6 @@ var BuildSession = class {
|
|
|
4333
4365
|
android,
|
|
4334
4366
|
macos,
|
|
4335
4367
|
project: params.project,
|
|
4336
|
-
packageName: optional(params.packageName),
|
|
4337
4368
|
isExpo: params.isExpo === true,
|
|
4338
4369
|
cacheArtifact: params.cacheArtifact === true,
|
|
4339
4370
|
stimBuild: params.stimBuild,
|
|
@@ -4342,6 +4373,9 @@ var BuildSession = class {
|
|
|
4342
4373
|
iosProjectPath: optional(params.iosProjectPath),
|
|
4343
4374
|
runtime: optional(params.runtime),
|
|
4344
4375
|
expectedFingerprint: params.fingerprint,
|
|
4376
|
+
...expectedSources ? { expectedSources } : {},
|
|
4377
|
+
...expectedIgnored ? { expectedIgnored } : {},
|
|
4378
|
+
...expectedTracked ? { expectedTracked } : {},
|
|
4345
4379
|
optimizations: isJsonObject(params.optimizations) ? params.optimizations : null
|
|
4346
4380
|
}
|
|
4347
4381
|
});
|
|
@@ -6823,6 +6857,28 @@ function hostedDeviceExclusions(records) {
|
|
|
6823
6857
|
key: hostedDeviceId(record.device)
|
|
6824
6858
|
}] : []);
|
|
6825
6859
|
}
|
|
6860
|
+
/** macOS lets 127.0.0.1 bind beside a wildcard listener and takes its loopback traffic, so probe by connecting. */
|
|
6861
|
+
function loopbackAccepts(port) {
|
|
6862
|
+
const accepts = (host) => new Promise((resolve) => {
|
|
6863
|
+
const socket = connect({
|
|
6864
|
+
host,
|
|
6865
|
+
port
|
|
6866
|
+
});
|
|
6867
|
+
const done = (open) => {
|
|
6868
|
+
socket.destroy();
|
|
6869
|
+
resolve(open);
|
|
6870
|
+
};
|
|
6871
|
+
socket.setTimeout(1e3, () => done(true));
|
|
6872
|
+
socket.once("connect", () => done(true));
|
|
6873
|
+
socket.once("error", (error) => done(![
|
|
6874
|
+
"ECONNREFUSED",
|
|
6875
|
+
"EADDRNOTAVAIL",
|
|
6876
|
+
"EAFNOSUPPORT",
|
|
6877
|
+
"ENETUNREACH"
|
|
6878
|
+
].includes(error.code ?? "")));
|
|
6879
|
+
});
|
|
6880
|
+
return Promise.all([accepts("127.0.0.1"), accepts("::1")]).then((open) => open.includes(true));
|
|
6881
|
+
}
|
|
6826
6882
|
/** Owns hosted reservations, not build jobs or viewer connections. Native work runs in the packaged CLI child. */
|
|
6827
6883
|
var DeviceHost = class {
|
|
6828
6884
|
owned = /* @__PURE__ */ new Map();
|
|
@@ -7190,15 +7246,20 @@ var DeviceHost = class {
|
|
|
7190
7246
|
await this.restoreAndroidMetro(record, owned);
|
|
7191
7247
|
return { result: { port } };
|
|
7192
7248
|
}
|
|
7249
|
+
const devClientPort = record.platform === "ios" ? params.clientMetroPort : void 0;
|
|
7193
7250
|
const bridge = createMetroBridge({
|
|
7194
7251
|
peer,
|
|
7195
7252
|
gatewayPort: params.gatewayPort,
|
|
7196
7253
|
secret: params.secret
|
|
7197
7254
|
});
|
|
7198
|
-
const
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7255
|
+
const inUse = () => Object.assign(/* @__PURE__ */ new Error(`Port ${devClientPort} is in use on the hosting Mac, and an Expo dev client loads its bundle from the client's Metro port.`), { code: "device-busy" });
|
|
7256
|
+
const ready = (async () => {
|
|
7257
|
+
if (devClientPort !== void 0 && await loopbackAccepts(devClientPort)) throw inUse();
|
|
7258
|
+
return new Promise((resolve, reject) => {
|
|
7259
|
+
bridge.server.once("error", (error) => reject(devClientPort !== void 0 && error.code === "EADDRINUSE" ? inUse() : error));
|
|
7260
|
+
bridge.server.listen(devClientPort ?? record.metroPort ?? 0, "127.0.0.1", () => resolve(bridge.server.address().port));
|
|
7261
|
+
});
|
|
7262
|
+
})();
|
|
7202
7263
|
const metro = {
|
|
7203
7264
|
bridge,
|
|
7204
7265
|
ready,
|
|
@@ -7224,7 +7285,7 @@ var DeviceHost = class {
|
|
|
7224
7285
|
throw error;
|
|
7225
7286
|
}
|
|
7226
7287
|
} catch (error) {
|
|
7227
|
-
return refused$1("action-failed", error.message);
|
|
7288
|
+
return refused$1(error.code === "device-busy" ? "device-busy" : "action-failed", error.message);
|
|
7228
7289
|
}
|
|
7229
7290
|
}
|
|
7230
7291
|
async restoreAndroidMetro(record, owned) {
|
|
@@ -7253,6 +7314,24 @@ var DeviceHost = class {
|
|
|
7253
7314
|
if (owned.data === settled) delete owned.data;
|
|
7254
7315
|
}
|
|
7255
7316
|
}
|
|
7317
|
+
/** Names the ports a new dev-client bridge could not bind, apart from this client's own dev-client bridges. */
|
|
7318
|
+
async metroPorts(client, params) {
|
|
7319
|
+
if (this.closed || !this.options.allowed(client)) return refused$1("forbidden", "Current device-host approval is required.");
|
|
7320
|
+
if (!isJsonObject(params) || !Array.isArray(params.ports) || params.ports.length > 256 || params.ports.some((port) => typeof port !== "number" || !Number.isInteger(port) || port < 1 || port > 65535)) return refused$1("bad-request", "Metro ports needs at most 256 TCP ports.");
|
|
7321
|
+
try {
|
|
7322
|
+
const ports = [...new Set(params.ports)];
|
|
7323
|
+
const sessions = new Set(readHostedSessions().flatMap((record) => record.client === client ? [record.id] : []));
|
|
7324
|
+
const own = new Set([...this.owned].flatMap(([id, { metro }]) => sessions.has(id) && metro?.port !== void 0 && metro.port === metro.clientMetroPort ? [metro.port] : []));
|
|
7325
|
+
const busy = [];
|
|
7326
|
+
const queue = ports.filter((port) => !own.has(port));
|
|
7327
|
+
await Promise.all(Array.from({ length: 16 }, async () => {
|
|
7328
|
+
for (let port = queue.shift(); port !== void 0; port = queue.shift()) if (await loopbackAccepts(port)) busy.push(port);
|
|
7329
|
+
}));
|
|
7330
|
+
return { result: { busy: busy.toSorted((a, b) => a - b) } };
|
|
7331
|
+
} catch (error) {
|
|
7332
|
+
return refused$1("action-failed", error.message);
|
|
7333
|
+
}
|
|
7334
|
+
}
|
|
7256
7335
|
async metroClose(client, params) {
|
|
7257
7336
|
if (!this.options.allowed(client)) return refused$1("forbidden", "Current device-host approval is required.");
|
|
7258
7337
|
if (!isJsonObject(params) || typeof params.session !== "string") return refused$1("bad-request", "Metro close needs a session.");
|
|
@@ -16909,7 +16988,7 @@ async function startServer(options) {
|
|
|
16909
16988
|
});
|
|
16910
16989
|
}
|
|
16911
16990
|
if (method.startsWith("device-host.input.")) return input(id, method.slice(12), raw, session);
|
|
16912
|
-
const answer = method === "device-host.offer" ? await hostedDevices.offer(session.id, raw) : method === "device-host.reserve" ? await hostedDevices.reserve(session.id, raw) : method === "device-host.attach" ? hostedDevices.attach(session.id, raw) : method === "device-host.stop" ? hostedDevices.stop(session.id, raw) : method === "device-host.app.offer" ? hostedDevices.appOffer(session.id, raw) : method === "device-host.app.chunk" ? await hostedDevices.appChunk(session.id, raw) : method === "device-host.app.handoff" ? await hostedDevices.appHandoff(session.id, raw) : method === "device-host.app.launch" ? hostedDevices.appLaunch(session.id, raw) : method === "device-host.app.attach" ? hostedDevices.appAttach(session.id, raw) : method === "device-host.logs.query" ? await hostedDevices.logsQuery(session.id, raw) : method === "device-host.metro.open" ? await hostedDevices.metroOpen(session.id, raw, peer) : await hostedDevices.metroClose(session.id, raw);
|
|
16991
|
+
const answer = method === "device-host.offer" ? await hostedDevices.offer(session.id, raw) : method === "device-host.reserve" ? await hostedDevices.reserve(session.id, raw) : method === "device-host.attach" ? hostedDevices.attach(session.id, raw) : method === "device-host.stop" ? hostedDevices.stop(session.id, raw) : method === "device-host.app.offer" ? hostedDevices.appOffer(session.id, raw) : method === "device-host.app.chunk" ? await hostedDevices.appChunk(session.id, raw) : method === "device-host.app.handoff" ? await hostedDevices.appHandoff(session.id, raw) : method === "device-host.app.launch" ? hostedDevices.appLaunch(session.id, raw) : method === "device-host.app.attach" ? hostedDevices.appAttach(session.id, raw) : method === "device-host.logs.query" ? await hostedDevices.logsQuery(session.id, raw) : method === "device-host.metro.open" ? await hostedDevices.metroOpen(session.id, raw, peer) : method === "device-host.metro.ports" ? await hostedDevices.metroPorts(session.id, raw) : await hostedDevices.metroClose(session.id, raw);
|
|
16913
16992
|
return send(socket, "error" in answer ? {
|
|
16914
16993
|
id,
|
|
16915
16994
|
error: answer.error
|
package/dist/stim-server.mjs
CHANGED
|
@@ -4,6 +4,6 @@ const encode = (version) => version.split(".", 3).reduce((sum, part) => sum * 1e
|
|
|
4
4
|
if (encode(process.versions.node) < encode("22.12.0")) {
|
|
5
5
|
process.stderr.write(`STIM_NODE_UNSUPPORTED: stim-server needs Node 22.12.0 or later; this is Node ${process.versions.node} at ${process.execPath}. To run stim-server where a project pins an older Node, see https://stim.appandflow.com/docs/requirements#older-node-pins\n`);
|
|
6
6
|
process.exitCode = 1;
|
|
7
|
-
} else import("./stim-server-
|
|
7
|
+
} else import("./stim-server-Z40-_Git.mjs");
|
|
8
8
|
//#endregion
|
|
9
9
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stim-cli/server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.0",
|
|
4
4
|
"description": "Serves Stim status to paired clients over Tailscale, and runs reload and stop for clients granted control.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -28,13 +28,13 @@
|
|
|
28
28
|
"./protocol.schema.json": "./dist/protocol.schema.json"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@stim-cli/agent-driver": "^1.
|
|
32
|
-
"@stim-cli/agent-driver-agent-device": "^1.
|
|
33
|
-
"@stim-cli/cache": "^1.
|
|
34
|
-
"@stim-cli/core": "^1.
|
|
35
|
-
"@stim-cli/transport": "^1.
|
|
36
|
-
"@stim-cli/transport-tailscale": "^1.
|
|
37
|
-
"stim": "1.
|
|
31
|
+
"@stim-cli/agent-driver": "^1.24.0",
|
|
32
|
+
"@stim-cli/agent-driver-agent-device": "^1.24.0",
|
|
33
|
+
"@stim-cli/cache": "^1.24.0",
|
|
34
|
+
"@stim-cli/core": "^1.24.0",
|
|
35
|
+
"@stim-cli/transport": "^1.24.0",
|
|
36
|
+
"@stim-cli/transport-tailscale": "^1.24.0",
|
|
37
|
+
"stim": "1.24.0",
|
|
38
38
|
"ws": "^8.21.3"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|