@camstack/server 1.2.36 → 1.2.38

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/dist/launcher.js CHANGED
@@ -56,6 +56,7 @@ const path = __importStar(require("node:path"));
56
56
  const tar = __importStar(require("tar"));
57
57
  const yaml = __importStar(require("js-yaml"));
58
58
  const framework_nodepath_js_1 = require("./framework-nodepath.js");
59
+ const package_inventory_js_1 = require("./package-inventory.js");
59
60
  const bootstrap_packages_js_1 = require("./bootstrap-packages.js");
60
61
  /** Path of the manifest file embedded inside every archive. */
61
62
  const ARCHIVE_MANIFEST_NAME = '.camstack-backup-manifest.json';
@@ -435,6 +436,64 @@ async function launch() {
435
436
  console.warn(`[launcher] could NOT resolve ${pkg}: ${err instanceof Error ? err.message : String(err)}`);
436
437
  }
437
438
  }
439
+ // WHAT ELSE could have run? (D45 enforcement step 2)
440
+ //
441
+ // The line above says which copy this boot resolved. It cannot say what else
442
+ // is lying around, and that is the half that produced three wrong diagnoses:
443
+ // one agent held FOUR copies of @camstack/system (1.2.28 / 1.2.28 / 1.2.0 /
444
+ // 1.2.30), so every version the node reported was true of a copy nobody was
445
+ // running.
446
+ //
447
+ // Reported, not fatal. Every node in the fleet currently HAS extra copies —
448
+ // the image seeds under /opt and the legacy /data/framework — and D45 records
449
+ // their removal as a deliberate, scheduled migration with a container
450
+ // recreate as the escape hatch, not something a routine boot should enact by
451
+ // refusing to start. `CAMSTACK_INVENTORY_STRICT=1` makes it fatal, which is
452
+ // how a layout that HAS been cleaned keeps itself clean.
453
+ try {
454
+ const inventory = (0, package_inventory_js_1.inventoryHostProvided)((0, package_inventory_js_1.hostProvidedSearchRoots)({
455
+ nodePath: process.env['NODE_PATH'],
456
+ pathSeparator: process.platform === 'win32' ? ';' : ':',
457
+ dataDir: DATA_DIR,
458
+ serverNodeModules: nodeModulesDir,
459
+ seedRoots: (0, package_inventory_js_1.seedNodeModulesRoots)(fs),
460
+ }), {
461
+ exists: (dir) => fs.existsSync(path.join(dir, 'package.json')),
462
+ readVersion: (dir) => {
463
+ try {
464
+ const raw = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8'));
465
+ const version = typeof raw === 'object' && raw !== null
466
+ ? raw.version
467
+ : undefined;
468
+ return typeof version === 'string' ? version : null;
469
+ }
470
+ catch {
471
+ return null;
472
+ }
473
+ },
474
+ realPath: (dir) => {
475
+ try {
476
+ return fs.realpathSync(dir);
477
+ }
478
+ catch {
479
+ return dir;
480
+ }
481
+ },
482
+ });
483
+ const report = (0, package_inventory_js_1.formatInventoryReport)(inventory);
484
+ if (report !== '') {
485
+ console.error(`[launcher] ${report}`);
486
+ if (process.env['CAMSTACK_INVENTORY_STRICT'] === '1') {
487
+ console.error('[launcher] CAMSTACK_INVENTORY_STRICT=1 — refusing to boot on this layout');
488
+ process.exit(1);
489
+ }
490
+ }
491
+ }
492
+ catch (err) {
493
+ // The inventory is diagnostics. It must never be the reason a node fails
494
+ // to boot when it is the thing that was supposed to explain failures.
495
+ console.warn(`[launcher] package inventory failed: ${err instanceof Error ? err.message : String(err)}`);
496
+ }
438
497
  // Now safe to load the role's runtime entry (both have static imports from
439
498
  // @camstack/system, resolved only after the framework dir + NODE_PATH setup
440
499
  // above). The agent boots from THIS same @camstack/server closure — there is
@@ -0,0 +1,180 @@
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.HOST_PROVIDED_PACKAGES = void 0;
37
+ exports.hostProvidedSearchRoots = hostProvidedSearchRoots;
38
+ exports.seedNodeModulesRoots = seedNodeModulesRoots;
39
+ exports.inventoryHostProvided = inventoryHostProvided;
40
+ exports.formatInventoryReport = formatInventoryReport;
41
+ /**
42
+ * Boot-time inventory of host-provided packages — enforcement step 2 of
43
+ * [D45](../../../docs/decisions/adr-0045.md).
44
+ *
45
+ * D45 requires exactly ONE copy of every host-provided package per node, inside
46
+ * the active server closure. The record exists because that was not true: one
47
+ * agent held four copies of `@camstack/system` — 1.2.28 under the image seed,
48
+ * 1.2.28 under a second seed tree, 1.2.0 in the legacy `/data/framework`, and
49
+ * 1.2.30 in the closure — and each could be truthfully quoted as "the version".
50
+ * Three consecutive diagnoses of which one actually ran were each disproved by
51
+ * the next measurement.
52
+ *
53
+ * Step 1 (the launcher naming the copy it RESOLVED) answers "which one runs".
54
+ * This answers the other half — "what else is lying around that could have
55
+ * run" — which is the question a resolved path alone cannot.
56
+ *
57
+ * Everything here is pure: the caller injects the three filesystem operations,
58
+ * so the whole inventory is testable against a described layout rather than a
59
+ * real disk.
60
+ */
61
+ const path = __importStar(require("node:path"));
62
+ /**
63
+ * Packages the closure provides to everything on the node. Addons never bundle
64
+ * these ([D15](../../../docs/decisions/adr-0015.md)); a second copy on disk is
65
+ * a resolution race waiting for a boot order to change.
66
+ */
67
+ exports.HOST_PROVIDED_PACKAGES = [
68
+ '@camstack/system',
69
+ '@camstack/types',
70
+ '@camstack/sdk',
71
+ ];
72
+ /**
73
+ * Every directory under which a host-provided package could sit as
74
+ * `<root>/<pkgName>`.
75
+ *
76
+ * The list is explicit rather than a filesystem walk: these are the places a
77
+ * copy has actually turned up, each named in D45, and a bounded list keeps this
78
+ * to a handful of stat calls at boot. `/data/addons` is a root in its own right
79
+ * because the bootstrap-install layout puts the package DIRECTLY there
80
+ * (`/data/addons/@camstack/system`), not under a `node_modules`.
81
+ */
82
+ function hostProvidedSearchRoots(input) {
83
+ const fromNodePath = (input.nodePath ?? '')
84
+ .split(input.pathSeparator)
85
+ .map((p) => p.trim())
86
+ .filter((p) => p.length > 0);
87
+ const roots = [
88
+ input.serverNodeModules,
89
+ ...fromNodePath,
90
+ path.join(input.dataDir, 'framework', 'node_modules'),
91
+ path.join(input.dataDir, 'addons'),
92
+ ...input.seedRoots,
93
+ ];
94
+ const seen = new Set();
95
+ return roots.filter((r) => {
96
+ if (seen.has(r))
97
+ return false;
98
+ seen.add(r);
99
+ return true;
100
+ });
101
+ }
102
+ /**
103
+ * The image seed trees: `/opt/<name>/node_modules`.
104
+ *
105
+ * Enumerated rather than hardcoded because their names have changed with the
106
+ * image layout (`camstack-hub`, `camstack-seed-framework`, …) and a hardcoded
107
+ * list would quietly stop finding the copy it was written for. Returns empty
108
+ * when `/opt` does not exist, which is every dev machine.
109
+ */
110
+ function seedNodeModulesRoots(fs, optDir = '/opt') {
111
+ if (!fs.existsSync(optDir))
112
+ return [];
113
+ try {
114
+ return fs
115
+ .readdirSync(optDir)
116
+ .map((name) => path.join(optDir, name, 'node_modules'))
117
+ .filter((dir) => fs.existsSync(dir));
118
+ }
119
+ catch {
120
+ return [];
121
+ }
122
+ }
123
+ /**
124
+ * Find every copy of every host-provided package under `roots`.
125
+ *
126
+ * A copy whose package.json will not parse is still RECORDED, with a null
127
+ * version. Dropping it would under-report the very thing this exists to
128
+ * count — and an unreadable copy is exactly the kind that has been sitting
129
+ * somewhere since an interrupted install.
130
+ */
131
+ function inventoryHostProvided(roots, fs, packages = exports.HOST_PROVIDED_PACKAGES) {
132
+ const copies = [];
133
+ for (const pkg of packages) {
134
+ // Keyed by canonical path: two roots that are the same directory (a
135
+ // symlinked closure, a repeated NODE_PATH entry) are ONE copy, and calling
136
+ // them two would cry wolf on a node that is actually compliant.
137
+ const seenReal = new Set();
138
+ for (const root of roots) {
139
+ const dir = path.join(root, pkg);
140
+ if (!fs.exists(dir))
141
+ continue;
142
+ const real = fs.realPath(dir);
143
+ if (seenReal.has(real))
144
+ continue;
145
+ seenReal.add(real);
146
+ copies.push({ pkg, path: dir, version: fs.readVersion(dir) });
147
+ }
148
+ }
149
+ const countByPkg = new Map();
150
+ for (const c of copies)
151
+ countByPkg.set(c.pkg, (countByPkg.get(c.pkg) ?? 0) + 1);
152
+ const duplicated = [...countByPkg.entries()]
153
+ .filter(([, n]) => n > 1)
154
+ .map(([pkg]) => pkg)
155
+ .sort();
156
+ return { copies, duplicated, rootsScanned: roots };
157
+ }
158
+ /**
159
+ * The loud report. Empty string when the node is clean — a compliant boot says
160
+ * nothing, so the one that isn't stands out instead of scrolling past.
161
+ */
162
+ function formatInventoryReport(result) {
163
+ if (result.duplicated.length === 0)
164
+ return '';
165
+ const lines = [
166
+ 'D45 violation — a host-provided package exists more than once on this node.',
167
+ 'Which copy runs is a module-resolution outcome, and every version this node',
168
+ 'reports is a claim about one of them rather than about the node.',
169
+ '',
170
+ ];
171
+ for (const pkg of result.duplicated) {
172
+ const found = result.copies.filter((c) => c.pkg === pkg);
173
+ lines.push(` ${pkg} — ${found.length} copies:`);
174
+ for (const c of found) {
175
+ lines.push(` ${c.version ?? '<unreadable package.json>'} ${c.path}`);
176
+ }
177
+ }
178
+ lines.push('', ` roots scanned: ${result.rootsScanned.join(', ')}`);
179
+ return lines.join('\n');
180
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.36",
3
+ "version": "1.2.38",
4
4
  "private": false,
5
5
  "files": [
6
6
  "dist",
@@ -35,16 +35,16 @@
35
35
  "dependencies": {
36
36
  "@camstack/addon-admin-ui": "1.2.22",
37
37
  "@camstack/addon-agent-ui": "1.2.10",
38
- "@camstack/addon-auth": "1.2.10",
38
+ "@camstack/addon-auth": "1.2.11",
39
39
  "@camstack/addon-decoder-nodeav": "1.2.9",
40
- "@camstack/addon-notifiers": "1.2.11",
40
+ "@camstack/addon-notifiers": "1.2.12",
41
41
  "@camstack/addon-pipeline": "1.2.39",
42
42
  "@camstack/addon-pipeline-orchestrator": "1.2.19",
43
43
  "@camstack/addon-post-analysis": "1.2.24",
44
44
  "@camstack/sdk": "1.2.9",
45
45
  "@camstack/shm-ring": "1.1.9",
46
- "@camstack/system": "1.2.31",
47
- "@camstack/types": "1.2.23",
46
+ "@camstack/system": "1.2.32",
47
+ "@camstack/types": "1.2.24",
48
48
  "@camstack/ui-library": "1.2.18",
49
49
  "@fastify/compress": "^9.0.0",
50
50
  "@fastify/cookie": "^11.0.2",