@camstack/agent 1.1.43 → 1.1.45

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/cli.js CHANGED
@@ -60,6 +60,9 @@ var require_dist = __commonJS({
60
60
  var src_exports = {};
61
61
  __export(src_exports, {
62
62
  AGENT_ROOT_SPEC: () => AGENT_ROOT_SPEC2,
63
+ DEV_UPLOADS_DIRNAME: () => DEV_UPLOADS_DIRNAME,
64
+ DEV_UPLOADS_KEEP_COUNT: () => DEV_UPLOADS_KEEP_COUNT,
65
+ DEV_UPLOAD_MANIFEST_FILE: () => DEV_UPLOAD_MANIFEST_FILE,
63
66
  HOST_EXTERNAL_SPECIFIERS: () => HOST_EXTERNAL_SPECIFIERS,
64
67
  HUB_ROOT_SPEC: () => HUB_ROOT_SPEC,
65
68
  RESTART_INTENT_FILE: () => RESTART_INTENT_FILE,
@@ -69,11 +72,17 @@ var require_dist = __commonJS({
69
72
  clearRestartIntentMarker: () => clearRestartIntentMarker,
70
73
  compareSemver: () => compareSemver,
71
74
  detectWorkspaceRoot: () => detectWorkspaceRoot,
75
+ devChannelEpoch: () => devChannelEpoch,
76
+ devUploadManifestPath: () => devUploadManifestPath,
77
+ devUploadVersionDir: () => devUploadVersionDir,
78
+ devUploadsDir: () => devUploadsDir,
72
79
  emptyServerRootState: () => emptyServerRootState,
80
+ isDevChannelVersion: () => isDevChannelVersion,
73
81
  isHostExternal: () => isHostExternal,
74
82
  isServerRootState: () => isServerRootState,
75
83
  minNodeMajorOf: () => minNodeMajorOf,
76
84
  planBoot: () => planBoot,
85
+ readDevUploadManifest: () => readDevUploadManifest,
77
86
  readRestartIntentMarker: () => readRestartIntentMarker,
78
87
  readServerRootState: () => readServerRootState,
79
88
  registerActiveRootResolver: () => registerActiveRootResolver,
@@ -86,10 +95,58 @@ var require_dist = __commonJS({
86
95
  validateVersionDir: () => validateVersionDir,
87
96
  versionDir: () => versionDir,
88
97
  versionsDir: () => versionsDir,
98
+ writeDevUploadManifest: () => writeDevUploadManifest,
89
99
  writeRestartIntentMarker: () => writeRestartIntentMarker2,
90
100
  writeServerRootState: () => writeServerRootState
91
101
  });
92
102
  module2.exports = __toCommonJS(src_exports);
103
+ var fs8 = __toESM2(require("fs"));
104
+ var path9 = __toESM2(require("path"));
105
+ var DEV_UPLOADS_DIRNAME = "dev-uploads";
106
+ var DEV_UPLOAD_MANIFEST_FILE = "manifest.json";
107
+ var DEV_UPLOADS_KEEP_COUNT = 3;
108
+ var DEV_CHANNEL_VERSION_RE = /-dev\.\d+$/;
109
+ function isDevChannelVersion(version) {
110
+ return DEV_CHANNEL_VERSION_RE.test(version);
111
+ }
112
+ function devChannelEpoch(version) {
113
+ const match = /-dev\.(\d+)$/.exec(version);
114
+ if (match === null) return null;
115
+ const epoch = Number.parseInt(match[1] ?? "", 10);
116
+ return Number.isNaN(epoch) ? null : epoch;
117
+ }
118
+ function devUploadsDir(rootDir) {
119
+ return path9.join(rootDir, DEV_UPLOADS_DIRNAME);
120
+ }
121
+ function devUploadVersionDir(rootDir, version) {
122
+ return path9.join(devUploadsDir(rootDir), version);
123
+ }
124
+ function devUploadManifestPath(versionDirPath) {
125
+ return path9.join(versionDirPath, DEV_UPLOAD_MANIFEST_FILE);
126
+ }
127
+ function isDevUploadManifest(v) {
128
+ if (typeof v !== "object" || v === null) return false;
129
+ const m = v;
130
+ if (typeof m["version"] !== "string") return false;
131
+ const packages = m["packages"];
132
+ if (typeof packages !== "object" || packages === null || Array.isArray(packages)) return false;
133
+ return Object.values(packages).every((filename) => typeof filename === "string");
134
+ }
135
+ function readDevUploadManifest(versionDirPath) {
136
+ try {
137
+ const raw = JSON.parse(fs8.readFileSync(devUploadManifestPath(versionDirPath), "utf-8"));
138
+ return isDevUploadManifest(raw) ? raw : null;
139
+ } catch {
140
+ return null;
141
+ }
142
+ }
143
+ function writeDevUploadManifest(versionDirPath, manifest) {
144
+ fs8.mkdirSync(versionDirPath, { recursive: true });
145
+ const target = devUploadManifestPath(versionDirPath);
146
+ const tmp = `${target}.tmp`;
147
+ fs8.writeFileSync(tmp, JSON.stringify(manifest, null, 2), "utf-8");
148
+ fs8.renameSync(tmp, target);
149
+ }
93
150
  var HUB_ROOT_SPEC = {
94
151
  packageName: "@camstack/server",
95
152
  entryRelPath: ["dist", "launcher.js"]
@@ -98,8 +155,8 @@ var require_dist = __commonJS({
98
155
  packageName: "@camstack/agent",
99
156
  entryRelPath: ["dist", "cli.js"]
100
157
  };
101
- var fs8 = __toESM2(require("fs"));
102
- var path9 = __toESM2(require("path"));
158
+ var fs22 = __toESM2(require("fs"));
159
+ var path22 = __toESM2(require("path"));
103
160
  var SERVER_ROOT_DIRNAME = "server-root";
104
161
  var SERVER_ROOT_STATE_FILE = "state.json";
105
162
  var RESTART_INTENT_FILE = ".restart-intent";
@@ -113,22 +170,22 @@ var require_dist = __commonJS({
113
170
  };
114
171
  }
115
172
  function serverRootDir2(dataDir) {
116
- return path9.join(dataDir, SERVER_ROOT_DIRNAME);
173
+ return path22.join(dataDir, SERVER_ROOT_DIRNAME);
117
174
  }
118
175
  function versionsDir(rootDir) {
119
- return path9.join(rootDir, "versions");
176
+ return path22.join(rootDir, "versions");
120
177
  }
121
178
  function versionDir(rootDir, version) {
122
- return path9.join(versionsDir(rootDir), version);
179
+ return path22.join(versionsDir(rootDir), version);
123
180
  }
124
181
  function rootPackageDir(versionDirPath, spec) {
125
- return path9.join(versionDirPath, "node_modules", ...spec.packageName.split("/"));
182
+ return path22.join(versionDirPath, "node_modules", ...spec.packageName.split("/"));
126
183
  }
127
184
  function rootEntryPath(versionDirPath, spec) {
128
- return path9.join(rootPackageDir(versionDirPath, spec), ...spec.entryRelPath);
185
+ return path22.join(rootPackageDir(versionDirPath, spec), ...spec.entryRelPath);
129
186
  }
130
187
  function stateFilePath(rootDir) {
131
- return path9.join(rootDir, SERVER_ROOT_STATE_FILE);
188
+ return path22.join(rootDir, SERVER_ROOT_STATE_FILE);
132
189
  }
133
190
  function isNullableString(v) {
134
191
  return v === null || typeof v === "string";
@@ -155,21 +212,21 @@ var require_dist = __commonJS({
155
212
  }
156
213
  function readServerRootState(rootDir) {
157
214
  try {
158
- const raw = JSON.parse(fs8.readFileSync(stateFilePath(rootDir), "utf-8"));
215
+ const raw = JSON.parse(fs22.readFileSync(stateFilePath(rootDir), "utf-8"));
159
216
  return isServerRootState(raw) ? raw : null;
160
217
  } catch {
161
218
  return null;
162
219
  }
163
220
  }
164
221
  function writeServerRootState(rootDir, state) {
165
- fs8.mkdirSync(rootDir, { recursive: true });
222
+ fs22.mkdirSync(rootDir, { recursive: true });
166
223
  const target = stateFilePath(rootDir);
167
224
  const tmp = `${target}.tmp`;
168
- fs8.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf-8");
169
- fs8.renameSync(tmp, target);
225
+ fs22.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf-8");
226
+ fs22.renameSync(tmp, target);
170
227
  }
171
228
  function restartIntentMarkerPath(rootDir) {
172
- return path9.join(rootDir, RESTART_INTENT_FILE);
229
+ return path22.join(rootDir, RESTART_INTENT_FILE);
173
230
  }
174
231
  function isRestartIntentMarker(v) {
175
232
  if (typeof v !== "object" || v === null) return false;
@@ -177,15 +234,15 @@ var require_dist = __commonJS({
177
234
  return typeof m["requestedAtMs"] === "number" && typeof m["reason"] === "string";
178
235
  }
179
236
  function writeRestartIntentMarker2(rootDir, marker) {
180
- fs8.mkdirSync(rootDir, { recursive: true });
237
+ fs22.mkdirSync(rootDir, { recursive: true });
181
238
  const target = restartIntentMarkerPath(rootDir);
182
239
  const tmp = `${target}.tmp`;
183
- fs8.writeFileSync(tmp, JSON.stringify(marker, null, 2), "utf-8");
184
- fs8.renameSync(tmp, target);
240
+ fs22.writeFileSync(tmp, JSON.stringify(marker, null, 2), "utf-8");
241
+ fs22.renameSync(tmp, target);
185
242
  }
186
243
  function readRestartIntentMarker(rootDir) {
187
244
  try {
188
- const raw = JSON.parse(fs8.readFileSync(restartIntentMarkerPath(rootDir), "utf-8"));
245
+ const raw = JSON.parse(fs22.readFileSync(restartIntentMarkerPath(rootDir), "utf-8"));
189
246
  return isRestartIntentMarker(raw) ? raw : null;
190
247
  } catch {
191
248
  return null;
@@ -193,7 +250,7 @@ var require_dist = __commonJS({
193
250
  }
194
251
  function clearRestartIntentMarker(rootDir) {
195
252
  try {
196
- fs8.rmSync(restartIntentMarkerPath(rootDir), { force: true });
253
+ fs22.rmSync(restartIntentMarkerPath(rootDir), { force: true });
197
254
  } catch {
198
255
  }
199
256
  }
@@ -206,13 +263,13 @@ var require_dist = __commonJS({
206
263
  function validateVersionDir(rootDir, version, nodeMajor, spec) {
207
264
  const vDir = versionDir(rootDir, version);
208
265
  const entry = rootEntryPath(vDir, spec);
209
- if (!fs8.existsSync(entry)) {
266
+ if (!fs22.existsSync(entry)) {
210
267
  return `root entry missing: ${entry}`;
211
268
  }
212
- const pkgJsonPath = path9.join(rootPackageDir(vDir, spec), "package.json");
269
+ const pkgJsonPath = path22.join(rootPackageDir(vDir, spec), "package.json");
213
270
  let pkg;
214
271
  try {
215
- const raw = JSON.parse(fs8.readFileSync(pkgJsonPath, "utf-8"));
272
+ const raw = JSON.parse(fs22.readFileSync(pkgJsonPath, "utf-8"));
216
273
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
217
274
  return `package.json malformed: ${pkgJsonPath}`;
218
275
  }
@@ -359,25 +416,25 @@ var require_dist = __commonJS({
359
416
  if (pa.prerelease > pb.prerelease) return 1;
360
417
  return 0;
361
418
  }
362
- var fs22 = __toESM2(require("fs"));
363
- var path22 = __toESM2(require("path"));
419
+ var fs32 = __toESM2(require("fs"));
420
+ var path32 = __toESM2(require("path"));
364
421
  function detectWorkspaceRoot(fromDir) {
365
- let dir = path22.resolve(fromDir);
422
+ let dir = path32.resolve(fromDir);
366
423
  for (; ; ) {
367
- const pkgPath = path22.join(dir, "package.json");
424
+ const pkgPath = path32.join(dir, "package.json");
368
425
  try {
369
- const raw = JSON.parse(fs22.readFileSync(pkgPath, "utf-8"));
426
+ const raw = JSON.parse(fs32.readFileSync(pkgPath, "utf-8"));
370
427
  if (typeof raw === "object" && raw !== null && raw["workspaces"] !== void 0) {
371
428
  return dir;
372
429
  }
373
430
  } catch {
374
431
  }
375
- const parent = path22.dirname(dir);
432
+ const parent = path32.dirname(dir);
376
433
  if (parent === dir) return null;
377
434
  dir = parent;
378
435
  }
379
436
  }
380
- var path32 = __toESM2(require("path"));
437
+ var path42 = __toESM2(require("path"));
381
438
  var import_node_url = require("url");
382
439
  var HOST_EXTERNAL_SPECIFIERS = [
383
440
  "@camstack/system",
@@ -405,7 +462,7 @@ var require_dist = __commonJS({
405
462
  return;
406
463
  }
407
464
  const anchorURL = (0, import_node_url.pathToFileURL)(
408
- path32.join(activeRootDir, "node_modules", "__camstack_starter_anchor__.js")
465
+ path42.join(activeRootDir, "node_modules", "__camstack_starter_anchor__.js")
409
466
  ).href;
410
467
  const hooks = {
411
468
  resolve: (specifier, context, nextResolve) => {
@@ -483,8 +540,8 @@ var require_dist = __commonJS({
483
540
  options.loadEntry(options.seedEntry);
484
541
  }
485
542
  var import_node_child_process = require("child_process");
486
- var fs32 = __toESM2(require("fs"));
487
- var path42 = __toESM2(require("path"));
543
+ var fs42 = __toESM2(require("fs"));
544
+ var path52 = __toESM2(require("path"));
488
545
  var import_node_util = require("util");
489
546
  var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
490
547
  function buildNpmRegistryArgs(registry) {
@@ -496,7 +553,7 @@ var require_dist = __commonJS({
496
553
  var RESTART_REASON_PREFIX = "server-update";
497
554
  function readPackageVersion(pkgJsonPath) {
498
555
  try {
499
- const raw = JSON.parse(fs32.readFileSync(pkgJsonPath, "utf-8"));
556
+ const raw = JSON.parse(fs42.readFileSync(pkgJsonPath, "utf-8"));
500
557
  if (typeof raw === "object" && raw !== null) {
501
558
  const version = raw["version"];
502
559
  if (typeof version === "string") return version;
@@ -523,7 +580,7 @@ var require_dist = __commonJS({
523
580
  this.envNames = options.envNames;
524
581
  this.logger = options.logger;
525
582
  this.restartServerFn = options.restartServer;
526
- this.dataDir = path42.resolve(options.dataDir);
583
+ this.dataDir = path52.resolve(options.dataDir);
527
584
  this.execNpm = options.execNpm ?? (async (args2, opts) => {
528
585
  const { stdout } = await execFileAsync("npm", [...args2], {
529
586
  cwd: opts.cwd,
@@ -552,7 +609,7 @@ var require_dist = __commonJS({
552
609
  seedVersion() {
553
610
  const seedDir = this.env[this.envNames.seedDir];
554
611
  if (seedDir === void 0 || seedDir.length === 0) return null;
555
- return readPackageVersion(path42.join(seedDir, "package.json"));
612
+ return readPackageVersion(path52.join(seedDir, "package.json"));
556
613
  }
557
614
  rootDir() {
558
615
  return serverRootDir2(this.dataDir);
@@ -570,7 +627,7 @@ var require_dist = __commonJS({
570
627
  const rootDir = this.rootDir();
571
628
  const raw = readServerRootState(rootDir);
572
629
  if (raw !== null) return { state: raw, corrupt: false };
573
- return { state: emptyServerRootState(), corrupt: fs32.existsSync(stateFilePath(rootDir)) };
630
+ return { state: emptyServerRootState(), corrupt: fs42.existsSync(stateFilePath(rootDir)) };
574
631
  }
575
632
  updateState(state) {
576
633
  if (this.inFlight === "checking") return "checking";
@@ -710,6 +767,17 @@ var require_dist = __commonJS({
710
767
  message: `Refused: ${this.spec.packageName}@${target} is already running.`
711
768
  };
712
769
  }
770
+ if (isDevChannelVersion(target)) {
771
+ const uploadsDir = devUploadVersionDir(this.rootDir(), target);
772
+ if (!fs42.existsSync(uploadsDir)) {
773
+ return {
774
+ accepted: false,
775
+ targetVersion: target,
776
+ restarting: false,
777
+ message: `Refused: dev-channel version ${target} has no uploaded tarballs on this node (${uploadsDir} is missing). Dev server deploys are hub-only in this phase \u2014 upload via \`camstack deploy-server\` against this node first.`
778
+ };
779
+ }
780
+ }
713
781
  this.inFlight = "staging";
714
782
  try {
715
783
  await this.stageAndActivate(target);
@@ -756,34 +824,40 @@ var require_dist = __commonJS({
756
824
  async stageAndActivate(target) {
757
825
  const rootDir = this.rootDir();
758
826
  const vDir = versionsDir(rootDir);
759
- fs32.mkdirSync(vDir, { recursive: true });
760
- const stagingDir = path42.join(vDir, `.staging-${target}-${process.pid}-${this.now()}`);
761
- fs32.mkdirSync(stagingDir, { recursive: true });
827
+ fs42.mkdirSync(vDir, { recursive: true });
828
+ const stagingDir = path52.join(vDir, `.staging-${target}-${process.pid}-${this.now()}`);
829
+ fs42.mkdirSync(stagingDir, { recursive: true });
762
830
  try {
763
- fs32.writeFileSync(
764
- path42.join(stagingDir, "package.json"),
765
- JSON.stringify({ name: "camstack-node-root", private: true }, null, 2),
766
- "utf-8"
767
- );
831
+ const devDir = devUploadVersionDir(rootDir, target);
768
832
  const registry = this.env["CAMSTACK_NPM_REGISTRY"];
769
- await this.execNpm(
770
- [
771
- "install",
772
- "--omit=dev",
773
- "--no-audit",
774
- "--no-fund",
775
- "--loglevel=error",
776
- `${this.spec.packageName}@${target}`,
777
- ...buildNpmRegistryArgs(registry)
778
- ],
779
- { cwd: stagingDir, timeoutMs: NPM_INSTALL_TIMEOUT_MS }
780
- );
833
+ const installArgs = ["install", "--omit=dev", "--no-audit", "--no-fund", "--loglevel=error"];
834
+ if (fs42.existsSync(devDir)) {
835
+ fs42.writeFileSync(
836
+ path52.join(stagingDir, "package.json"),
837
+ JSON.stringify(this.buildDevUploadsPackageJson(devDir, target), null, 2),
838
+ "utf-8"
839
+ );
840
+ await this.execNpm([...installArgs, ...buildNpmRegistryArgs(registry)], {
841
+ cwd: stagingDir,
842
+ timeoutMs: NPM_INSTALL_TIMEOUT_MS
843
+ });
844
+ } else {
845
+ fs42.writeFileSync(
846
+ path52.join(stagingDir, "package.json"),
847
+ JSON.stringify({ name: "camstack-node-root", private: true }, null, 2),
848
+ "utf-8"
849
+ );
850
+ await this.execNpm(
851
+ [...installArgs, `${this.spec.packageName}@${target}`, ...buildNpmRegistryArgs(registry)],
852
+ { cwd: stagingDir, timeoutMs: NPM_INSTALL_TIMEOUT_MS }
853
+ );
854
+ }
781
855
  const entry = rootEntryPath(stagingDir, this.spec);
782
- if (!fs32.existsSync(entry)) {
856
+ if (!fs42.existsSync(entry)) {
783
857
  throw new Error(`staged closure is missing the root entry (${entry})`);
784
858
  }
785
859
  const stagedVersion = readPackageVersion(
786
- path42.join(rootPackageDir(stagingDir, this.spec), "package.json")
860
+ path52.join(rootPackageDir(stagingDir, this.spec), "package.json")
787
861
  );
788
862
  if (stagedVersion !== target) {
789
863
  throw new Error(
@@ -791,17 +865,61 @@ var require_dist = __commonJS({
791
865
  );
792
866
  }
793
867
  const dest = versionDir(rootDir, target);
794
- if (fs32.existsSync(dest)) {
868
+ if (fs42.existsSync(dest)) {
795
869
  const aside = `${dest}.evicted-${this.now()}`;
796
- await fs32.promises.rename(dest, aside);
797
- await fs32.promises.rm(aside, { recursive: true, force: true }).catch(() => void 0);
870
+ await fs42.promises.rename(dest, aside);
871
+ await fs42.promises.rm(aside, { recursive: true, force: true }).catch(() => void 0);
798
872
  }
799
- await fs32.promises.rename(stagingDir, dest);
873
+ await fs42.promises.rename(stagingDir, dest);
800
874
  } catch (err) {
801
- await fs32.promises.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
875
+ await fs42.promises.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
802
876
  throw err;
803
877
  }
804
878
  }
879
+ /**
880
+ * Synthetic staging package.json for the dev server-deploy channel:
881
+ * `dependencies` carries the root package as a `file:` ref to its uploaded
882
+ * tgz; `overrides` maps EVERY OTHER uploaded `@camstack/<name>` to its tgz
883
+ * so any transitive occurrence resolves to the uploaded copy, never npm.
884
+ * Fail-closed: a missing/invalid manifest, a manifest for a different
885
+ * version, a missing root package, or a listed-but-absent tgz all throw
886
+ * (surfaced by `applyServerUpdate` as a staging failure).
887
+ */
888
+ buildDevUploadsPackageJson(devDir, target) {
889
+ const manifest = readDevUploadManifest(devDir);
890
+ if (manifest === null) {
891
+ throw new Error(`dev-uploads manifest missing or invalid in ${devDir}`);
892
+ }
893
+ if (manifest.version !== target) {
894
+ throw new Error(
895
+ `dev-uploads manifest version mismatch: dir is for ${target}, manifest says ${manifest.version}`
896
+ );
897
+ }
898
+ const rootTgz = manifest.packages[this.spec.packageName];
899
+ if (rootTgz === void 0) {
900
+ throw new Error(
901
+ `dev-uploads for ${target} do not include the root package ${this.spec.packageName}`
902
+ );
903
+ }
904
+ const fileRef = (filename) => {
905
+ const abs = path52.join(devDir, filename);
906
+ if (!fs42.existsSync(abs)) {
907
+ throw new Error(`dev-uploads tarball listed in the manifest is missing: ${abs}`);
908
+ }
909
+ return `file:${abs}`;
910
+ };
911
+ const overrides = {};
912
+ for (const [pkgName, filename] of Object.entries(manifest.packages)) {
913
+ if (pkgName === this.spec.packageName) continue;
914
+ overrides[pkgName] = fileRef(filename);
915
+ }
916
+ return {
917
+ name: "camstack-node-root",
918
+ private: true,
919
+ dependencies: { [this.spec.packageName]: fileRef(rootTgz) },
920
+ ...Object.keys(overrides).length > 0 ? { overrides } : {}
921
+ };
922
+ }
805
923
  // ── Rollback ──────────────────────────────────────────────────────────
806
924
  async rollbackServerUpdate() {
807
925
  if (this.inFlight !== "idle") {
@@ -931,6 +1049,7 @@ var require_dist = __commonJS({
931
1049
  (v) => typeof v === "string"
932
1050
  )
933
1051
  );
1052
+ this.sweepDevUploads(DEV_UPLOADS_KEEP_COUNT);
934
1053
  this.logger.info("root package update confirmed healthy", {
935
1054
  meta: {
936
1055
  version: promoted.currentVersion,
@@ -954,23 +1073,23 @@ var require_dist = __commonJS({
954
1073
  const vDir = versionsDir(this.rootDir());
955
1074
  let entries;
956
1075
  try {
957
- entries = fs32.readdirSync(vDir);
1076
+ entries = fs42.readdirSync(vDir);
958
1077
  } catch {
959
1078
  return;
960
1079
  }
961
1080
  for (const entry of entries) {
962
1081
  if (keep.includes(entry)) continue;
963
- const full = path42.join(vDir, entry);
1082
+ const full = path52.join(vDir, entry);
964
1083
  if (entry.startsWith(".")) {
965
1084
  try {
966
- const ageMs = this.now() - fs32.statSync(full).mtimeMs;
1085
+ const ageMs = this.now() - fs42.statSync(full).mtimeMs;
967
1086
  if (ageMs < _RootUpdateService.STALE_TRANSIENT_MS) continue;
968
1087
  } catch {
969
1088
  continue;
970
1089
  }
971
1090
  }
972
1091
  try {
973
- fs32.rmSync(full, { recursive: true, force: true });
1092
+ fs42.rmSync(full, { recursive: true, force: true });
974
1093
  this.logger.debug("pruned server-root version dir", { meta: { entry } });
975
1094
  } catch (err) {
976
1095
  this.logger.warn("failed to prune server-root version dir", {
@@ -979,6 +1098,56 @@ var require_dist = __commonJS({
979
1098
  }
980
1099
  }
981
1100
  }
1101
+ /**
1102
+ * Promote-time sweep of `server-root/dev-uploads/`: keep only the
1103
+ * `keepCount` most recent entries (ordered by the self-describing
1104
+ * `-dev.<epochSeconds>` suffix; non-dev-shaped residue counts as oldest).
1105
+ * Uses the same rename-aside graveyard pattern as the addon installer —
1106
+ * a rename is a metadata op that succeeds even when a tarball is held
1107
+ * open, and residue in the graveyard is re-swept on the next promote.
1108
+ */
1109
+ sweepDevUploads(keepCount) {
1110
+ const dir = devUploadsDir(this.rootDir());
1111
+ let entries;
1112
+ try {
1113
+ entries = fs42.readdirSync(dir);
1114
+ } catch {
1115
+ return;
1116
+ }
1117
+ const graveyard = path52.join(dir, ".sweeping");
1118
+ try {
1119
+ for (const residue of fs42.readdirSync(graveyard)) {
1120
+ try {
1121
+ fs42.rmSync(path52.join(graveyard, residue), { recursive: true, force: true });
1122
+ } catch {
1123
+ }
1124
+ }
1125
+ } catch {
1126
+ }
1127
+ const versions = entries.filter((name) => !name.startsWith("."));
1128
+ const byNewestFirst = [...versions].sort(
1129
+ (a, b) => (devChannelEpoch(b) ?? -1) - (devChannelEpoch(a) ?? -1)
1130
+ );
1131
+ const doomed = byNewestFirst.slice(keepCount);
1132
+ if (doomed.length === 0) return;
1133
+ fs42.mkdirSync(graveyard, { recursive: true });
1134
+ for (const entry of doomed) {
1135
+ const aside = path52.join(graveyard, `${entry}-${this.now()}`);
1136
+ try {
1137
+ fs42.renameSync(path52.join(dir, entry), aside);
1138
+ } catch (err) {
1139
+ this.logger.warn("failed to move dev-uploads entry aside for sweep", {
1140
+ meta: { entry, error: err instanceof Error ? err.message : String(err) }
1141
+ });
1142
+ continue;
1143
+ }
1144
+ try {
1145
+ fs42.rmSync(aside, { recursive: true, force: true });
1146
+ this.logger.debug("swept dev-uploads entry", { meta: { entry } });
1147
+ } catch {
1148
+ }
1149
+ }
1150
+ }
982
1151
  /** Transient (.staging- / .evicted-) dirs younger than this are never pruned. */
983
1152
  static STALE_TRANSIENT_MS = 24 * 60 * 60 * 1e3;
984
1153
  };
@@ -1059,6 +1228,39 @@ function deriveHubUrlFromRegistry(nodes) {
1059
1228
  return `https://${host}:${HUB_API_PORT}`;
1060
1229
  }
1061
1230
 
1231
+ // src/agent-http-auth.ts
1232
+ var import_node_crypto = require("crypto");
1233
+ function isLoopbackAddress(addr) {
1234
+ if (!addr) return false;
1235
+ if (addr === "::1") return true;
1236
+ const v4 = addr.startsWith("::ffff:") ? addr.slice("::ffff:".length) : addr;
1237
+ const parts = v4.split(".");
1238
+ if (parts.length !== 4 || parts[0] !== "127") return false;
1239
+ return parts.every((p) => /^\d{1,3}$/.test(p));
1240
+ }
1241
+ function bearerMatchesSecret(authorization, secret) {
1242
+ if (secret.length === 0) return false;
1243
+ if (!authorization?.startsWith("Bearer ")) return false;
1244
+ const token = Buffer.from(authorization.slice("Bearer ".length));
1245
+ const expected = Buffer.from(secret);
1246
+ return token.length === expected.length && (0, import_node_crypto.timingSafeEqual)(token, expected);
1247
+ }
1248
+ function isAuthorizedAgentRequest(input, clusterSecret) {
1249
+ if (isLoopbackAddress(input.remoteAddress)) return true;
1250
+ if (clusterSecret === null || clusterSecret.length === 0) return false;
1251
+ return bearerMatchesSecret(input.authorization, clusterSecret);
1252
+ }
1253
+ function isPairingRequest(method, urlPath) {
1254
+ const pathOnly = urlPath.split("?")[0] ?? urlPath;
1255
+ if (method === "GET") {
1256
+ return pathOnly === "/api/agent/status" || pathOnly === "/api/agent/config" || pathOnly === "/api/agent/discovered-nodes";
1257
+ }
1258
+ if (method === "POST") {
1259
+ return pathOnly === "/api/agent/config" || pathOnly === "/api/agent/restart";
1260
+ }
1261
+ return false;
1262
+ }
1263
+
1062
1264
  // src/agent-http.ts
1063
1265
  function compareVersions(a, b) {
1064
1266
  if (a === null && b === null) return 0;
@@ -1147,7 +1349,32 @@ async function createAgentHttpServer(getBroker, config) {
1147
1349
  const app = (0, import_fastify.default)({ logger: false });
1148
1350
  const cors = await import("@fastify/cors");
1149
1351
  await app.register(cors.default);
1352
+ const currentSecret = () => {
1353
+ const raw = readConfigFile(config.configPath);
1354
+ return typeof raw.secret === "string" && raw.secret.length > 0 ? raw.secret : null;
1355
+ };
1356
+ app.addHook("onRequest", async (req, reply) => {
1357
+ const pathOnly = req.url.split("?")[0] ?? req.url;
1358
+ const isProtected = pathOnly.startsWith("/api/") || pathOnly.startsWith("/health/");
1359
+ if (!isProtected) return;
1360
+ const secret = currentSecret();
1361
+ const authInput = {
1362
+ remoteAddress: req.socket.remoteAddress ?? void 0,
1363
+ ...typeof req.headers.authorization === "string" ? { authorization: req.headers.authorization } : {}
1364
+ };
1365
+ if (isAuthorizedAgentRequest(authInput, secret)) return;
1366
+ if (secret === null && isPairingRequest(req.method, pathOnly)) return;
1367
+ return reply.status(401).send({ ok: false, error: "unauthorized" });
1368
+ });
1150
1369
  app.get("/health", async (_req, reply) => {
1370
+ try {
1371
+ await getBroker().call("$agent.health");
1372
+ return { ok: true };
1373
+ } catch {
1374
+ return reply.status(503).send({ ok: false });
1375
+ }
1376
+ });
1377
+ app.get("/health/details", async (_req, reply) => {
1151
1378
  try {
1152
1379
  const result = await getBroker().call("$agent.health");
1153
1380
  return result;
@@ -1466,7 +1693,7 @@ var import_moleculer = require("moleculer");
1466
1693
  // src/agent-deploy-swap.ts
1467
1694
  var fs4 = __toESM(require("fs"));
1468
1695
  var path4 = __toESM(require("path"));
1469
- var import_node_crypto = require("crypto");
1696
+ var import_node_crypto2 = require("crypto");
1470
1697
  function rmrf(target) {
1471
1698
  fs4.rmSync(target, { recursive: true, force: true });
1472
1699
  }
@@ -1490,7 +1717,7 @@ async function applyDeployedBundle(input) {
1490
1717
  const liveParent = path4.dirname(liveDir);
1491
1718
  const liveBase = path4.basename(liveDir);
1492
1719
  fs4.mkdirSync(liveParent, { recursive: true });
1493
- const token = (0, import_node_crypto.randomBytes)(6).toString("hex");
1720
+ const token = (0, import_node_crypto2.randomBytes)(6).toString("hex");
1494
1721
  const nextDir = path4.join(liveParent, `.${liveBase}.next.${token}`);
1495
1722
  const backupDir = path4.join(liveParent, `.${liveBase}.backup.${token}`);
1496
1723
  fs4.mkdirSync(nextDir, { recursive: true });
@@ -1539,7 +1766,7 @@ async function applyModelDistribution(seam, params) {
1539
1766
  }
1540
1767
 
1541
1768
  // src/fetch-bundle-from-hub.ts
1542
- var import_node_crypto2 = require("crypto");
1769
+ var import_node_crypto3 = require("crypto");
1543
1770
  var import_undici = require("undici");
1544
1771
  async function fetchBundleFromHub(opts) {
1545
1772
  const authHeader = { Authorization: `Bearer ${opts.token}`, "accept-encoding": "identity" };
@@ -1554,7 +1781,7 @@ async function fetchBundleFromHub(opts) {
1554
1781
  if (buffer.length !== opts.bytes) {
1555
1782
  throw new Error(`bundle bytes mismatch: expected ${opts.bytes}, got ${buffer.length}`);
1556
1783
  }
1557
- const sha = (0, import_node_crypto2.createHash)("sha256").update(buffer).digest("hex");
1784
+ const sha = (0, import_node_crypto3.createHash)("sha256").update(buffer).digest("hex");
1558
1785
  if (sha !== opts.sha256) {
1559
1786
  throw new Error(`bundle sha256 mismatch: expected ${opts.sha256}, got ${sha}`);
1560
1787
  }