@camstack/server 1.1.70 → 1.1.72

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.
@@ -1136,17 +1136,6 @@ function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx) {
1136
1136
  (0, collection_preference_js_1.persistCollectionDisabled)(configService, input.capName, updated?.disabledProviders ?? []);
1137
1137
  return { success: true };
1138
1138
  },
1139
- updateFrameworkPackage: async (input) => ps.updateFrameworkPackage({
1140
- packageName: input.packageName,
1141
- ...(input.version !== undefined ? { version: input.version } : {}),
1142
- ...(ctx.user?.username !== undefined
1143
- ? { requestedBy: ctx.user.username }
1144
- : ctx.user?.id !== undefined
1145
- ? { requestedBy: ctx.user.id }
1146
- : {}),
1147
- ...(input.deferRestart !== undefined ? { deferRestart: input.deferRestart } : {}),
1148
- runner: lifecycleRunner,
1149
- }),
1150
1139
  getVersions: async (input) => ps.getPackageVersions(input.name),
1151
1140
  restartAddon: async (input) => ar.restartAddon(input.addonId),
1152
1141
  retryLoad: async (input) => {
@@ -75,8 +75,6 @@ function createLifecycleJobRunner(deps) {
75
75
  applyAddonUpdate: deps.applyAddonUpdate,
76
76
  emit: deps.emit,
77
77
  now: nowFn,
78
- stageFramework: deps.stageFramework,
79
- requestFrameworkSwap: deps.requestFrameworkSwap,
80
78
  });
81
79
  /**
82
80
  * Set of jobIds currently executing — used to guard cancellation.
@@ -605,14 +605,6 @@ function createCapRouter_addons(getProvider, _createRemoteProxy) {
605
605
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
606
606
  return p.setCapabilityProviderEnabled(input);
607
607
  }),
608
- updateFrameworkPackage: trpc_middleware_js_1.adminProcedure
609
- .input(types_10.addonsCapability.methods.updateFrameworkPackage.input.loose())
610
- .output(types_10.addonsCapability.methods.updateFrameworkPackage.output)
611
- .mutation(async ({ input, ctx }) => {
612
- const p = requireCapProvider('addons', () => getProvider(ctx));
613
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
614
- return p.updateFrameworkPackage(input);
615
- }),
616
608
  getVersions: trpc_middleware_js_1.protectedProcedure
617
609
  .input(types_10.addonsCapability.methods.getVersions.input.loose())
618
610
  .output(types_10.addonsCapability.methods.getVersions.output)
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.enrichInputWithUserAgent = enrichInputWithUserAgent;
4
4
  exports.enrichInputWithRelayClass = enrichInputWithRelayClass;
5
+ exports.enrichHintsWithClientClass = enrichHintsWithClientClass;
5
6
  exports.wrapWebrtcSessionProviderWithRelay = wrapWebrtcSessionProviderWithRelay;
6
7
  exports.buildAppRouter = buildAppRouter;
7
8
  const addon_settings_router_js_1 = require("../core/addon-settings.router.js");
@@ -87,6 +88,52 @@ function enrichInputWithRelayClass(input, clientClass) {
87
88
  const relayOnly = FORCE_RELAY_REMOTE ? (0, client_ip_js_1.deriveRelayOnly)(clientClass) : false;
88
89
  return { ...input, relayOnly };
89
90
  }
91
+ /**
92
+ * Initial adaptive rung for a genuinely-REMOTE viewer.
93
+ *
94
+ * A `remote` (public / CGNAT-4G / internet) client cannot reliably receive the
95
+ * high tier's large keyframe over a lossy relay path — LAN can, which is why
96
+ * adaptive works on LAN but a remote adaptive viewer sees a permanent black
97
+ * frame (ICE/DTLS up, no decodable frame). So a remote adaptive session STARTS
98
+ * on the smallest tier; the broker's `AdaptiveController` upgrades from there
99
+ * once the link proves healthy. `lan` and `vpn` (Tailscale, direct overlay)
100
+ * keep the full-quality default and are never biased.
101
+ */
102
+ const REMOTE_INITIAL_ADAPTIVE_TIER = 'low';
103
+ /**
104
+ * Bias the INITIAL adaptive-tier selection for a genuinely-remote viewer by
105
+ * injecting a `prefersTier` hint the broker's `selectBestBroker` already
106
+ * honours. The hub is the only layer that knows the client's network class
107
+ * (the forked broker cannot see the HTTP request), so — exactly like
108
+ * `relayOnly` and the User-Agent — the class is read here and threaded down
109
+ * through the EXISTING `hints` cap field (no cap/schema change: `prefersTier`
110
+ * is already part of `webrtcClientHintsSchema`).
111
+ *
112
+ * Applied ONLY when:
113
+ * - the client class is `remote` (lan/vpn keep the full-quality default), and
114
+ * - the target is adaptive (`handleOffer` defaults an absent target to
115
+ * adaptive; a pinned profile/cam-stream is the operator's explicit choice
116
+ * and passes through untouched), and
117
+ * - the caller did not already set `prefersTier` (no client does today, but
118
+ * keep the trusted-injection rule symmetric with relayOnly/userAgent).
119
+ *
120
+ * Immutable — builds a NEW input (and a new `hints`), never mutates the
121
+ * caller's. On a downgrade re-offer the broker's controller-chosen tier intent
122
+ * overrides this hint, so a healthy remote session can still climb above `low`.
123
+ */
124
+ function enrichHintsWithClientClass(input, clientClass) {
125
+ if (clientClass !== 'remote')
126
+ return input;
127
+ const isAdaptive = input.target === undefined || input.target.kind === 'adaptive';
128
+ if (!isAdaptive)
129
+ return input;
130
+ if (input.hints?.prefersTier !== undefined)
131
+ return input;
132
+ return {
133
+ ...input,
134
+ hints: { ...input.hints, prefersTier: REMOTE_INITIAL_ADAPTIVE_TIER },
135
+ };
136
+ }
90
137
  /**
91
138
  * Per-request wrapper around the resolved `webrtc-session` broker singleton.
92
139
  *
@@ -125,8 +172,8 @@ function wrapWebrtcSessionProviderWithRelay(provider, ctx) {
125
172
  const clientClass = (0, client_ip_js_1.classifyClientRequest)(ctx.req);
126
173
  return {
127
174
  ...provider,
128
- createSession: (input) => provider.createSession(enrichInputWithRelayClass(enrichInputWithUserAgent(input, userAgent), clientClass)),
129
- handleOffer: (input) => provider.handleOffer(enrichInputWithRelayClass(enrichInputWithUserAgent(input, userAgent), clientClass)),
175
+ createSession: (input) => provider.createSession(enrichHintsWithClientClass(enrichInputWithRelayClass(enrichInputWithUserAgent(input, userAgent), clientClass), clientClass)),
176
+ handleOffer: (input) => provider.handleOffer(enrichHintsWithClientClass(enrichInputWithRelayClass(enrichInputWithUserAgent(input, userAgent), clientClass), clientClass)),
130
177
  };
131
178
  }
132
179
  /**
@@ -4,7 +4,6 @@ exports.PostBootService = void 0;
4
4
  const node_crypto_1 = require("node:crypto");
5
5
  const system_1 = require("@camstack/system");
6
6
  const types_1 = require("@camstack/types");
7
- const resume_framework_swap_js_1 = require("./resume-framework-swap.js");
8
7
  const reconcile_lifecycle_jobs_js_1 = require("./reconcile-lifecycle-jobs.js");
9
8
  class PostBootService {
10
9
  eventBus;
@@ -57,24 +56,12 @@ class PostBootService {
57
56
  // restart, …). `readPendingRestart` clears the marker atomically so
58
57
  // we never re-fire on a crash-loop boot.
59
58
  this.emitRestartCompletedIfPending(dataPath);
60
- // If a framework swap was applied on the previous boot and left a
61
- // `.framework-swap-confirm.json` marker, mark the journal job done and
62
- // delete the marker + backups now that the hub is healthy. This also
63
- // disarms the crash-loop rollback (the NEXT boot won't roll back a
64
- // healthy update).
65
- await this.resumeFrameworkSwapIfPending(dataPath);
66
- // Then re-drive every other non-terminal lifecycle job (addon task phases)
67
- // from its on-disk checkpoint. Order matters: framework resume FIRST (marks
68
- // the framework task `applied`→`done`), THEN addon reconcile (which leaves
69
- // framework tasks untouched).
59
+ // Re-drive every non-terminal lifecycle job (addon task phases) from its
60
+ // on-disk checkpoint. Framework tasks are no longer applied by the engine
61
+ // (single-copy collapse framework updates ship via applyServerUpdate);
62
+ // any stray framework task is skipped during reconcile.
70
63
  await this.reconcileLifecycleJobsIfAny();
71
64
  }
72
- async resumeFrameworkSwapIfPending(dataDir) {
73
- const result = await (0, resume_framework_swap_js_1.resumeFrameworkSwapJob)(dataDir);
74
- if (result.resumed) {
75
- this.logger.info('Framework update completed', { meta: { jobId: result.jobId } });
76
- }
77
- }
78
65
  async reconcileLifecycleJobsIfAny() {
79
66
  const { resumed, failed } = await (0, reconcile_lifecycle_jobs_js_1.reconcileLifecycleJobsAtBoot)();
80
67
  if (resumed + failed > 0) {
@@ -8,12 +8,13 @@
8
8
  * resumes from its checkpoint (reusing a still-valid `stagedPath` or
9
9
  * re-fetching, then applying).
10
10
  *
11
- * Best-effort, mirrors `resume-framework-swap.ts`: this NEVER throws — any error
12
- * (runner not initialized, journal corruption, …) is swallowed and reported as
13
- * `{ resumed: 0, failed: 0 }` so the post-boot path can never crash the hub.
11
+ * Best-effort: this NEVER throws — any error (runner not initialized, journal
12
+ * corruption, …) is swallowed and reported as `{ resumed: 0, failed: 0 }` so the
13
+ * post-boot path can never crash the hub.
14
14
  *
15
- * Ordering note: the framework `applied`→`done` resume (`resumeFrameworkSwapJob`)
16
- * MUST run BEFORE this reconcile deliberately leaves framework tasks untouched.
15
+ * Framework tasks are no longer applied by the engine (single-copy collapse —
16
+ * framework updates ship via `applyServerUpdate`); any stray non-terminal
17
+ * framework task is marked `skipped` during reconcile so the job can finalize.
17
18
  */
18
19
  Object.defineProperty(exports, "__esModule", { value: true });
19
20
  exports.reconcileLifecycleJobsAtBoot = reconcileLifecycleJobsAtBoot;
@@ -36,8 +36,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.AddonPackageService = exports.FRAMEWORK_PACKAGES = exports.SYSTEM_PACKAGE = void 0;
37
37
  exports.isFrameworkPackage = isFrameworkPackage;
38
38
  exports.extractTgzStripped = extractTgzStripped;
39
- exports.sweepStaleFrameworkBackups = sweepStaleFrameworkBackups;
40
- exports.swapInFrameworkPackage = swapInFrameworkPackage;
41
39
  const fs = __importStar(require("node:fs"));
42
40
  const path = __importStar(require("node:path"));
43
41
  const os = __importStar(require("node:os"));
@@ -911,9 +909,9 @@ class AddonPackageService {
911
909
  /**
912
910
  * Download the npm tarball for `name@version` as a Buffer.
913
911
  *
914
- * Wraps the module-level `httpsDownloadTarball` for use by the
915
- * lifecycle job engine (TarballFetcher signature). The `signal` is
916
- * forwarded to the underlying fetch calls so the AbortController
912
+ * Resolves the tarball URL from the registry packument, then fetches it,
913
+ * for use by the lifecycle job engine (TarballFetcher signature). The
914
+ * `signal` is forwarded to the underlying fetch calls so the AbortController
917
915
  * timeout wired in LifecycleJobEngine fires correctly.
918
916
  */
919
917
  async fetchAddonTarball(name, version, signal) {
@@ -1040,210 +1038,6 @@ class AddonPackageService {
1040
1038
  }));
1041
1039
  return rows;
1042
1040
  }
1043
- /**
1044
- * Update one of the framework packages (manifest `camstack.system:
1045
- * true`) and schedule a hub restart.
1046
- *
1047
- * Steps:
1048
- * 1. Allow-list the package name (refuses anything not framework).
1049
- * 2. Resolve `'latest'`/`undefined` to a concrete version via `npm view`.
1050
- * 3. Run `npm install --prefix <appRoot> <name>@<version> --no-save`.
1051
- * 4. Write a `.restart-pending` marker (kind: `framework-update`).
1052
- * 5. Emit `system.restarting` event.
1053
- * 6. `scheduleSelfRestart({ delayMs: 500 })` — gives the cap method
1054
- * time to return before the WS drops.
1055
- *
1056
- * Returns BEFORE the exit fires so the admin UI receives `restartingAt`
1057
- * and can pivot to the reconnect overlay.
1058
- */
1059
- /**
1060
- * Install the framework by SWAPPING each package's own directory, exactly
1061
- * like every addon is installed (download the single-package tarball →
1062
- * extract → atomic dir swap). We NEVER run `npm install --prefix <appRoot>`.
1063
- *
1064
- * Why: `npm install --prefix /data` manages the WHOLE node_modules tree — it
1065
- * prunes every package not in @camstack/system's closure (the addons +
1066
- * ui-library), deletes the stray manifest.json, and (via temp-install + merge
1067
- * variants) can clobber working native bindings. A per-package tarball swap
1068
- * touches only that package's folder; nothing else can be pruned, deleted, or
1069
- * corrupted.
1070
- *
1071
- * Swaps the @camstack framework packages versioned in lockstep with system:
1072
- * the pure-JS libs (types, sdk) AND @camstack/shm-ring. shm-ring is native
1073
- * but SAFE to swap because it ships its compiled `.node` prebuilds INSIDE the
1074
- * npm tarball (prebuildify + node-gyp-build resolves the matching one at
1075
- * require-time) — a bare extract is sufficient, no build or download.
1076
- *
1077
- * It does NOT swap download/compile-on-install natives such as `better-sqlite3`
1078
- * (prebuild-install fetches the binary from GitHub releases at install time —
1079
- * a bare extract would leave it without a binding). Those are third-party,
1080
- * hoisted, change only across majors, and are updated by an image redeploy.
1081
- * System's own deps stay exactly where they are, bindings intact.
1082
- */
1083
- async installFrameworkPackages(packageName, toVersion, appRoot) {
1084
- const registry = process.env['CAMSTACK_NPM_REGISTRY'];
1085
- // @camstack/shm-ring is native but ships prebuilds in its tarball → safe.
1086
- const lockstepDeps = ['@camstack/types', '@camstack/sdk', '@camstack/shm-ring'];
1087
- const swapTargets = [packageName, ...lockstepDeps];
1088
- // Self-heal: a previous run killed mid-swap (e.g. process restart while a
1089
- // slow download was in flight) can leave a `<pkg>.fw-bak` backup behind.
1090
- // It's inert (not a valid package the loader picks up) but sweep it so the
1091
- // tree stays clean.
1092
- sweepStaleFrameworkBackups(appRoot);
1093
- const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'camstack-fw-'));
1094
- try {
1095
- for (const pkg of swapTargets) {
1096
- const isPrimary = pkg === packageName;
1097
- try {
1098
- const tgzPath = await packTarball(pkg, toVersion, tmpRoot, registry);
1099
- const extractDir = fs.mkdtempSync(path.join(tmpRoot, 'x-'));
1100
- await execFileAsync('tar', ['-xzf', tgzPath, '-C', extractDir], { timeout: 60_000 });
1101
- const result = swapInFrameworkPackage(path.join(extractDir, 'package'), appRoot);
1102
- this.logger.info('updateFrameworkPackage: package swapped', {
1103
- meta: { name: result.name, version: result.version },
1104
- });
1105
- }
1106
- catch (err) {
1107
- if (isPrimary)
1108
- throw err;
1109
- // A lockstep dep may not be published at this exact version (or the
1110
- // download flaked) — keep the existing copy rather than fail the whole
1111
- // framework update. System is the one that MUST succeed.
1112
- this.logger.warn(`updateFrameworkPackage: lockstep dep ${pkg}@${toVersion} not swapped`, {
1113
- meta: { error: (0, types_1.errMsg)(err) },
1114
- });
1115
- }
1116
- }
1117
- }
1118
- finally {
1119
- fs.rmSync(tmpRoot, { recursive: true, force: true });
1120
- }
1121
- }
1122
- /**
1123
- * Resolve a version specifier (`'latest'`, semver tag, exact) for a
1124
- * framework package to a concrete version via `npm view`.
1125
- * Delegates to the module-level `resolveNpmVersion` helper.
1126
- * Exposed as a public method so the lifecycle job engine wiring in
1127
- * cap-providers can pass it as `resolveVersion` without duplicating the
1128
- * npm-view/manifest logic.
1129
- */
1130
- async resolveFrameworkVersion(packageName, versionSpec) {
1131
- return resolveNpmVersion(packageName, versionSpec, process.env['CAMSTACK_NPM_REGISTRY']);
1132
- }
1133
- /**
1134
- * Return the currently-installed version of a framework package, or `null`
1135
- * when it cannot be determined (not installed / no manifest).
1136
- * Delegates to the module-level `readResolvedPackageManifest` helper.
1137
- * Exposed as a public method for use by the lifecycle job engine wiring.
1138
- */
1139
- currentFrameworkVersionOf(packageName) {
1140
- const manifest = readResolvedPackageManifest(packageName);
1141
- if (manifest !== null && typeof manifest['version'] === 'string') {
1142
- return manifest['version'];
1143
- }
1144
- return null;
1145
- }
1146
- /**
1147
- * Update a framework package via the staged launcher-swap engine.
1148
- *
1149
- * New path (when `runner` is provided):
1150
- * 1. Resolve toVersion via `npm view`.
1151
- * 2. Delegate to `runner.startJob` with a `target:'framework'` task.
1152
- * The engine calls `stageFramework` → downloads+stages the 4 lockstep
1153
- * packages → calls the wired `requestFrameworkSwap`
1154
- * (`requestFrameworkSwapAndRestart`), which writes the
1155
- * `.pending-framework-swap.json` marker AND schedules the self-restart.
1156
- * The hub exits; the launcher applies the swap on the next boot. The
1157
- * restart is owned by that seam, so this method does NOT schedule one.
1158
- * 3. Return `{ packageName, fromVersion, toVersion, restartingAt }`.
1159
- * `restartingAt = 0` only on the legacy in-hub path when `deferRestart`
1160
- * is set (engine path always restarts once the framework is staged).
1161
- *
1162
- * Legacy path (when no `runner`): keeps the original in-hub live-swap for
1163
- * backward compatibility with callers (e.g. tests) that have not wired the
1164
- * engine deps.
1165
- */
1166
- async updateFrameworkPackage(input) {
1167
- const { packageName } = input;
1168
- if (packageName !== exports.SYSTEM_PACKAGE) {
1169
- throw new Error(`updateFrameworkPackage: '${packageName}' is not a framework package. Allowed: ${exports.SYSTEM_PACKAGE}`);
1170
- }
1171
- const fromVersion = this.currentFrameworkVersionOf(packageName) ?? 'unknown';
1172
- const requestedVersion = input.version ?? 'latest';
1173
- const toVersion = await this.resolveFrameworkVersion(packageName, requestedVersion);
1174
- // ── New staged-swap engine path ──────────────────────────────────
1175
- if (input.runner !== undefined) {
1176
- this.logger.info('updateFrameworkPackage: delegating to staged swap engine', {
1177
- meta: { packageName, fromVersion, toVersion, deferRestart: input.deferRestart ?? false },
1178
- });
1179
- await input.runner.startJob({
1180
- kind: 'update',
1181
- targets: [{ name: packageName, version: toVersion, target: 'framework' }],
1182
- createdBy: input.requestedBy ?? 'system',
1183
- });
1184
- // The engine's framework task already wrote the pending-swap marker AND
1185
- // scheduled the self-restart via the wired `requestFrameworkSwap`
1186
- // (`requestFrameworkSwapAndRestart`) — that is the single restart owner,
1187
- // so we must NOT schedule a second one here. We only surface the
1188
- // pre-restart toast event for the interactive single-update path.
1189
- if (input.deferRestart !== true) {
1190
- this.eventBusService.emit({
1191
- id: (0, node_crypto_1.randomUUID)(),
1192
- timestamp: new Date(),
1193
- source: { type: 'core', id: 'addon-package-service' },
1194
- category: types_1.EventCategory.SystemRestarting,
1195
- data: {
1196
- kind: 'framework-update',
1197
- packageName,
1198
- fromVersion,
1199
- toVersion,
1200
- requestedAt: Date.now(),
1201
- },
1202
- });
1203
- }
1204
- const restartingAt = input.deferRestart === true ? 0 : Date.now() + 500;
1205
- return { packageName, fromVersion, toVersion, restartingAt };
1206
- }
1207
- // ── Legacy in-hub live-swap path (no runner injected) ───────────
1208
- const appRoot = resolveFrameworkPackageAppRoot(packageName, this.logger);
1209
- this.logger.info('updateFrameworkPackage: installing (legacy in-hub path)', {
1210
- meta: { packageName, fromVersion, toVersion, appRoot },
1211
- });
1212
- await this.installFrameworkPackages(packageName, toVersion, appRoot);
1213
- if (input.deferRestart === true) {
1214
- this.logger.info(`updateFrameworkPackage(${packageName}@${toVersion}): install done, restart deferred`);
1215
- // Sentinel: 0 signals "no restart scheduled" to the caller
1216
- return { packageName, fromVersion, toVersion, restartingAt: 0 };
1217
- }
1218
- const restartingAt = Date.now();
1219
- const markerPayload = {
1220
- kind: 'framework-update',
1221
- packageName,
1222
- fromVersion,
1223
- toVersion,
1224
- requestedAt: restartingAt,
1225
- ...(input.requestedBy !== undefined ? { requestedBy: input.requestedBy } : {}),
1226
- };
1227
- try {
1228
- (0, system_1.writePendingRestart)(this.resolveDataDir(), markerPayload);
1229
- }
1230
- catch (err) {
1231
- // The npm install already completed — the restart will still
1232
- // pick up the new version, just without the completion toast.
1233
- this.logger.warn('Failed to write restart marker after framework update', {
1234
- meta: { error: (0, types_1.errMsg)(err) },
1235
- });
1236
- }
1237
- this.eventBusService.emit({
1238
- id: (0, node_crypto_1.randomUUID)(),
1239
- timestamp: new Date(),
1240
- source: { type: 'core', id: 'addon-package-service' },
1241
- category: types_1.EventCategory.SystemRestarting,
1242
- data: markerPayload,
1243
- });
1244
- (0, system_1.scheduleSelfRestart)({ delayMs: 500 });
1245
- return { packageName, fromVersion, toVersion, restartingAt };
1246
- }
1247
1041
  // =========================================================================
1248
1042
  // Reload
1249
1043
  // =========================================================================
@@ -1333,10 +1127,12 @@ class AddonPackageService {
1333
1127
  }
1334
1128
  /**
1335
1129
  * Run auto-update: check each installed package against its configured
1336
- * channel, then route ALL packages that have a newer version through ONE
1337
- * durable engine bulk job (F3 Task 5). The framework (`@camstack/system`) is
1338
- * auto-detected by the engine and ordered LAST; auto-updates survive a reboot
1339
- * via the boot reconcile, exactly like a manual "Update all".
1130
+ * channel, then route ALL addon packages that have a newer version through ONE
1131
+ * durable engine bulk job (F3 Task 5); auto-updates survive a reboot via the
1132
+ * boot reconcile, exactly like a manual "Update all". The framework
1133
+ * (`@camstack/system`) is NOT applied by the engine it ships via
1134
+ * `applyServerUpdate` (single-copy collapse), so a swept framework target is
1135
+ * skipped by the engine.
1340
1136
  *
1341
1137
  * Candidate selection (channel resolution + npm metadata fetch + "is there a
1342
1138
  * newer version?" comparison) is unchanged from the previous per-item loop;
@@ -1394,9 +1190,10 @@ class AddonPackageService {
1394
1190
  // Execute as ONE durable engine bulk job. The runner singleton is resolved
1395
1191
  // lazily via a dynamic import (the singleton module transitively imports
1396
1192
  // SYSTEM_PACKAGE back from this module) to avoid an init-order cycle. The
1397
- // singleton is initialized at boot, before the scheduler timer fires. The
1398
- // framework target, if present, is ordered last by the engine. Wrap so a
1399
- // failure is logged, not thrown out of the timer callback.
1193
+ // singleton is initialized at boot, before the scheduler timer fires. A
1194
+ // framework target, if present, is skipped by the engine (framework updates
1195
+ // ship via applyServerUpdate). Wrap so a failure is logged, not thrown out
1196
+ // of the timer callback.
1400
1197
  try {
1401
1198
  const { getLifecycleRunner } = await Promise.resolve().then(() => __importStar(require('../lifecycle/lifecycle-runner.singleton.js')));
1402
1199
  const runner = getLifecycleRunner();
@@ -1754,7 +1551,7 @@ async function extractTgzStripped(tgz, destDir) {
1754
1551
  }
1755
1552
  }
1756
1553
  // ---------------------------------------------------------------------------
1757
- // Framework live-update helpers
1554
+ // npm tarball / version helpers
1758
1555
  // ---------------------------------------------------------------------------
1759
1556
  /**
1760
1557
  * Build the npm CLI args that pin every relevant registry to
@@ -1763,179 +1560,16 @@ async function extractTgzStripped(tgz, destDir) {
1763
1560
  * user-home `.npmrc` files commonly declare
1764
1561
  * `@camstack:registry=https://registry.npmjs.org/`, and that scoped
1765
1562
  * entry takes precedence over the plain `--registry` CLI flag for
1766
- * `@camstack/*` lookups — which is exactly the path framework-update
1767
- * traverses.
1563
+ * `@camstack/*` lookups.
1768
1564
  *
1769
- * Without this, the e2e suite's verdaccio gets bypassed even with
1770
- * `CAMSTACK_NPM_REGISTRY` set, AND in production any operator running
1771
- * their own private npm proxy via `@camstack:registry` would have
1772
- * `updateFrameworkPackage` silently route around it.
1565
+ * Without this, an operator running their own private npm proxy via
1566
+ * `@camstack:registry` would have package downloads silently route around it.
1773
1567
  */
1774
1568
  function buildNpmRegistryArgs(registry) {
1775
1569
  if (registry === undefined || registry.length === 0)
1776
1570
  return [];
1777
1571
  return ['--registry', registry, `--@camstack:registry=${registry}`];
1778
1572
  }
1779
- /**
1780
- * Remove any leftover `<pkg>.fw-bak` backup dirs under `appRoot/node_modules/
1781
- * @camstack` — debris from a framework swap that was interrupted before its
1782
- * own cleanup ran. Best-effort; returns the swept names.
1783
- */
1784
- function sweepStaleFrameworkBackups(appRoot) {
1785
- const scopeDir = path.join(appRoot, 'node_modules', '@camstack');
1786
- if (!fs.existsSync(scopeDir))
1787
- return [];
1788
- const swept = [];
1789
- for (const entry of fs.readdirSync(scopeDir, { withFileTypes: true })) {
1790
- if (!entry.isDirectory() || !entry.name.endsWith('.fw-bak'))
1791
- continue;
1792
- try {
1793
- fs.rmSync(path.join(scopeDir, entry.name), { recursive: true, force: true });
1794
- swept.push(entry.name);
1795
- }
1796
- catch {
1797
- // ignore — inert leftover, next sweep retries
1798
- }
1799
- }
1800
- return swept;
1801
- }
1802
- /**
1803
- * Download the `.tgz` for an EXACT `pkg@version` into `destRoot`, returning its
1804
- * path. Prefers a direct HTTPS GET (registry metadata → tarball URL) because
1805
- * `fetch` + `AbortSignal.timeout` aborts RELIABLY on a slow/flaky network — an
1806
- * `npm pack` child can stall for minutes past its `execFile` timeout (npm holds
1807
- * its stdio pipes open, so the awaited promise never settles), which would wedge
1808
- * the whole framework update. Falls back to `npm pack` (SIGKILL on timeout) only
1809
- * when the HTTPS path fails (private registry quirks, auth, etc.).
1810
- */
1811
- async function packTarball(pkg, version, destRoot, registry) {
1812
- const dir = fs.mkdtempSync(path.join(destRoot, 'dl-'));
1813
- try {
1814
- return await httpsDownloadTarball(pkg, version, dir, registry);
1815
- }
1816
- catch (httpErr) {
1817
- const args = [
1818
- 'pack',
1819
- `${pkg}@${version}`,
1820
- '--pack-destination',
1821
- dir,
1822
- ...buildNpmRegistryArgs(registry),
1823
- ];
1824
- // SIGKILL (not the default SIGTERM): npm can ignore SIGTERM while a
1825
- // grandchild holds the pipes — SIGKILL guarantees the timeout fires.
1826
- await execFileAsync('npm', args, { timeout: 60_000, killSignal: 'SIGKILL' });
1827
- const tgz = fs.readdirSync(dir).find((f) => f.endsWith('.tgz'));
1828
- if (tgz === undefined) {
1829
- throw new Error(`download failed for ${pkg}@${version}: ${(0, types_1.errMsg)(httpErr)}`, {
1830
- cause: httpErr,
1831
- });
1832
- }
1833
- return path.join(dir, tgz);
1834
- }
1835
- }
1836
- /**
1837
- * Direct HTTPS download of an exact `pkg@version` tarball. Resolves the tarball
1838
- * URL from the registry packument, then streams it to a file. Every network
1839
- * call is bounded by an `AbortSignal.timeout` so a DNS/registry stall fails fast
1840
- * instead of hanging the framework update.
1841
- */
1842
- async function httpsDownloadTarball(pkg, version, dir, registry) {
1843
- const reg = (registry ?? 'https://registry.npmjs.org').replace(/\/+$/, '');
1844
- const metaUrl = `${reg}/${encodeURIComponent(pkg).replace(/^%40/, '@')}`;
1845
- const metaRes = await fetch(metaUrl, { signal: AbortSignal.timeout(20_000) });
1846
- if (!metaRes.ok)
1847
- throw new Error(`registry GET ${metaUrl} → ${metaRes.status}`);
1848
- const meta = (await metaRes.json());
1849
- const tarballUrl = meta.versions?.[version]?.dist?.tarball;
1850
- if (typeof tarballUrl !== 'string') {
1851
- throw new Error(`no tarball url for ${pkg}@${version}`);
1852
- }
1853
- const tarRes = await fetch(tarballUrl, { signal: AbortSignal.timeout(120_000) });
1854
- if (!tarRes.ok)
1855
- throw new Error(`tarball GET ${tarballUrl} → ${tarRes.status}`);
1856
- const buf = Buffer.from(await tarRes.arrayBuffer());
1857
- const outPath = path.join(dir, `${pkg.replace('@', '').replace('/', '-')}-${version}.tgz`);
1858
- fs.writeFileSync(outPath, buf);
1859
- return outPath;
1860
- }
1861
- /**
1862
- * Atomically replace one `@camstack/*` package directory in
1863
- * `appRoot/node_modules` with the contents of an extracted npm package dir
1864
- * (`stagedPackageDir` = the `package/` folder from a `.tgz`).
1865
- *
1866
- * This is the framework-update primitive: it touches ONLY the target package's
1867
- * folder. The current copy is renamed to a sibling `.fw-bak` backup first and
1868
- * restored if the copy fails, so a crash mid-swap can't leave a half-written
1869
- * package. Refuses anything outside the `@camstack/` scope as a safety guard.
1870
- * Returns the installed package name + version.
1871
- */
1872
- function swapInFrameworkPackage(stagedPackageDir, appRoot) {
1873
- const pkgJsonPath = path.join(stagedPackageDir, 'package.json');
1874
- if (!fs.existsSync(pkgJsonPath)) {
1875
- throw new Error(`swapInFrameworkPackage: no package.json in ${stagedPackageDir}`);
1876
- }
1877
- const parsed = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
1878
- const pkg = parsed;
1879
- if (typeof pkg.name !== 'string' || typeof pkg.version !== 'string') {
1880
- throw new Error(`swapInFrameworkPackage: invalid package.json in ${stagedPackageDir}`);
1881
- }
1882
- if (!pkg.name.startsWith('@camstack/')) {
1883
- throw new Error(`swapInFrameworkPackage: refusing non-@camstack package ${pkg.name}`);
1884
- }
1885
- const targetDir = path.join(appRoot, 'node_modules', pkg.name);
1886
- const backupDir = `${targetDir}.fw-bak`;
1887
- fs.rmSync(backupDir, { recursive: true, force: true });
1888
- const hadExisting = fs.existsSync(targetDir);
1889
- if (hadExisting)
1890
- fs.renameSync(targetDir, backupDir);
1891
- try {
1892
- fs.mkdirSync(path.dirname(targetDir), { recursive: true });
1893
- fs.cpSync(stagedPackageDir, targetDir, { recursive: true });
1894
- }
1895
- catch (err) {
1896
- // Roll back to the previous copy so a failed swap never leaves the
1897
- // package missing or half-written.
1898
- fs.rmSync(targetDir, { recursive: true, force: true });
1899
- if (hadExisting)
1900
- fs.renameSync(backupDir, targetDir);
1901
- throw err;
1902
- }
1903
- fs.rmSync(backupDir, { recursive: true, force: true });
1904
- return { name: pkg.name, version: pkg.version };
1905
- }
1906
- /**
1907
- * Resolve the directory whose `node_modules/<pkg>/` holds the currently-
1908
- * installed copy of a framework package. `npm install --prefix <appRoot>`
1909
- * will then update that exact copy in place.
1910
- *
1911
- * Strategy: ask Node's resolver where it finds the package today, then walk
1912
- * up to the `node_modules/`-parent. This matches whatever resolution path
1913
- * the running hub actually uses (server-local node_modules in prod;
1914
- * workspace-root in dev; bundled in Electron) without hard-coding either.
1915
- *
1916
- * Test knob: `CAMSTACK_FRAMEWORK_APP_ROOT_OVERRIDE` short-circuits the walk
1917
- * and returns the env-supplied path. Used by the e2e suite to redirect the
1918
- * `npm install --prefix` side-effects into an isolated temp dir instead of
1919
- * the workspace's `server/backend/node_modules/`. Never set in production.
1920
- */
1921
- function resolveFrameworkPackageAppRoot(packageName, logger) {
1922
- const override = process.env['CAMSTACK_FRAMEWORK_APP_ROOT_OVERRIDE'];
1923
- if (override !== undefined && override.length > 0) {
1924
- return override;
1925
- }
1926
- const resolved = require.resolve(`${packageName}/package.json`);
1927
- // …/<appRoot>/node_modules/<scope>/<name>/package.json
1928
- // walk up: package.json → name → scope → node_modules → appRoot
1929
- let dir = path.dirname(resolved);
1930
- while (dir !== path.dirname(dir)) {
1931
- if (path.basename(dir) === 'node_modules') {
1932
- return path.dirname(dir);
1933
- }
1934
- dir = path.dirname(dir);
1935
- }
1936
- logger.warn(`Could not resolve appRoot for ${packageName}; falling back to process.cwd()`);
1937
- return process.cwd();
1938
- }
1939
1573
  /**
1940
1574
  * Read a framework package's `package.json`, resolved however the
1941
1575
  * running hub actually loads it — workspace symlink in dev, a real
@@ -1946,11 +1580,6 @@ function resolveFrameworkPackageAppRoot(packageName, logger) {
1946
1580
  * `@camstack/sdk`, `@camstack/ui-library`) make that throw — so we
1947
1581
  * fall back to resolving the package's main entry and walking up to
1948
1582
  * the first `package.json` whose `name` matches.
1949
- *
1950
- * This is deliberately independent of `resolveFrameworkPackageAppRoot`:
1951
- * that walk only finds a real `node_modules`-parent, which doesn't
1952
- * exist for workspace-symlinked packages in dev — the cause of the
1953
- * `vunknown` version label in the System Packages UI.
1954
1583
  */
1955
1584
  function readResolvedPackageManifest(packageName) {
1956
1585
  try {