@camstack/server 1.2.277 → 1.2.278

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.
@@ -66,6 +66,7 @@ const path = __importStar(require("node:path"));
66
66
  const system_1 = require("@camstack/system");
67
67
  const types_1 = require("@camstack/types");
68
68
  const system_ensure_prebuilds_js_1 = require("../core/server-update/system-ensure-prebuilds.js");
69
+ const system_apply_closure_patches_js_1 = require("../core/server-update/system-apply-closure-patches.js");
69
70
  const system_exec_npm_js_1 = require("../core/server-update/system-exec-npm.js");
70
71
  const update_availability_emitter_js_1 = require("../core/update-availability-emitter.js");
71
72
  const update_availability_store_js_1 = require("../core/update-availability-store.js");
@@ -194,6 +195,11 @@ class AgentUpdateService extends index_js_1.RootUpdateService {
194
195
  // ships the identical bare Node, so its `applyServerUpdate` staging would
195
196
  // hit the same source-only `better-sqlite3` miss without this.
196
197
  ensureNativePrebuilds: options.ensureNativePrebuilds ?? (0, system_ensure_prebuilds_js_1.buildSystemEnsureNativePrebuilds)(options.logger),
198
+ // Apply the closure patches ourselves. npm 12 blocks the root package's
199
+ // postinstall by default, and a silently-unpatched closure is refused by
200
+ // the validator — an update that can never apply. Idempotent, so this
201
+ // costs nothing where npm did run the lifecycle.
202
+ applyClosurePatches: options.applyClosurePatches ?? (0, system_apply_closure_patches_js_1.buildSystemApplyClosurePatches)(options.logger),
197
203
  env: options.env,
198
204
  now: options.now,
199
205
  });
@@ -61,7 +61,7 @@
61
61
  * over the window (D245).
62
62
  */
63
63
  Object.defineProperty(exports, "__esModule", { value: true });
64
- exports.LoggingSettingsService = exports.MAX_CHANNEL_WINDOW_MS = exports.EMPTY_LOG_CHANNEL_PLANE = exports.HUB_NODE_ID = exports.LOGGING_SETTINGS_KEY = void 0;
64
+ exports.LoggingSettingsService = exports.MAX_CHANNEL_WINDOW_MS = exports.EMPTY_LOG_CHANNEL_PLANE = exports.OPS_LOG_RETENTION_DEFAULT_DAYS = exports.HUB_NODE_ID = exports.LOGGING_SETTINGS_KEY = void 0;
65
65
  exports.buildLogChannelPlane = buildLogChannelPlane;
66
66
  exports.toLoggingSettingsRecord = toLoggingSettingsRecord;
67
67
  exports.resolveLevel = resolveLevel;
@@ -79,6 +79,22 @@ const SYSTEM_SETTINGS_COLLECTION = 'system-settings';
79
79
  /** The node a hub speaks for when a caller does not name one. */
80
80
  exports.HUB_NODE_ID = 'hub';
81
81
  const LOG_LEVELS = ['debug', 'info', 'warn', 'error'];
82
+ /**
83
+ * The persisted row. Every field optional — an older row must still read, and
84
+ * a field this version does not know about must survive a write by a version
85
+ * that does not know it either.
86
+ */
87
+ /**
88
+ * How long the analytics ops log is kept when nothing is configured.
89
+ *
90
+ * Thirty days, the number that was hardcoded in `pipeline-analytics/index.ts`
91
+ * until D485. It is longer than any device retention window on purpose:
92
+ * `retention-model.ts` calls it "the one root that must NOT follow a device
93
+ * window … the audit trail is not history of the camera, it is history of the
94
+ * sweep". That reasoning is why the value stays 30 by default; it was never a
95
+ * reason for the value to be unreachable.
96
+ */
97
+ exports.OPS_LOG_RETENTION_DEFAULT_DAYS = 30;
82
98
  /** A plane that reaches nothing. The shape of a hub with no addon booted yet. */
83
99
  exports.EMPTY_LOG_CHANNEL_PLANE = {
84
100
  declarations: async () => [],
@@ -184,6 +200,12 @@ class LoggingSettingsService {
184
200
  if (levelChanged) {
185
201
  next = mergeLevel(next, scope, component, patch.level ?? null, this.now());
186
202
  }
203
+ if (patch.opsLogRetentionDays !== undefined) {
204
+ // A scalar, merged like any other field. Absent leaves it alone — the
205
+ // patch contract: arming a channel must not silently rewrite how long the
206
+ // audit trail lives.
207
+ next = { ...next, opsLogRetentionDays: patch.opsLogRetentionDays, updatedAt: this.now() };
208
+ }
187
209
  if (channelsChanged) {
188
210
  // Validated BEFORE anything is written: a patch that names an
189
211
  // undeclared channel, or narrows a channel whose lines carry no
@@ -285,6 +307,7 @@ class LoggingSettingsService {
285
307
  activeWindows: await this.describeWindows(),
286
308
  channels: await this.declaredChannels(),
287
309
  activeChannels: describeChannelWindows(record ?? {}, this.now()),
310
+ opsLogRetentionDays: record?.opsLogRetentionDays ?? exports.OPS_LOG_RETENTION_DEFAULT_DAYS,
288
311
  persisted: record !== null,
289
312
  };
290
313
  }
@@ -74,10 +74,13 @@
74
74
  "and no forceTurn guard.",
75
75
  "",
76
76
  "It now reaches production because `@camstack/server` ships these patches",
77
- "in `dist/closure-patches/` and applies them from its own `postinstall`,",
78
- "which runs inside the staging closure (the closure install passes no",
79
- "`--ignore-scripts`). A closure where a marker did not land is refused by",
80
- "`findUnappliedClosurePatches` before it can be swapped in.",
77
+ "in `dist/closure-patches/` and applies them from its own `postinstall`.",
78
+ "That alone was NOT enough (D484): npm 12 blocks a dependency's install",
79
+ "scripts by default, so on the Mac-mini agent the postinstall never ran and",
80
+ "the closure came out unpatched with exit 0. `stageClosure` now also",
81
+ "invokes the applier directly, and the staging manifest allow-lists the",
82
+ "packages whose scripts must run. A closure where a marker did not land is",
83
+ "still refused by `findUnappliedClosurePatches` before it can be swapped in.",
81
84
  "",
82
85
  "The version is EXACT, in this file and in all three declaring packages.",
83
86
  "A caret would let npm resolve 0.22.10 into the closure, the patch would",
@@ -55,6 +55,7 @@ const path = __importStar(require("node:path"));
55
55
  const index_js_1 = require("../../server-root/index.js");
56
56
  const update_availability_emitter_js_1 = require("../update-availability-emitter.js");
57
57
  const system_ensure_prebuilds_js_1 = require("./system-ensure-prebuilds.js");
58
+ const system_apply_closure_patches_js_1 = require("./system-apply-closure-patches.js");
58
59
  const system_exec_npm_js_1 = require("./system-exec-npm.js");
59
60
  class ServerUpdateService extends index_js_1.RootUpdateService {
60
61
  updateAvailability;
@@ -95,6 +96,11 @@ class ServerUpdateService extends index_js_1.RootUpdateService {
95
96
  // native-prebuild validator refuses the swap (toolchain-less packaged
96
97
  // desktop). No-op on docker where the install already carried it.
97
98
  ensureNativePrebuilds: options.ensureNativePrebuilds ?? (0, system_ensure_prebuilds_js_1.buildSystemEnsureNativePrebuilds)(options.logger),
99
+ // Apply the closure patches ourselves. npm 12 blocks the root package's
100
+ // postinstall by default, and a silently-unpatched closure is refused by
101
+ // the validator — an update that can never apply. Idempotent, so this
102
+ // costs nothing where npm did run the lifecycle.
103
+ applyClosurePatches: options.applyClosurePatches ?? (0, system_apply_closure_patches_js_1.buildSystemApplyClosurePatches)(options.logger),
98
104
  env: options.env,
99
105
  now: options.now,
100
106
  });
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.buildSystemApplyClosurePatches = buildSystemApplyClosurePatches;
37
+ /**
38
+ * `buildSystemApplyClosurePatches` — run the root package's OWN shipped patch
39
+ * applier inside a staged closure, without going through npm.
40
+ *
41
+ * ## Why this exists
42
+ *
43
+ * `@camstack/server` applies its `closure-postinstall` patches (D478 — werift)
44
+ * from its `postinstall`. npm 12 blocks a dependency's install scripts unless
45
+ * the installing manifest allow-lists it, and the block is a WARNING, not an
46
+ * error: the install succeeds, the patch silently does not land, and the only
47
+ * thing that notices is `findUnappliedClosurePatches` refusing the swap.
48
+ *
49
+ * Measured 2026-09-13 on the Mac-mini agent, which had bootstrapped npm 12.0.1
50
+ * while hub and little-unraid still ran npm 11. That node sat two versions
51
+ * behind with no other symptom, and the applier — run by hand in the very same
52
+ * staging closure — reported `APPLIED werift@0.22.9` on three files. The code
53
+ * was never wrong; it was never called.
54
+ *
55
+ * `CLOSURE_ALLOW_SCRIPTS` in `root-update-service.ts` un-blocks the lifecycle
56
+ * again, and that is the right fix for `better-sqlite3` and `node-av`, which
57
+ * genuinely need npm to fetch or build something. It is NOT the right fix for a
58
+ * patch of ours: making our own patch depend on an npm policy is the dependency
59
+ * D478 was written to remove. So this runs unconditionally and in addition.
60
+ *
61
+ * Safe to run twice: the applier is idempotent by construction — every hunk
62
+ * recognises its own output, so an already-patched file is skipped rather than
63
+ * patched again.
64
+ *
65
+ * ## Which Node
66
+ *
67
+ * `process.execPath` — the one already running. A packaged desktop ships a bare
68
+ * bundled Node and has no `node` on PATH at all (the Mac agent's process runs
69
+ * with `PATH=/usr/bin:/bin:/usr/sbin:/sbin`), so resolving `node` by name would
70
+ * ENOENT on exactly the host this was written for.
71
+ */
72
+ const node_child_process_1 = require("node:child_process");
73
+ const fs = __importStar(require("node:fs"));
74
+ const path = __importStar(require("node:path"));
75
+ const node_util_1 = require("node:util");
76
+ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
77
+ /** Where the published root package keeps its applier, inside a closure. */
78
+ const APPLIER_REL = ['node_modules', '@camstack', 'server', 'scripts', 'closure-postinstall.mjs'];
79
+ /** A patch pass must never be the slow part of an update. */
80
+ const APPLY_TIMEOUT_MS = 60_000;
81
+ function buildSystemApplyClosurePatches(logger) {
82
+ return async (closureDir) => {
83
+ const applier = path.join(closureDir, ...APPLIER_REL);
84
+ if (!fs.existsSync(applier)) {
85
+ // A closure from before D478, or a root package that ships no patches.
86
+ // Nothing to apply is not a failure — and the patch VALIDATOR, not this,
87
+ // is what decides whether a missing patch matters.
88
+ logger.debug('closure ships no patch applier — nothing to apply', {
89
+ meta: { applier },
90
+ });
91
+ return;
92
+ }
93
+ const { stdout, stderr } = await execFileAsync(process.execPath, [applier], {
94
+ cwd: path.dirname(path.dirname(applier)),
95
+ timeout: APPLY_TIMEOUT_MS,
96
+ });
97
+ // The applier names every file it touched and every one it refused. Those
98
+ // lines are the only record that the patch pass happened at all, so they
99
+ // are forwarded rather than swallowed.
100
+ const output = `${stdout}${stderr}`.trim();
101
+ if (output.length > 0) {
102
+ logger.info('closure patches applied', { meta: { output } });
103
+ }
104
+ };
105
+ }
@@ -1071,6 +1071,15 @@ var require_dist = __commonJS({
1071
1071
  __name(buildNpmRegistryArgs, "buildNpmRegistryArgs");
1072
1072
  var NPM_VIEW_TIMEOUT_MS = 2e4;
1073
1073
  var NPM_INSTALL_TIMEOUT_MS = 15 * 6e4;
1074
+ var CLOSURE_ALLOW_SCRIPTS = {
1075
+ "@camstack/server": true,
1076
+ "better-sqlite3": true,
1077
+ "node-av": true,
1078
+ "node-pty": true,
1079
+ ssh2: true,
1080
+ "cpu-features": true,
1081
+ esbuild: true
1082
+ };
1074
1083
  var RESTART_REASON_PREFIX = "server-update";
1075
1084
  var STALE_TRANSIENT_MS = 24 * 60 * 60 * 1e3;
1076
1085
  function readWrapperProbes() {
@@ -1118,6 +1127,7 @@ var require_dist = __commonJS({
1118
1127
  dataDir;
1119
1128
  execNpm;
1120
1129
  ensureNativePrebuildsFn;
1130
+ applyClosurePatchesFn;
1121
1131
  env;
1122
1132
  readWrapperProbesFn;
1123
1133
  now;
@@ -1146,6 +1156,7 @@ var require_dist = __commonJS({
1146
1156
  };
1147
1157
  });
1148
1158
  this.ensureNativePrebuildsFn = options.ensureNativePrebuilds ?? (async () => void 0);
1159
+ this.applyClosurePatchesFn = options.applyClosurePatches ?? (async () => void 0);
1149
1160
  this.env = options.env ?? process.env;
1150
1161
  this.readWrapperProbesFn = options.readWrapperProbes ?? readWrapperProbes;
1151
1162
  this.now = options.now ?? Date.now;
@@ -1461,7 +1472,8 @@ var require_dist = __commonJS({
1461
1472
  } else {
1462
1473
  fs6.writeFileSync(path6.join(stagingDir, "package.json"), JSON.stringify({
1463
1474
  name: "camstack-node-root",
1464
- private: true
1475
+ private: true,
1476
+ allowScripts: CLOSURE_ALLOW_SCRIPTS
1465
1477
  }, null, 2), "utf-8");
1466
1478
  await this.execNpm([
1467
1479
  ...installArgs,
@@ -1494,6 +1506,15 @@ var require_dist = __commonJS({
1494
1506
  if (missingNatives.length > 0) {
1495
1507
  throw new Error(`staged closure missing native prebuilds: ${missingNatives.join(", ")} \u2014 refusing to arm a swap that would break the forked runners`);
1496
1508
  }
1509
+ try {
1510
+ await this.applyClosurePatchesFn(stagingDir);
1511
+ } catch (err) {
1512
+ this.logger.warn("closure-patch apply step failed \u2014 validator will decide", {
1513
+ meta: {
1514
+ error: err instanceof Error ? err.message : String(err)
1515
+ }
1516
+ });
1517
+ }
1497
1518
  const unpatched = findUnappliedClosurePatches(stagingDir);
1498
1519
  if (unpatched.length > 0) {
1499
1520
  throw new Error(`staged closure did not apply its shipped patches: ${unpatched.join(", ")} \u2014 refusing to arm a swap into an unpatched closure`);
@@ -1541,6 +1562,7 @@ var require_dist = __commonJS({
1541
1562
  return {
1542
1563
  name: "camstack-node-root",
1543
1564
  private: true,
1565
+ allowScripts: CLOSURE_ALLOW_SCRIPTS,
1544
1566
  dependencies: {
1545
1567
  [this.spec.packageName]: fileRef(rootTgz)
1546
1568
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.277",
3
+ "version": "1.2.278",
4
4
  "private": false,
5
5
  "files": [
6
6
  "dist",
@@ -35,18 +35,18 @@
35
35
  ]
36
36
  },
37
37
  "dependencies": {
38
- "@camstack/addon-admin-ui": "*",
39
- "@camstack/addon-agent-ui": "*",
40
- "@camstack/addon-auth": "*",
41
- "@camstack/addon-decoder-nodeav": "*",
42
- "@camstack/addon-notifiers": "*",
43
- "@camstack/addon-pipeline": "*",
44
- "@camstack/addon-pipeline-orchestrator": "*",
45
- "@camstack/addon-post-analysis": "*",
46
- "@camstack/sdk": "*",
47
- "@camstack/system": "*",
48
- "@camstack/types": "*",
49
- "@camstack/ui-library": "*",
38
+ "@camstack/addon-admin-ui": "1.2.187",
39
+ "@camstack/addon-agent-ui": "1.2.105",
40
+ "@camstack/addon-auth": "1.2.107",
41
+ "@camstack/addon-decoder-nodeav": "1.2.100",
42
+ "@camstack/addon-notifiers": "1.2.104",
43
+ "@camstack/addon-pipeline": "1.2.227",
44
+ "@camstack/addon-pipeline-orchestrator": "1.2.190",
45
+ "@camstack/addon-post-analysis": "1.2.228",
46
+ "@camstack/sdk": "1.2.103",
47
+ "@camstack/system": "1.2.227",
48
+ "@camstack/types": "1.2.184",
49
+ "@camstack/ui-library": "1.2.151",
50
50
  "@fastify/compress": "^9.0.0",
51
51
  "@fastify/cookie": "^11.0.2",
52
52
  "@fastify/cors": "^11.2.0",
@@ -13,8 +13,8 @@
13
13
  * arrives from npm unpatched. Production ran it unpatched from the day the
14
14
  * patch was written until 2026-09-12.
15
15
  *
16
- * The closure is an `npm install` into a staging dir and it passes no
17
- * `--ignore-scripts`, so this package's own `postinstall` is the one place that
16
+ * The closure is an `npm install` into a staging dir, so this package's own
17
+ * `postinstall` is the natural place to do it — it is the one place that
18
18
  * (a) runs inside the closure, (b) runs with the NEW release's patch files,
19
19
  * and (c) needs no new dependency, no diff library and no change to the update
20
20
  * service. Every node gets it identically — hub, Docker agent, Electron agent
@@ -29,6 +29,15 @@
29
29
  * must never leave a package half-patched — every hunk is applied to an
30
30
  * in-memory copy and the file is written once, or not at all.
31
31
  *
32
+ * ## It is no longer the ONLY place, and must not be (D484)
33
+ *
34
+ * "the install passes no `--ignore-scripts`" was true and was the wrong
35
+ * question. npm 12 blocks a dependency's install scripts WITHOUT the flag, so
36
+ * on the Mac-mini agent this script was simply never called and the closure
37
+ * came out unpatched with exit 0. `stageClosure` therefore also invokes this
38
+ * applier directly, and the idempotence below is what makes running twice
39
+ * harmless rather than a double patch.
40
+ *
32
41
  * The FAIL-CLOSED half lives elsewhere, in `@camstack/node-root`'s
33
42
  * `findUnappliedClosurePatches`, which refuses to swap into a closure whose
34
43
  * markers are absent. Silence here is therefore never silence overall: either
@@ -1,106 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.purgeRequireCacheUnder = purgeRequireCacheUnder;
7
- exports.bundleGraphRootFor = bundleGraphRootFor;
8
- /**
9
- * require-cache — evicting an addon's own modules so a hot-updated bundle
10
- * is actually re-read.
11
- *
12
- * Lives in its own file because the behaviour it encodes is a Node semantic
13
- * that cannot be observed from inside the test runner: the runner transforms
14
- * modules itself, so only a real `node` process re-executing a real CommonJS
15
- * file proves anything. `require-cache.spec.ts` spawns one.
16
- */
17
- const node_fs_1 = __importDefault(require("node:fs"));
18
- const node_path_1 = __importDefault(require("node:path"));
19
- /**
20
- * Drop every `require`-cached module that lives under `dir`.
21
- *
22
- * Node's ESM loader serves a CommonJS module from the require cache, keyed by
23
- * the resolved filename — the `?t=` query an ESM import uses to force a re-read
24
- * is stripped before that lookup and therefore does nothing. Since every addon
25
- * bundle here is CJS, evicting the addon's own entries is what makes a
26
- * hot-updated bundle actually load.
27
- *
28
- * Matched against the REAL path as well as the given one. Node keys the cache
29
- * by the resolved-and-symlink-followed filename, while an addon directory is
30
- * assembled from the configured `dataDir` and never realpath'd — so a single
31
- * symlink anywhere above it (a `dataDir` under macOS's `/tmp`, a relocated data
32
- * root) makes a prefix match on the given path miss every entry and this
33
- * function silently do nothing. Which is precisely the failure it exists to
34
- * fix, wearing its clothes: the catalog would look refreshed and be stale.
35
- *
36
- * Scoped to one directory on purpose: a blanket cache clear would evict the
37
- * hub's own modules and every other addon's, turning a catalog refresh into a
38
- * process-wide reload. Best-effort — a cache that cannot be walked leaves the
39
- * previous (stale) behaviour rather than failing the caller.
40
- *
41
- * **The directory must be the whole BUNDLE, never the entry's own folder** —
42
- * see {@link bundleGraphRootFor}. A narrower scope purges half a module graph,
43
- * which is worse than purging none of it.
44
- */
45
- function purgeRequireCacheUnder(dir) {
46
- try {
47
- const cache = require.cache;
48
- if (cache === undefined)
49
- return;
50
- const prefixes = candidatePrefixes(dir);
51
- for (const key of Object.keys(cache)) {
52
- if (prefixes.some((prefix) => key.startsWith(prefix)))
53
- delete cache[key];
54
- }
55
- }
56
- catch {
57
- // Non-CJS host, or a frozen cache. The caller's import still runs.
58
- }
59
- }
60
- /**
61
- * The directory that holds one addon bundle's WHOLE module graph.
62
- *
63
- * A multi-entry bundle does not live under its entry's folder. Every addon in
64
- * this repo builds through `tools/build/vite-lib.preset.ts`, whose entries are
65
- * `dist/<addon>/index.js` — and whose SHARED CHUNKS (`dist/dist-<hash>.js`,
66
- * `dist/node-<hash>.js`, …) sit one level up, in `dist/`. Purging
67
- * `dirname(entryPath)` therefore evicts the entry and leaves the chunks, so the
68
- * re-import RE-EXECUTES the entry against module state that was never reset.
69
- *
70
- * That is not a stale-cache bug, it is a corrupted one, and it took the hub's
71
- * `stream-broker` down on 2026-08-29: the entry chunk carries
72
- * `log-channels.ts`'s module-scope `declareLogChannel` calls while the
73
- * `LogChannelRegistry` singleton they write to is inlined into the shared
74
- * chunk. Half the graph re-ran, the registry did not, and the second
75
- * declaration threw `log channel "stream-broker.webrtc" is already declared in
76
- * this process` — a message that is correct, and that named the loader rather
77
- * than the addon. `dist/` is the unit the bundler emits, so `dist/` is the unit
78
- * the cache is purged in.
79
- *
80
- * `node_modules/` under the addon is deliberately OUTSIDE this root: a package
81
- * that declares `camstack.nativeDependencies` installs `.node` bindings there,
82
- * and re-`dlopen`ing a native addon is not a refresh, it is a coin flip.
83
- *
84
- * Falls back to the entry's directory when there is no `dist/` above it (a
85
- * loose `loadFromPath` module in a test or a dev tree) — the historical
86
- * behaviour, for the shape that has no bundle.
87
- */
88
- function bundleGraphRootFor(addonDir, entryPath) {
89
- const distDir = node_path_1.default.resolve(addonDir, 'dist');
90
- const withSep = distDir.endsWith(node_path_1.default.sep) ? distDir : `${distDir}${node_path_1.default.sep}`;
91
- return entryPath.startsWith(withSep) ? distDir : node_path_1.default.dirname(entryPath);
92
- }
93
- /** The directory as given and as Node sees it, both as trailing-separator prefixes. */
94
- function candidatePrefixes(dir) {
95
- const withSep = (d) => (d.endsWith(node_path_1.default.sep) ? d : `${d}${node_path_1.default.sep}`);
96
- const given = withSep(dir);
97
- let real = given;
98
- try {
99
- real = withSep(node_fs_1.default.realpathSync(dir));
100
- }
101
- catch {
102
- // Directory gone between the caller's existsSync and here — `given` alone
103
- // is still a correct (if narrower) match.
104
- }
105
- return real === given ? [given] : [given, real];
106
- }