@stim-cli/core 1.13.0 → 1.15.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 CHANGED
@@ -35,4 +35,19 @@ registry and its JSON Schema, the created-device and EAS session ledger readers,
35
35
  NDJSON log records and queries, and the status and settings payload types. The
36
36
  CLI owns every write to that state. The cache packages do not import it.
37
37
 
38
+ `@stim-cli/core/oversight` holds the pure notification and needs-attention rules
39
+ shared by stim-server and the phone. It imports no Node APIs; mobile consumers
40
+ import this subpath without loading the Node-only root or state entry points.
41
+ Build core before typechecking, testing or bundling the phone app.
42
+
38
43
  The npm scope remains `@stim-cli` until the `@stim` scope is available.
44
+
45
+ `@stim-cli/core/protocol` defines the server's wire contracts. The
46
+ `phone-protocol` entry derives the phone's receiving types, including optional
47
+ fields from older servers. `receive-protocol` contains precompiled validators
48
+ for method results, known events and errors, with no Node or schema compiler
49
+ dependency in the phone runtime. Unknown compatible fields remain accepted.
50
+ Display enums accept open strings, and unknown values show neutral information or are skipped when unsupported.
51
+
52
+ After changing these contracts, run `pnpm run rpc:generate` at the repository
53
+ root; `pnpm run rpc:check` verifies the committed validators in CI.
@@ -1,8 +1,9 @@
1
1
  import { S as quotedPath, _ as releaseClaim, r as ClaimRefusedError, x as tryAcquireClaim } from "./ownership-claim-O31Y5iIb.mjs";
2
- import fs, { existsSync, lstatSync, mkdirSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
2
+ import fs, { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
3
3
  import os from "node:os";
4
4
  import path, { basename, dirname, join } from "node:path";
5
- import { createHash } from "node:crypto";
5
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
6
+ import { connect, createServer, isIP } from "node:net";
6
7
  //#region dir-lock.ts
7
8
  const DEFAULT_LOCK_WAIT_MS = 12e3;
8
9
  const DEFAULT_LOCK_POLL_MS = 25;
@@ -150,6 +151,12 @@ function withDirLock(lockPath, fn, { waitMs = DEFAULT_LOCK_WAIT_MS, pollMs = DEF
150
151
  }
151
152
  }
152
153
  //#endregion
154
+ //#region macos-resources.ts
155
+ /** Whether a resource destination is a contained, non-empty path under Contents/Resources. */
156
+ function validMacosResourceDestination(value) {
157
+ return typeof value === "string" && value.length > 0 && value.length <= 1024 && !value.includes("\0") && !value.includes("\\") && !/^[A-Za-z]:/.test(value) && value.split("/").every((part) => part !== "" && part !== "." && part !== "..");
158
+ }
159
+ //#endregion
153
160
  //#region artifact-store.ts
154
161
  function artifactIn(dir) {
155
162
  if (!existsSync(dir)) return null;
@@ -247,6 +254,122 @@ function storeArtifact(dest, buildPath, { runFile, overwrite = false, writeMetad
247
254
  return artifactIn(dest);
248
255
  }
249
256
  //#endregion
257
+ //#region compiled-helper.ts
258
+ /**
259
+ * Returns `<dir>/<name>-<hash>`, compiling it with `compile` first when it is missing. The hash covers `version` and
260
+ * each input's name and bytes, so a changed source or compiler builds a new helper beside the old one. `compile`
261
+ * writes to a private temporary path that is renamed into place, so concurrent builders never expose a partial file.
262
+ */
263
+ async function compiledHelper({ dir, name, inputs, version, compile }) {
264
+ const hash = createHash("sha256").update(version);
265
+ for (const input of inputs) hash.update(basename(input)).update(readFileSync(input));
266
+ const helper = join(dir, `${name}-${hash.digest("hex").slice(0, 16)}`);
267
+ if (existsSync(helper)) return helper;
268
+ mkdirSync(dir, {
269
+ recursive: true,
270
+ mode: 448
271
+ });
272
+ const output = `${helper}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
273
+ try {
274
+ await compile(output);
275
+ renameSync(output, helper);
276
+ } finally {
277
+ rmSync(output, { force: true });
278
+ }
279
+ return helper;
280
+ }
281
+ //#endregion
282
+ //#region metro-bridge.ts
283
+ function port(value) {
284
+ if (!Number.isInteger(value) || value < 1 || value > 65535) throw new Error("Invalid Metro bridge port.");
285
+ }
286
+ function credential(secret) {
287
+ if (!/^[a-f0-9]{64}$/.test(secret)) throw new Error("A Metro bridge needs a 256-bit secret.");
288
+ return Buffer.from(`${secret}\n`, "ascii");
289
+ }
290
+ function trackedServer(accept) {
291
+ const sockets = /* @__PURE__ */ new Set();
292
+ let closing;
293
+ const track = (socket) => {
294
+ sockets.add(socket);
295
+ socket.once("close", () => sockets.delete(socket));
296
+ socket.on("error", () => socket.destroy());
297
+ };
298
+ const server = createServer((socket) => {
299
+ if (closing || sockets.size >= 128) return void socket.destroy();
300
+ track(socket);
301
+ accept(socket, track);
302
+ });
303
+ return {
304
+ server,
305
+ close: () => {
306
+ closing ??= new Promise((resolve, reject) => {
307
+ for (const socket of sockets) socket.destroy();
308
+ if (!server.listening) return resolve();
309
+ server.close((error) => error ? reject(error) : resolve());
310
+ });
311
+ return closing;
312
+ }
313
+ };
314
+ }
315
+ function forward(source, target) {
316
+ source.once("close", () => target.destroy());
317
+ target.once("close", () => source.destroy());
318
+ source.pipe(target).pipe(source);
319
+ }
320
+ /** Bind this gateway only to the client's own Tailscale address; `peer` is the pinned worker's literal address. */
321
+ function createMetroGateway({ metroPort, peer, secret }) {
322
+ port(metroPort);
323
+ if (!isIP(peer)) throw new Error("The Metro gateway requires a pinned peer address.");
324
+ const expected = credential(secret);
325
+ return trackedServer((socket, track) => {
326
+ if (socket.remoteAddress?.replace(/^::ffff:/, "") !== peer) return void socket.destroy();
327
+ const deadline = setTimeout(() => socket.destroy(), 5e3);
328
+ socket.once("close", () => clearTimeout(deadline));
329
+ let prefix = Buffer.alloc(0);
330
+ const authenticate = (chunk) => {
331
+ const needed = expected.length - prefix.length;
332
+ prefix = Buffer.concat([prefix, chunk.subarray(0, needed)]);
333
+ if (prefix.length < expected.length) return;
334
+ socket.removeListener("data", authenticate);
335
+ socket.pause();
336
+ if (!timingSafeEqual(prefix, expected)) return void socket.destroy();
337
+ clearTimeout(deadline);
338
+ const upstream = connect({
339
+ host: "127.0.0.1",
340
+ port: metroPort
341
+ });
342
+ track(upstream);
343
+ if (chunk.length > needed) socket.unshift(chunk.subarray(needed));
344
+ forward(socket, upstream);
345
+ };
346
+ socket.on("data", authenticate);
347
+ });
348
+ }
349
+ /** Bind this endpoint only to worker loopback; `peer` comes from the approved client's authenticated connection. */
350
+ function createMetroBridge({ gatewayPort, peer, secret }) {
351
+ port(gatewayPort);
352
+ if (!isIP(peer)) throw new Error("The Metro bridge requires an authenticated peer address.");
353
+ const prefix = credential(secret);
354
+ return trackedServer((socket, track) => {
355
+ socket.pause();
356
+ const upstream = connect({
357
+ host: peer,
358
+ port: gatewayPort,
359
+ timeout: 5e3
360
+ });
361
+ track(upstream);
362
+ upstream.once("timeout", () => upstream.destroy());
363
+ upstream.once("close", () => socket.destroy());
364
+ upstream.once("connect", () => {
365
+ upstream.setTimeout(0);
366
+ upstream.write(prefix);
367
+ forward(socket, upstream);
368
+ });
369
+ socket.once("close", () => upstream.destroy());
370
+ });
371
+ }
372
+ //#endregion
250
373
  //#region index.ts
251
374
  const warnedRelative = /* @__PURE__ */ new Set();
252
375
  function absoluteEnvPath(name) {
@@ -353,14 +476,19 @@ function buildTarget(options) {
353
476
  if (SIMULATOR_UDID.test(name) || EMULATOR_SERIAL.test(name)) return "sim";
354
477
  return `on-${slug(name)}`;
355
478
  }
479
+ /** The simulator slice a local build compiles for this machine: `arm64` or `x86_64`. */
480
+ function hostSimulatorArch(arch = process.arch) {
481
+ return arch === "x64" ? "x86_64" : "arm64";
482
+ }
356
483
  function buildCacheKey(platform, fingerprintHash, options = {}) {
357
484
  const opts = options && typeof options === "object" ? options : {};
358
- const abi = platform === "android" && typeof opts.abi === "string" ? slug(opts.abi) : "";
485
+ const target = buildTarget(opts);
486
+ const abi = platform === "android" && typeof opts.abi === "string" ? slug(opts.abi) : platform === "ios" && target === "sim" && typeof opts.arch === "string" ? slug(opts.arch) : "";
359
487
  const allArch = platform === "android" && opts.allArch === true ? "-all-arch" : "";
360
488
  const compiler = typeof opts.compiler === "string" ? slug(opts.compiler) : "";
361
489
  const profile = typeof opts.buildProfile === "string" ? slug(opts.buildProfile) : "";
362
490
  const scheme = platform === "ios" && typeof opts.scheme === "string" && opts.scheme ? createHash("sha256").update(opts.scheme).digest("hex") : "";
363
- return `${fingerprintHash}-${buildVariant(platform, opts)}-${buildTarget(opts)}${abi ? `-${abi}` : ""}${allArch}${compiler ? `-${compiler}` : ""}${profile ? `-${profile}` : ""}${scheme ? `-scheme-${scheme}` : ""}`;
491
+ return `${fingerprintHash}-${buildVariant(platform, opts)}-${target}${abi ? `-${abi}` : ""}${allArch}${compiler ? `-${compiler}` : ""}${profile ? `-${profile}` : ""}${scheme ? `-scheme-${scheme}` : ""}`;
364
492
  }
365
493
  const METRO_NAMED_CACHE_LAYOUT = "metro-named-v1";
366
494
  function cacheManifestLockPath(file) {
@@ -426,4 +554,4 @@ function matchesCache(cache, match) {
426
554
  return match.layout === void 0 || cache.layout === match.layout;
427
555
  }
428
556
  //#endregion
429
- export { artifactIn as C, withDirLock as E, workspaceStateDir as S, storeArtifact as T, updateCacheManifest as _, buildCacheRoot as a, workspaceName as b, cachePathSetting as c, readCacheManifest as d, registerCache as f, tagSharedStore as g, sharedStoreRoot as h, buildCacheKey as i, configDir as l, rotatedLogPath as m, METRO_NAMED_CACHE_LAYOUT as n, cacheManifestLockPath as o, rotateLog as p, STORE_ROOT_TAG as r, cacheNameSegment as s, LOG_ROTATE_BYTES as t, metroCacheRoot as u, workspaceId as v, resolveArtifact as w, workspaceSlug as x, workspaceLogDir as y };
557
+ export { validMacosResourceDestination as A, workspaceStateDir as C, artifactIn as D, compiledHelper as E, resolveArtifact as O, workspaceSlug as S, createMetroGateway as T, tagSharedStore as _, buildCacheRoot as a, workspaceLogDir as b, cachePathSetting as c, metroCacheRoot as d, readCacheManifest as f, sharedStoreRoot as g, rotatedLogPath as h, buildCacheKey as i, withDirLock as j, storeArtifact as k, configDir as l, rotateLog as m, METRO_NAMED_CACHE_LAYOUT as n, cacheManifestLockPath as o, registerCache as p, STORE_ROOT_TAG as r, cacheNameSegment as s, LOG_ROTATE_BYTES as t, hostSimulatorArch as u, updateCacheManifest as v, createMetroBridge as w, workspaceName as x, workspaceId as y };