@camstack/agent 1.1.42 → 1.1.44

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.mjs CHANGED
@@ -2,8 +2,8 @@
2
2
  import { createRequire as __cr } from 'node:module'; const require = globalThis.require ?? __cr(import.meta.url);
3
3
  import {
4
4
  startAgent
5
- } from "./chunk-MMKRM4RT.mjs";
6
- import "./chunk-L34OOQI3.mjs";
5
+ } from "./chunk-Z7VZQCFX.mjs";
6
+ import "./chunk-Y6BUFOWG.mjs";
7
7
 
8
8
  // src/cli.ts
9
9
  import { existsSync } from "fs";
package/dist/index.js CHANGED
@@ -64,6 +64,9 @@ var require_dist = __commonJS({
64
64
  var src_exports2 = {};
65
65
  __export2(src_exports2, {
66
66
  AGENT_ROOT_SPEC: () => AGENT_ROOT_SPEC2,
67
+ DEV_UPLOADS_DIRNAME: () => DEV_UPLOADS_DIRNAME,
68
+ DEV_UPLOADS_KEEP_COUNT: () => DEV_UPLOADS_KEEP_COUNT,
69
+ DEV_UPLOAD_MANIFEST_FILE: () => DEV_UPLOAD_MANIFEST_FILE,
67
70
  HOST_EXTERNAL_SPECIFIERS: () => HOST_EXTERNAL_SPECIFIERS,
68
71
  HUB_ROOT_SPEC: () => HUB_ROOT_SPEC,
69
72
  RESTART_INTENT_FILE: () => RESTART_INTENT_FILE,
@@ -73,11 +76,17 @@ var require_dist = __commonJS({
73
76
  clearRestartIntentMarker: () => clearRestartIntentMarker,
74
77
  compareSemver: () => compareSemver,
75
78
  detectWorkspaceRoot: () => detectWorkspaceRoot,
79
+ devChannelEpoch: () => devChannelEpoch,
80
+ devUploadManifestPath: () => devUploadManifestPath,
81
+ devUploadVersionDir: () => devUploadVersionDir,
82
+ devUploadsDir: () => devUploadsDir,
76
83
  emptyServerRootState: () => emptyServerRootState,
84
+ isDevChannelVersion: () => isDevChannelVersion,
77
85
  isHostExternal: () => isHostExternal,
78
86
  isServerRootState: () => isServerRootState,
79
87
  minNodeMajorOf: () => minNodeMajorOf,
80
88
  planBoot: () => planBoot,
89
+ readDevUploadManifest: () => readDevUploadManifest,
81
90
  readRestartIntentMarker: () => readRestartIntentMarker,
82
91
  readServerRootState: () => readServerRootState,
83
92
  registerActiveRootResolver: () => registerActiveRootResolver,
@@ -90,10 +99,58 @@ var require_dist = __commonJS({
90
99
  validateVersionDir: () => validateVersionDir,
91
100
  versionDir: () => versionDir,
92
101
  versionsDir: () => versionsDir,
102
+ writeDevUploadManifest: () => writeDevUploadManifest,
93
103
  writeRestartIntentMarker: () => writeRestartIntentMarker2,
94
104
  writeServerRootState: () => writeServerRootState
95
105
  });
96
106
  module2.exports = __toCommonJS2(src_exports2);
107
+ var fs8 = __toESM2(require("fs"));
108
+ var path8 = __toESM2(require("path"));
109
+ var DEV_UPLOADS_DIRNAME = "dev-uploads";
110
+ var DEV_UPLOAD_MANIFEST_FILE = "manifest.json";
111
+ var DEV_UPLOADS_KEEP_COUNT = 3;
112
+ var DEV_CHANNEL_VERSION_RE = /-dev\.\d+$/;
113
+ function isDevChannelVersion(version) {
114
+ return DEV_CHANNEL_VERSION_RE.test(version);
115
+ }
116
+ function devChannelEpoch(version) {
117
+ const match = /-dev\.(\d+)$/.exec(version);
118
+ if (match === null) return null;
119
+ const epoch = Number.parseInt(match[1] ?? "", 10);
120
+ return Number.isNaN(epoch) ? null : epoch;
121
+ }
122
+ function devUploadsDir(rootDir) {
123
+ return path8.join(rootDir, DEV_UPLOADS_DIRNAME);
124
+ }
125
+ function devUploadVersionDir(rootDir, version) {
126
+ return path8.join(devUploadsDir(rootDir), version);
127
+ }
128
+ function devUploadManifestPath(versionDirPath) {
129
+ return path8.join(versionDirPath, DEV_UPLOAD_MANIFEST_FILE);
130
+ }
131
+ function isDevUploadManifest(v) {
132
+ if (typeof v !== "object" || v === null) return false;
133
+ const m = v;
134
+ if (typeof m["version"] !== "string") return false;
135
+ const packages = m["packages"];
136
+ if (typeof packages !== "object" || packages === null || Array.isArray(packages)) return false;
137
+ return Object.values(packages).every((filename) => typeof filename === "string");
138
+ }
139
+ function readDevUploadManifest(versionDirPath) {
140
+ try {
141
+ const raw = JSON.parse(fs8.readFileSync(devUploadManifestPath(versionDirPath), "utf-8"));
142
+ return isDevUploadManifest(raw) ? raw : null;
143
+ } catch {
144
+ return null;
145
+ }
146
+ }
147
+ function writeDevUploadManifest(versionDirPath, manifest) {
148
+ fs8.mkdirSync(versionDirPath, { recursive: true });
149
+ const target = devUploadManifestPath(versionDirPath);
150
+ const tmp = `${target}.tmp`;
151
+ fs8.writeFileSync(tmp, JSON.stringify(manifest, null, 2), "utf-8");
152
+ fs8.renameSync(tmp, target);
153
+ }
97
154
  var HUB_ROOT_SPEC = {
98
155
  packageName: "@camstack/server",
99
156
  entryRelPath: ["dist", "launcher.js"]
@@ -102,8 +159,8 @@ var require_dist = __commonJS({
102
159
  packageName: "@camstack/agent",
103
160
  entryRelPath: ["dist", "cli.js"]
104
161
  };
105
- var fs8 = __toESM2(require("fs"));
106
- var path8 = __toESM2(require("path"));
162
+ var fs22 = __toESM2(require("fs"));
163
+ var path22 = __toESM2(require("path"));
107
164
  var SERVER_ROOT_DIRNAME = "server-root";
108
165
  var SERVER_ROOT_STATE_FILE = "state.json";
109
166
  var RESTART_INTENT_FILE = ".restart-intent";
@@ -117,22 +174,22 @@ var require_dist = __commonJS({
117
174
  };
118
175
  }
119
176
  function serverRootDir2(dataDir) {
120
- return path8.join(dataDir, SERVER_ROOT_DIRNAME);
177
+ return path22.join(dataDir, SERVER_ROOT_DIRNAME);
121
178
  }
122
179
  function versionsDir(rootDir) {
123
- return path8.join(rootDir, "versions");
180
+ return path22.join(rootDir, "versions");
124
181
  }
125
182
  function versionDir(rootDir, version) {
126
- return path8.join(versionsDir(rootDir), version);
183
+ return path22.join(versionsDir(rootDir), version);
127
184
  }
128
185
  function rootPackageDir(versionDirPath, spec) {
129
- return path8.join(versionDirPath, "node_modules", ...spec.packageName.split("/"));
186
+ return path22.join(versionDirPath, "node_modules", ...spec.packageName.split("/"));
130
187
  }
131
188
  function rootEntryPath(versionDirPath, spec) {
132
- return path8.join(rootPackageDir(versionDirPath, spec), ...spec.entryRelPath);
189
+ return path22.join(rootPackageDir(versionDirPath, spec), ...spec.entryRelPath);
133
190
  }
134
191
  function stateFilePath(rootDir) {
135
- return path8.join(rootDir, SERVER_ROOT_STATE_FILE);
192
+ return path22.join(rootDir, SERVER_ROOT_STATE_FILE);
136
193
  }
137
194
  function isNullableString(v) {
138
195
  return v === null || typeof v === "string";
@@ -159,21 +216,21 @@ var require_dist = __commonJS({
159
216
  }
160
217
  function readServerRootState(rootDir) {
161
218
  try {
162
- const raw = JSON.parse(fs8.readFileSync(stateFilePath(rootDir), "utf-8"));
219
+ const raw = JSON.parse(fs22.readFileSync(stateFilePath(rootDir), "utf-8"));
163
220
  return isServerRootState(raw) ? raw : null;
164
221
  } catch {
165
222
  return null;
166
223
  }
167
224
  }
168
225
  function writeServerRootState(rootDir, state) {
169
- fs8.mkdirSync(rootDir, { recursive: true });
226
+ fs22.mkdirSync(rootDir, { recursive: true });
170
227
  const target = stateFilePath(rootDir);
171
228
  const tmp = `${target}.tmp`;
172
- fs8.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf-8");
173
- fs8.renameSync(tmp, target);
229
+ fs22.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf-8");
230
+ fs22.renameSync(tmp, target);
174
231
  }
175
232
  function restartIntentMarkerPath(rootDir) {
176
- return path8.join(rootDir, RESTART_INTENT_FILE);
233
+ return path22.join(rootDir, RESTART_INTENT_FILE);
177
234
  }
178
235
  function isRestartIntentMarker(v) {
179
236
  if (typeof v !== "object" || v === null) return false;
@@ -181,15 +238,15 @@ var require_dist = __commonJS({
181
238
  return typeof m["requestedAtMs"] === "number" && typeof m["reason"] === "string";
182
239
  }
183
240
  function writeRestartIntentMarker2(rootDir, marker) {
184
- fs8.mkdirSync(rootDir, { recursive: true });
241
+ fs22.mkdirSync(rootDir, { recursive: true });
185
242
  const target = restartIntentMarkerPath(rootDir);
186
243
  const tmp = `${target}.tmp`;
187
- fs8.writeFileSync(tmp, JSON.stringify(marker, null, 2), "utf-8");
188
- fs8.renameSync(tmp, target);
244
+ fs22.writeFileSync(tmp, JSON.stringify(marker, null, 2), "utf-8");
245
+ fs22.renameSync(tmp, target);
189
246
  }
190
247
  function readRestartIntentMarker(rootDir) {
191
248
  try {
192
- const raw = JSON.parse(fs8.readFileSync(restartIntentMarkerPath(rootDir), "utf-8"));
249
+ const raw = JSON.parse(fs22.readFileSync(restartIntentMarkerPath(rootDir), "utf-8"));
193
250
  return isRestartIntentMarker(raw) ? raw : null;
194
251
  } catch {
195
252
  return null;
@@ -197,7 +254,7 @@ var require_dist = __commonJS({
197
254
  }
198
255
  function clearRestartIntentMarker(rootDir) {
199
256
  try {
200
- fs8.rmSync(restartIntentMarkerPath(rootDir), { force: true });
257
+ fs22.rmSync(restartIntentMarkerPath(rootDir), { force: true });
201
258
  } catch {
202
259
  }
203
260
  }
@@ -210,13 +267,13 @@ var require_dist = __commonJS({
210
267
  function validateVersionDir(rootDir, version, nodeMajor, spec) {
211
268
  const vDir = versionDir(rootDir, version);
212
269
  const entry = rootEntryPath(vDir, spec);
213
- if (!fs8.existsSync(entry)) {
270
+ if (!fs22.existsSync(entry)) {
214
271
  return `root entry missing: ${entry}`;
215
272
  }
216
- const pkgJsonPath = path8.join(rootPackageDir(vDir, spec), "package.json");
273
+ const pkgJsonPath = path22.join(rootPackageDir(vDir, spec), "package.json");
217
274
  let pkg;
218
275
  try {
219
- const raw = JSON.parse(fs8.readFileSync(pkgJsonPath, "utf-8"));
276
+ const raw = JSON.parse(fs22.readFileSync(pkgJsonPath, "utf-8"));
220
277
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
221
278
  return `package.json malformed: ${pkgJsonPath}`;
222
279
  }
@@ -363,25 +420,25 @@ var require_dist = __commonJS({
363
420
  if (pa.prerelease > pb.prerelease) return 1;
364
421
  return 0;
365
422
  }
366
- var fs22 = __toESM2(require("fs"));
367
- var path22 = __toESM2(require("path"));
423
+ var fs32 = __toESM2(require("fs"));
424
+ var path32 = __toESM2(require("path"));
368
425
  function detectWorkspaceRoot(fromDir) {
369
- let dir = path22.resolve(fromDir);
426
+ let dir = path32.resolve(fromDir);
370
427
  for (; ; ) {
371
- const pkgPath = path22.join(dir, "package.json");
428
+ const pkgPath = path32.join(dir, "package.json");
372
429
  try {
373
- const raw = JSON.parse(fs22.readFileSync(pkgPath, "utf-8"));
430
+ const raw = JSON.parse(fs32.readFileSync(pkgPath, "utf-8"));
374
431
  if (typeof raw === "object" && raw !== null && raw["workspaces"] !== void 0) {
375
432
  return dir;
376
433
  }
377
434
  } catch {
378
435
  }
379
- const parent = path22.dirname(dir);
436
+ const parent = path32.dirname(dir);
380
437
  if (parent === dir) return null;
381
438
  dir = parent;
382
439
  }
383
440
  }
384
- var path32 = __toESM2(require("path"));
441
+ var path42 = __toESM2(require("path"));
385
442
  var import_node_url = require("url");
386
443
  var HOST_EXTERNAL_SPECIFIERS = [
387
444
  "@camstack/system",
@@ -409,7 +466,7 @@ var require_dist = __commonJS({
409
466
  return;
410
467
  }
411
468
  const anchorURL = (0, import_node_url.pathToFileURL)(
412
- path32.join(activeRootDir, "node_modules", "__camstack_starter_anchor__.js")
469
+ path42.join(activeRootDir, "node_modules", "__camstack_starter_anchor__.js")
413
470
  ).href;
414
471
  const hooks = {
415
472
  resolve: (specifier, context, nextResolve) => {
@@ -487,8 +544,8 @@ var require_dist = __commonJS({
487
544
  options.loadEntry(options.seedEntry);
488
545
  }
489
546
  var import_node_child_process = require("child_process");
490
- var fs32 = __toESM2(require("fs"));
491
- var path42 = __toESM2(require("path"));
547
+ var fs42 = __toESM2(require("fs"));
548
+ var path52 = __toESM2(require("path"));
492
549
  var import_node_util = require("util");
493
550
  var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
494
551
  function buildNpmRegistryArgs(registry) {
@@ -500,7 +557,7 @@ var require_dist = __commonJS({
500
557
  var RESTART_REASON_PREFIX = "server-update";
501
558
  function readPackageVersion(pkgJsonPath) {
502
559
  try {
503
- const raw = JSON.parse(fs32.readFileSync(pkgJsonPath, "utf-8"));
560
+ const raw = JSON.parse(fs42.readFileSync(pkgJsonPath, "utf-8"));
504
561
  if (typeof raw === "object" && raw !== null) {
505
562
  const version = raw["version"];
506
563
  if (typeof version === "string") return version;
@@ -527,7 +584,7 @@ var require_dist = __commonJS({
527
584
  this.envNames = options.envNames;
528
585
  this.logger = options.logger;
529
586
  this.restartServerFn = options.restartServer;
530
- this.dataDir = path42.resolve(options.dataDir);
587
+ this.dataDir = path52.resolve(options.dataDir);
531
588
  this.execNpm = options.execNpm ?? (async (args, opts) => {
532
589
  const { stdout } = await execFileAsync("npm", [...args], {
533
590
  cwd: opts.cwd,
@@ -556,7 +613,7 @@ var require_dist = __commonJS({
556
613
  seedVersion() {
557
614
  const seedDir = this.env[this.envNames.seedDir];
558
615
  if (seedDir === void 0 || seedDir.length === 0) return null;
559
- return readPackageVersion(path42.join(seedDir, "package.json"));
616
+ return readPackageVersion(path52.join(seedDir, "package.json"));
560
617
  }
561
618
  rootDir() {
562
619
  return serverRootDir2(this.dataDir);
@@ -574,7 +631,7 @@ var require_dist = __commonJS({
574
631
  const rootDir = this.rootDir();
575
632
  const raw = readServerRootState(rootDir);
576
633
  if (raw !== null) return { state: raw, corrupt: false };
577
- return { state: emptyServerRootState(), corrupt: fs32.existsSync(stateFilePath(rootDir)) };
634
+ return { state: emptyServerRootState(), corrupt: fs42.existsSync(stateFilePath(rootDir)) };
578
635
  }
579
636
  updateState(state) {
580
637
  if (this.inFlight === "checking") return "checking";
@@ -714,6 +771,17 @@ var require_dist = __commonJS({
714
771
  message: `Refused: ${this.spec.packageName}@${target} is already running.`
715
772
  };
716
773
  }
774
+ if (isDevChannelVersion(target)) {
775
+ const uploadsDir = devUploadVersionDir(this.rootDir(), target);
776
+ if (!fs42.existsSync(uploadsDir)) {
777
+ return {
778
+ accepted: false,
779
+ targetVersion: target,
780
+ restarting: false,
781
+ 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.`
782
+ };
783
+ }
784
+ }
717
785
  this.inFlight = "staging";
718
786
  try {
719
787
  await this.stageAndActivate(target);
@@ -760,34 +828,40 @@ var require_dist = __commonJS({
760
828
  async stageAndActivate(target) {
761
829
  const rootDir = this.rootDir();
762
830
  const vDir = versionsDir(rootDir);
763
- fs32.mkdirSync(vDir, { recursive: true });
764
- const stagingDir = path42.join(vDir, `.staging-${target}-${process.pid}-${this.now()}`);
765
- fs32.mkdirSync(stagingDir, { recursive: true });
831
+ fs42.mkdirSync(vDir, { recursive: true });
832
+ const stagingDir = path52.join(vDir, `.staging-${target}-${process.pid}-${this.now()}`);
833
+ fs42.mkdirSync(stagingDir, { recursive: true });
766
834
  try {
767
- fs32.writeFileSync(
768
- path42.join(stagingDir, "package.json"),
769
- JSON.stringify({ name: "camstack-node-root", private: true }, null, 2),
770
- "utf-8"
771
- );
835
+ const devDir = devUploadVersionDir(rootDir, target);
772
836
  const registry = this.env["CAMSTACK_NPM_REGISTRY"];
773
- await this.execNpm(
774
- [
775
- "install",
776
- "--omit=dev",
777
- "--no-audit",
778
- "--no-fund",
779
- "--loglevel=error",
780
- `${this.spec.packageName}@${target}`,
781
- ...buildNpmRegistryArgs(registry)
782
- ],
783
- { cwd: stagingDir, timeoutMs: NPM_INSTALL_TIMEOUT_MS }
784
- );
837
+ const installArgs = ["install", "--omit=dev", "--no-audit", "--no-fund", "--loglevel=error"];
838
+ if (fs42.existsSync(devDir)) {
839
+ fs42.writeFileSync(
840
+ path52.join(stagingDir, "package.json"),
841
+ JSON.stringify(this.buildDevUploadsPackageJson(devDir, target), null, 2),
842
+ "utf-8"
843
+ );
844
+ await this.execNpm([...installArgs, ...buildNpmRegistryArgs(registry)], {
845
+ cwd: stagingDir,
846
+ timeoutMs: NPM_INSTALL_TIMEOUT_MS
847
+ });
848
+ } else {
849
+ fs42.writeFileSync(
850
+ path52.join(stagingDir, "package.json"),
851
+ JSON.stringify({ name: "camstack-node-root", private: true }, null, 2),
852
+ "utf-8"
853
+ );
854
+ await this.execNpm(
855
+ [...installArgs, `${this.spec.packageName}@${target}`, ...buildNpmRegistryArgs(registry)],
856
+ { cwd: stagingDir, timeoutMs: NPM_INSTALL_TIMEOUT_MS }
857
+ );
858
+ }
785
859
  const entry = rootEntryPath(stagingDir, this.spec);
786
- if (!fs32.existsSync(entry)) {
860
+ if (!fs42.existsSync(entry)) {
787
861
  throw new Error(`staged closure is missing the root entry (${entry})`);
788
862
  }
789
863
  const stagedVersion = readPackageVersion(
790
- path42.join(rootPackageDir(stagingDir, this.spec), "package.json")
864
+ path52.join(rootPackageDir(stagingDir, this.spec), "package.json")
791
865
  );
792
866
  if (stagedVersion !== target) {
793
867
  throw new Error(
@@ -795,17 +869,61 @@ var require_dist = __commonJS({
795
869
  );
796
870
  }
797
871
  const dest = versionDir(rootDir, target);
798
- if (fs32.existsSync(dest)) {
872
+ if (fs42.existsSync(dest)) {
799
873
  const aside = `${dest}.evicted-${this.now()}`;
800
- await fs32.promises.rename(dest, aside);
801
- await fs32.promises.rm(aside, { recursive: true, force: true }).catch(() => void 0);
874
+ await fs42.promises.rename(dest, aside);
875
+ await fs42.promises.rm(aside, { recursive: true, force: true }).catch(() => void 0);
802
876
  }
803
- await fs32.promises.rename(stagingDir, dest);
877
+ await fs42.promises.rename(stagingDir, dest);
804
878
  } catch (err) {
805
- await fs32.promises.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
879
+ await fs42.promises.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
806
880
  throw err;
807
881
  }
808
882
  }
883
+ /**
884
+ * Synthetic staging package.json for the dev server-deploy channel:
885
+ * `dependencies` carries the root package as a `file:` ref to its uploaded
886
+ * tgz; `overrides` maps EVERY OTHER uploaded `@camstack/<name>` to its tgz
887
+ * so any transitive occurrence resolves to the uploaded copy, never npm.
888
+ * Fail-closed: a missing/invalid manifest, a manifest for a different
889
+ * version, a missing root package, or a listed-but-absent tgz all throw
890
+ * (surfaced by `applyServerUpdate` as a staging failure).
891
+ */
892
+ buildDevUploadsPackageJson(devDir, target) {
893
+ const manifest = readDevUploadManifest(devDir);
894
+ if (manifest === null) {
895
+ throw new Error(`dev-uploads manifest missing or invalid in ${devDir}`);
896
+ }
897
+ if (manifest.version !== target) {
898
+ throw new Error(
899
+ `dev-uploads manifest version mismatch: dir is for ${target}, manifest says ${manifest.version}`
900
+ );
901
+ }
902
+ const rootTgz = manifest.packages[this.spec.packageName];
903
+ if (rootTgz === void 0) {
904
+ throw new Error(
905
+ `dev-uploads for ${target} do not include the root package ${this.spec.packageName}`
906
+ );
907
+ }
908
+ const fileRef = (filename) => {
909
+ const abs = path52.join(devDir, filename);
910
+ if (!fs42.existsSync(abs)) {
911
+ throw new Error(`dev-uploads tarball listed in the manifest is missing: ${abs}`);
912
+ }
913
+ return `file:${abs}`;
914
+ };
915
+ const overrides = {};
916
+ for (const [pkgName, filename] of Object.entries(manifest.packages)) {
917
+ if (pkgName === this.spec.packageName) continue;
918
+ overrides[pkgName] = fileRef(filename);
919
+ }
920
+ return {
921
+ name: "camstack-node-root",
922
+ private: true,
923
+ dependencies: { [this.spec.packageName]: fileRef(rootTgz) },
924
+ ...Object.keys(overrides).length > 0 ? { overrides } : {}
925
+ };
926
+ }
809
927
  // ── Rollback ──────────────────────────────────────────────────────────
810
928
  async rollbackServerUpdate() {
811
929
  if (this.inFlight !== "idle") {
@@ -935,6 +1053,7 @@ var require_dist = __commonJS({
935
1053
  (v) => typeof v === "string"
936
1054
  )
937
1055
  );
1056
+ this.sweepDevUploads(DEV_UPLOADS_KEEP_COUNT);
938
1057
  this.logger.info("root package update confirmed healthy", {
939
1058
  meta: {
940
1059
  version: promoted.currentVersion,
@@ -958,23 +1077,23 @@ var require_dist = __commonJS({
958
1077
  const vDir = versionsDir(this.rootDir());
959
1078
  let entries;
960
1079
  try {
961
- entries = fs32.readdirSync(vDir);
1080
+ entries = fs42.readdirSync(vDir);
962
1081
  } catch {
963
1082
  return;
964
1083
  }
965
1084
  for (const entry of entries) {
966
1085
  if (keep.includes(entry)) continue;
967
- const full = path42.join(vDir, entry);
1086
+ const full = path52.join(vDir, entry);
968
1087
  if (entry.startsWith(".")) {
969
1088
  try {
970
- const ageMs = this.now() - fs32.statSync(full).mtimeMs;
1089
+ const ageMs = this.now() - fs42.statSync(full).mtimeMs;
971
1090
  if (ageMs < _RootUpdateService.STALE_TRANSIENT_MS) continue;
972
1091
  } catch {
973
1092
  continue;
974
1093
  }
975
1094
  }
976
1095
  try {
977
- fs32.rmSync(full, { recursive: true, force: true });
1096
+ fs42.rmSync(full, { recursive: true, force: true });
978
1097
  this.logger.debug("pruned server-root version dir", { meta: { entry } });
979
1098
  } catch (err) {
980
1099
  this.logger.warn("failed to prune server-root version dir", {
@@ -983,6 +1102,56 @@ var require_dist = __commonJS({
983
1102
  }
984
1103
  }
985
1104
  }
1105
+ /**
1106
+ * Promote-time sweep of `server-root/dev-uploads/`: keep only the
1107
+ * `keepCount` most recent entries (ordered by the self-describing
1108
+ * `-dev.<epochSeconds>` suffix; non-dev-shaped residue counts as oldest).
1109
+ * Uses the same rename-aside graveyard pattern as the addon installer —
1110
+ * a rename is a metadata op that succeeds even when a tarball is held
1111
+ * open, and residue in the graveyard is re-swept on the next promote.
1112
+ */
1113
+ sweepDevUploads(keepCount) {
1114
+ const dir = devUploadsDir(this.rootDir());
1115
+ let entries;
1116
+ try {
1117
+ entries = fs42.readdirSync(dir);
1118
+ } catch {
1119
+ return;
1120
+ }
1121
+ const graveyard = path52.join(dir, ".sweeping");
1122
+ try {
1123
+ for (const residue of fs42.readdirSync(graveyard)) {
1124
+ try {
1125
+ fs42.rmSync(path52.join(graveyard, residue), { recursive: true, force: true });
1126
+ } catch {
1127
+ }
1128
+ }
1129
+ } catch {
1130
+ }
1131
+ const versions = entries.filter((name) => !name.startsWith("."));
1132
+ const byNewestFirst = [...versions].sort(
1133
+ (a, b) => (devChannelEpoch(b) ?? -1) - (devChannelEpoch(a) ?? -1)
1134
+ );
1135
+ const doomed = byNewestFirst.slice(keepCount);
1136
+ if (doomed.length === 0) return;
1137
+ fs42.mkdirSync(graveyard, { recursive: true });
1138
+ for (const entry of doomed) {
1139
+ const aside = path52.join(graveyard, `${entry}-${this.now()}`);
1140
+ try {
1141
+ fs42.renameSync(path52.join(dir, entry), aside);
1142
+ } catch (err) {
1143
+ this.logger.warn("failed to move dev-uploads entry aside for sweep", {
1144
+ meta: { entry, error: err instanceof Error ? err.message : String(err) }
1145
+ });
1146
+ continue;
1147
+ }
1148
+ try {
1149
+ fs42.rmSync(aside, { recursive: true, force: true });
1150
+ this.logger.debug("swept dev-uploads entry", { meta: { entry } });
1151
+ } catch {
1152
+ }
1153
+ }
1154
+ }
986
1155
  /** Transient (.staging- / .evicted-) dirs younger than this are never pruned. */
987
1156
  static STALE_TRANSIENT_MS = 24 * 60 * 60 * 1e3;
988
1157
  };
@@ -1681,6 +1850,39 @@ function deriveHubUrlFromRegistry(nodes) {
1681
1850
  return `https://${host}:${HUB_API_PORT}`;
1682
1851
  }
1683
1852
 
1853
+ // src/agent-http-auth.ts
1854
+ var import_node_crypto3 = require("crypto");
1855
+ function isLoopbackAddress(addr) {
1856
+ if (!addr) return false;
1857
+ if (addr === "::1") return true;
1858
+ const v4 = addr.startsWith("::ffff:") ? addr.slice("::ffff:".length) : addr;
1859
+ const parts = v4.split(".");
1860
+ if (parts.length !== 4 || parts[0] !== "127") return false;
1861
+ return parts.every((p) => /^\d{1,3}$/.test(p));
1862
+ }
1863
+ function bearerMatchesSecret(authorization, secret) {
1864
+ if (secret.length === 0) return false;
1865
+ if (!authorization?.startsWith("Bearer ")) return false;
1866
+ const token = Buffer.from(authorization.slice("Bearer ".length));
1867
+ const expected = Buffer.from(secret);
1868
+ return token.length === expected.length && (0, import_node_crypto3.timingSafeEqual)(token, expected);
1869
+ }
1870
+ function isAuthorizedAgentRequest(input, clusterSecret) {
1871
+ if (isLoopbackAddress(input.remoteAddress)) return true;
1872
+ if (clusterSecret === null || clusterSecret.length === 0) return false;
1873
+ return bearerMatchesSecret(input.authorization, clusterSecret);
1874
+ }
1875
+ function isPairingRequest(method, urlPath) {
1876
+ const pathOnly = urlPath.split("?")[0] ?? urlPath;
1877
+ if (method === "GET") {
1878
+ return pathOnly === "/api/agent/status" || pathOnly === "/api/agent/config" || pathOnly === "/api/agent/discovered-nodes";
1879
+ }
1880
+ if (method === "POST") {
1881
+ return pathOnly === "/api/agent/config" || pathOnly === "/api/agent/restart";
1882
+ }
1883
+ return false;
1884
+ }
1885
+
1684
1886
  // src/agent-http.ts
1685
1887
  function compareVersions(a, b) {
1686
1888
  if (a === null && b === null) return 0;
@@ -1769,7 +1971,32 @@ async function createAgentHttpServer(getBroker, config) {
1769
1971
  const app = (0, import_fastify.default)({ logger: false });
1770
1972
  const cors = await import("@fastify/cors");
1771
1973
  await app.register(cors.default);
1974
+ const currentSecret = () => {
1975
+ const raw = readConfigFile(config.configPath);
1976
+ return typeof raw.secret === "string" && raw.secret.length > 0 ? raw.secret : null;
1977
+ };
1978
+ app.addHook("onRequest", async (req, reply) => {
1979
+ const pathOnly = req.url.split("?")[0] ?? req.url;
1980
+ const isProtected = pathOnly.startsWith("/api/") || pathOnly.startsWith("/health/");
1981
+ if (!isProtected) return;
1982
+ const secret = currentSecret();
1983
+ const authInput = {
1984
+ remoteAddress: req.socket.remoteAddress ?? void 0,
1985
+ ...typeof req.headers.authorization === "string" ? { authorization: req.headers.authorization } : {}
1986
+ };
1987
+ if (isAuthorizedAgentRequest(authInput, secret)) return;
1988
+ if (secret === null && isPairingRequest(req.method, pathOnly)) return;
1989
+ return reply.status(401).send({ ok: false, error: "unauthorized" });
1990
+ });
1772
1991
  app.get("/health", async (_req, reply) => {
1992
+ try {
1993
+ await getBroker().call("$agent.health");
1994
+ return { ok: true };
1995
+ } catch {
1996
+ return reply.status(503).send({ ok: false });
1997
+ }
1998
+ });
1999
+ app.get("/health/details", async (_req, reply) => {
1773
2000
  try {
1774
2001
  const result = await getBroker().call("$agent.health");
1775
2002
  return result;