@camstack/server 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/api/addon-upload.js +35 -30
- package/dist/api/core/auth.router.js +285 -38
- package/dist/api/core/cap-providers.js +25 -3
- package/dist/api/core/server-management.provider.js +14 -0
- package/dist/api/health/health.routes.js +5 -0
- package/dist/api/static/spa-static.js +69 -0
- package/dist/api/trpc/generated-cap-mounts.js +20 -1
- package/dist/api/trpc/generated-cap-routers.js +783 -707
- package/dist/api/trpc/trpc.router.js +5 -2
- package/dist/core/addon/addon-package.service.js +16 -19
- package/dist/core/addon/package-dir-utils.js +140 -0
- package/dist/core/moleculer/moleculer.service.js +8 -0
- package/dist/core/server-update/server-update.service.js +81 -0
- package/dist/core/topology/topology-emitter.service.js +6 -2
- package/dist/main.js +126 -13
- package/dist/manual-boot.js +11 -1
- package/dist/server-root/index.js +1124 -0
- package/dist/starter.js +81 -0
- package/package.json +7 -4
- package/dist/core/addon/framework-live-sync.js +0 -344
|
@@ -6,6 +6,7 @@ exports.buildAppRouter = buildAppRouter;
|
|
|
6
6
|
const addon_settings_router_js_1 = require("../core/addon-settings.router.js");
|
|
7
7
|
const auth_router_js_1 = require("../core/auth.router.js");
|
|
8
8
|
const cap_providers_js_1 = require("../core/cap-providers.js");
|
|
9
|
+
const server_management_provider_js_1 = require("../core/server-management.provider.js");
|
|
9
10
|
const capabilities_router_js_1 = require("../core/capabilities.router.js");
|
|
10
11
|
const event_bus_proxy_router_js_1 = require("../core/event-bus-proxy.router.js");
|
|
11
12
|
const hwaccel_router_js_1 = require("../core/hwaccel.router.js");
|
|
@@ -83,7 +84,9 @@ function buildServerProviders(services) {
|
|
|
83
84
|
system: wrap(() => (0, cap_providers_js_1.buildSystemProvider)(services.featureService, services.capabilityRegistry)),
|
|
84
85
|
toast: wrap((ctx) => (0, cap_providers_js_1.buildToastProvider)(services.toastService, ctx)),
|
|
85
86
|
integrations: wrap(() => (0, cap_providers_js_1.buildIntegrationsProvider)(services.addonRegistry, services.eventBus, services.loggingService, services.capabilityRegistry)),
|
|
86
|
-
nodes: wrap(() => (0, cap_providers_js_1.buildNodesProvider)(services.agentRegistry, services.moleculer, services.addonRegistry)),
|
|
87
|
+
nodes: wrap(() => (0, cap_providers_js_1.buildNodesProvider)(services.agentRegistry, services.moleculer, services.addonRegistry, (0, cap_providers_js_1.createNodeRootPackageLookup)(services.moleculer, services.serverUpdateService))),
|
|
88
|
+
// server-management — runtime-updatable root package (phase 1: hub only).
|
|
89
|
+
'server-management': wrap(() => (0, server_management_provider_js_1.buildServerManagementProvider)(services.serverUpdateService)),
|
|
87
90
|
addons: wrap((ctx) => (0, cap_providers_js_1.buildAddonsProvider)(services.addonRegistry, services.addonPackageService, services.loggingService, services.moleculer, services.configService, ctx)),
|
|
88
91
|
// webrtc-session — the resolved broker singleton, wrapped per request to
|
|
89
92
|
// inject the server-read User-Agent into signaling attribution (the broker
|
|
@@ -163,7 +166,7 @@ function buildCapabilityRouters(services) {
|
|
|
163
166
|
// clusterNodes — fixed core API. Write-side purge for the durable
|
|
164
167
|
// offline-node history (Track A "Forget node"); read side is push-only.
|
|
165
168
|
clusterNodes: (0, cluster_nodes_router_js_1.createClusterNodesRouter)(services.agentRegistry),
|
|
166
|
-
auth: (0, auth_router_js_1.createAuthRouter)(services.authService, services.capabilityRegistry),
|
|
169
|
+
auth: (0, auth_router_js_1.createAuthRouter)(services.authService, services.capabilityRegistry, services.moleculer),
|
|
167
170
|
// NOT MOUNTED — `mount: { kind: 'skip' }` legacy provider shapes
|
|
168
171
|
// (positional args / sync returns) that don't match the codegen
|
|
169
172
|
// routers' {input}-object + Promise<T> contract. The runtime builder
|
|
@@ -44,7 +44,7 @@ const os = __importStar(require("node:os"));
|
|
|
44
44
|
const node_child_process_1 = require("node:child_process");
|
|
45
45
|
const node_util_1 = require("node:util");
|
|
46
46
|
const node_crypto_1 = require("node:crypto");
|
|
47
|
-
const
|
|
47
|
+
const package_dir_utils_js_1 = require("./package-dir-utils.js");
|
|
48
48
|
const types_1 = require("@camstack/types");
|
|
49
49
|
const system_1 = require("@camstack/system");
|
|
50
50
|
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
@@ -54,24 +54,21 @@ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
|
54
54
|
*/
|
|
55
55
|
exports.SYSTEM_PACKAGE = '@camstack/system';
|
|
56
56
|
/**
|
|
57
|
-
* Host-provided framework packages that a
|
|
58
|
-
*
|
|
59
|
-
* addon-
|
|
60
|
-
* the server — delivering the new code with NO image rebuild.
|
|
57
|
+
* Host-provided framework packages that take a special path through
|
|
58
|
+
* `camstack deploy` (the addon-upload route) instead of the normal
|
|
59
|
+
* addon-install path.
|
|
61
60
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* prebuild the dist-only mirror cannot rebuild — it must go through the
|
|
70
|
-
* native-aware install path, not this JS-dist swap.
|
|
61
|
+
* Since the runtime-updatable root package shipped (2026-07-12 design), a
|
|
62
|
+
* framework upload on a DEPLOYED hub is rejected with guidance to update the
|
|
63
|
+
* `@camstack/server` root instead — the hub main + framework deps update as
|
|
64
|
+
* ONE npm closure via the `server-management` cap. In a WORKSPACE checkout
|
|
65
|
+
* the upload still triggers a plain restart: the hub resolves the framework
|
|
66
|
+
* from `packages/<pkg>/dist`, which the CLI rebuilt before uploading, so the
|
|
67
|
+
* dev loop (`camstack deploy packages/system`) is unchanged.
|
|
71
68
|
*
|
|
72
69
|
* Membership is by package NAME, independent of the `camstack.system` manifest
|
|
73
70
|
* flag (`@camstack/types` declares `system: false` yet is a framework package
|
|
74
|
-
* here) — that flag drives Admin-UI grouping; this gate drives
|
|
71
|
+
* here) — that flag drives Admin-UI grouping; this gate drives upload routing.
|
|
75
72
|
*/
|
|
76
73
|
exports.FRAMEWORK_PACKAGES = [
|
|
77
74
|
exports.SYSTEM_PACKAGE,
|
|
@@ -79,8 +76,8 @@ exports.FRAMEWORK_PACKAGES = [
|
|
|
79
76
|
'@camstack/sdk',
|
|
80
77
|
];
|
|
81
78
|
/**
|
|
82
|
-
* Returns true for a host-provided framework package
|
|
83
|
-
*
|
|
79
|
+
* Returns true for a host-provided framework package (special-cased by the
|
|
80
|
+
* addon-upload route — never installed as a normal addon).
|
|
84
81
|
*/
|
|
85
82
|
function isFrameworkPackage(packageName) {
|
|
86
83
|
return exports.FRAMEWORK_PACKAGES.includes(packageName);
|
|
@@ -1012,8 +1009,8 @@ class AddonPackageService {
|
|
|
1012
1009
|
// Content build-id of the code the hub ACTUALLY loaded — resolved from
|
|
1013
1010
|
// the same closure the running process uses. Surfaces "same semver,
|
|
1014
1011
|
// new code" (framework packages ship changes without always bumping).
|
|
1015
|
-
const hubPkgDir = (0,
|
|
1016
|
-
const buildId = hubPkgDir !== null ? (0,
|
|
1012
|
+
const hubPkgDir = (0, package_dir_utils_js_1.resolveHubClosurePackageDir)(packageName);
|
|
1013
|
+
const buildId = hubPkgDir !== null ? (0, package_dir_utils_js_1.computeDistBuildId)(path.join(hubPkgDir, 'dist')) : null;
|
|
1017
1014
|
let latestVersion = null;
|
|
1018
1015
|
try {
|
|
1019
1016
|
const args = [
|
|
@@ -0,0 +1,140 @@
|
|
|
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.resolveHubClosurePackageDir = resolveHubClosurePackageDir;
|
|
37
|
+
exports.computeDistBuildId = computeDistBuildId;
|
|
38
|
+
/**
|
|
39
|
+
* Package-dir introspection helpers for the hub's OWN framework closure.
|
|
40
|
+
*
|
|
41
|
+
* Extracted from the deleted `framework-live-sync.ts` (the dist-mirror
|
|
42
|
+
* machinery was superseded by the runtime-updatable root package — see
|
|
43
|
+
* docs/superpowers/specs/2026-07-12-runtime-updatable-node-packages-design.md);
|
|
44
|
+
* these two read-only helpers remain in use by `AddonPackageService`'s
|
|
45
|
+
* framework-package status surface.
|
|
46
|
+
*/
|
|
47
|
+
const fs = __importStar(require("node:fs"));
|
|
48
|
+
const path = __importStar(require("node:path"));
|
|
49
|
+
const node_crypto_1 = require("node:crypto");
|
|
50
|
+
const node_module_1 = require("node:module");
|
|
51
|
+
// Resolve framework packages the same way the running hub does — from THIS
|
|
52
|
+
// module's location, whose walk-up lands on the hub's own `node_modules`
|
|
53
|
+
// closure (identical to the hub main's `@camstack/system` resolution).
|
|
54
|
+
const requireFromHere = (0, node_module_1.createRequire)(__filename);
|
|
55
|
+
function readJsonObjectSafe(filePath) {
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
58
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
59
|
+
return { ...parsed };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
/* missing / malformed — treat as absent */
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function packageNameOf(pkgJsonPath) {
|
|
68
|
+
const obj = readJsonObjectSafe(pkgJsonPath);
|
|
69
|
+
const name = obj?.['name'];
|
|
70
|
+
return typeof name === 'string' ? name : null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The package directory the HUB MAIN process loads `<packageName>` from — its
|
|
74
|
+
* own `node_modules` closure. `require.resolve('<pkg>/package.json')` is the
|
|
75
|
+
* happy path; packages whose `exports` map omits that subpath fall back to
|
|
76
|
+
* resolving the main entry and walking up to the matching `package.json`.
|
|
77
|
+
* Returns null when the package is not resolvable.
|
|
78
|
+
*/
|
|
79
|
+
function resolveHubClosurePackageDir(packageName) {
|
|
80
|
+
try {
|
|
81
|
+
return path.dirname(requireFromHere.resolve(`${packageName}/package.json`));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// `exports` map blocks the package.json subpath — fall through.
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
let dir = path.dirname(requireFromHere.resolve(packageName));
|
|
88
|
+
while (dir !== path.dirname(dir)) {
|
|
89
|
+
const candidate = path.join(dir, 'package.json');
|
|
90
|
+
if (fs.existsSync(candidate) && packageNameOf(candidate) === packageName) {
|
|
91
|
+
return dir;
|
|
92
|
+
}
|
|
93
|
+
dir = path.dirname(dir);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// package itself not resolvable — treat as not installed.
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
function collectJsFiles(root, dir, out) {
|
|
102
|
+
let entries;
|
|
103
|
+
try {
|
|
104
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
for (const entry of entries) {
|
|
110
|
+
const full = path.join(dir, entry.name);
|
|
111
|
+
if (entry.isDirectory()) {
|
|
112
|
+
collectJsFiles(root, full, out);
|
|
113
|
+
}
|
|
114
|
+
else if (entry.isFile() && (entry.name.endsWith('.js') || entry.name.endsWith('.mjs'))) {
|
|
115
|
+
out.push(path.relative(root, full));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Content build-id of a `dist/` tree — md5 over every `.js`/`.mjs` file's
|
|
121
|
+
* relative path + bytes (sorted for determinism). Analogous to the Docker
|
|
122
|
+
* image's `seed-version` fingerprint. Returns null when the dir is missing or
|
|
123
|
+
* empty. Used to surface "same semver, new code" instead of trusting `version`.
|
|
124
|
+
*/
|
|
125
|
+
function computeDistBuildId(distDir) {
|
|
126
|
+
if (!fs.existsSync(distDir))
|
|
127
|
+
return null;
|
|
128
|
+
const files = [];
|
|
129
|
+
collectJsFiles(distDir, distDir, files);
|
|
130
|
+
if (files.length === 0)
|
|
131
|
+
return null;
|
|
132
|
+
files.sort();
|
|
133
|
+
const hash = (0, node_crypto_1.createHash)('md5');
|
|
134
|
+
for (const rel of files) {
|
|
135
|
+
hash.update(rel);
|
|
136
|
+
hash.update('\0');
|
|
137
|
+
hash.update(fs.readFileSync(path.join(distDir, rel)));
|
|
138
|
+
}
|
|
139
|
+
return hash.digest('hex');
|
|
140
|
+
}
|
|
@@ -921,6 +921,14 @@ class MoleculerService {
|
|
|
921
921
|
listClusterNativeCapsForDevice(deviceId) {
|
|
922
922
|
return this.nodeRegistry.listNativeCapEntriesForDevice(deviceId);
|
|
923
923
|
}
|
|
924
|
+
/**
|
|
925
|
+
* The root-package identity a node reported in its `registerNode`
|
|
926
|
+
* manifest (runtime-updatable node packages: version visibility), or null
|
|
927
|
+
* when the node never reported one (UDS children, pre-phase-2 agents).
|
|
928
|
+
*/
|
|
929
|
+
getNodeRootPackage(nodeId) {
|
|
930
|
+
return this.nodeRegistry.getNodeRootPackage(nodeId);
|
|
931
|
+
}
|
|
924
932
|
/**
|
|
925
933
|
* E2: Send a `set-log-level` UDS message to a hub-local child identified by
|
|
926
934
|
* `nodeId` (e.g. `hub/provider-reolink`). Extracts the `childId` from the
|
|
@@ -0,0 +1,81 @@
|
|
|
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.ServerUpdateService = void 0;
|
|
37
|
+
/**
|
|
38
|
+
* ServerUpdateService — hub-side adapter over the SHARED `RootUpdateService`
|
|
39
|
+
* (`@camstack/node-root`, bundled into `dist/server-root/index.js`).
|
|
40
|
+
*
|
|
41
|
+
* The stage/apply/rollback/probation engine — including the security-review
|
|
42
|
+
* fixes (rollback reentrancy guards, fresh-transient prune protection,
|
|
43
|
+
* `stateFileCorrupt` visibility) — lives in the shared core so the hub and
|
|
44
|
+
* the agent can never drift. This class only binds the hub's parameters:
|
|
45
|
+
* the `@camstack/server` root spec, the `CAMSTACK_SERVER_*` env markers,
|
|
46
|
+
* the `CAMSTACK_DATA` data-dir default and the running package.json path.
|
|
47
|
+
*
|
|
48
|
+
* The starter (server/backend/src/starter.ts → shared `runNodeStarter`)
|
|
49
|
+
* owns the OTHER half: choosing the closure at boot, bumping the probation
|
|
50
|
+
* counter, auto-rolling-back a version whose probation boot never confirmed.
|
|
51
|
+
*
|
|
52
|
+
* Spec: docs/superpowers/specs/2026-07-12-runtime-updatable-node-packages-design.md
|
|
53
|
+
*/
|
|
54
|
+
const path = __importStar(require("node:path"));
|
|
55
|
+
const index_js_1 = require("../../server-root/index.js");
|
|
56
|
+
class ServerUpdateService extends index_js_1.RootUpdateService {
|
|
57
|
+
constructor(options) {
|
|
58
|
+
super({
|
|
59
|
+
spec: index_js_1.HUB_ROOT_SPEC,
|
|
60
|
+
envNames: {
|
|
61
|
+
bootMode: 'CAMSTACK_SERVER_BOOT_MODE',
|
|
62
|
+
activeVersion: 'CAMSTACK_SERVER_ACTIVE_VERSION',
|
|
63
|
+
seedDir: 'CAMSTACK_SEED_SERVER_DIR',
|
|
64
|
+
},
|
|
65
|
+
logger: options.logger,
|
|
66
|
+
restartServer: options.restartServer,
|
|
67
|
+
dataDir: path.resolve(options.dataDir ??
|
|
68
|
+
options.env?.['CAMSTACK_DATA'] ??
|
|
69
|
+
process.env['CAMSTACK_DATA'] ??
|
|
70
|
+
'camstack-data'),
|
|
71
|
+
// dist/core/server-update/ → ../../../package.json = the running copy's
|
|
72
|
+
// package.json (workspace server/backend OR the active/baked closure).
|
|
73
|
+
runningPackageJsonPath: options.runningPackageJsonPath ?? path.resolve(__dirname, '..', '..', '..', 'package.json'),
|
|
74
|
+
workspaceProbeDir: __dirname,
|
|
75
|
+
execNpm: options.execNpm,
|
|
76
|
+
env: options.env,
|
|
77
|
+
now: options.now,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
exports.ServerUpdateService = ServerUpdateService;
|
|
@@ -38,14 +38,18 @@ class TopologyEmitterService {
|
|
|
38
38
|
eventBus;
|
|
39
39
|
agentRegistry;
|
|
40
40
|
addonRegistry;
|
|
41
|
+
getNodeRootPackage;
|
|
41
42
|
unsubscribers = [];
|
|
42
43
|
debounceTimer = null;
|
|
43
44
|
heartbeatTimer = null;
|
|
44
45
|
emitting = false;
|
|
45
|
-
constructor(eventBus, agentRegistry, addonRegistry
|
|
46
|
+
constructor(eventBus, agentRegistry, addonRegistry,
|
|
47
|
+
/** Root-package identity per node (version visibility) — optional. */
|
|
48
|
+
getNodeRootPackage) {
|
|
46
49
|
this.eventBus = eventBus;
|
|
47
50
|
this.agentRegistry = agentRegistry;
|
|
48
51
|
this.addonRegistry = addonRegistry;
|
|
52
|
+
this.getNodeRootPackage = getNodeRootPackage;
|
|
49
53
|
}
|
|
50
54
|
onModuleInit() {
|
|
51
55
|
for (const category of LIFECYCLE_CATEGORIES) {
|
|
@@ -85,7 +89,7 @@ class TopologyEmitterService {
|
|
|
85
89
|
return;
|
|
86
90
|
this.emitting = true;
|
|
87
91
|
try {
|
|
88
|
-
const nodes = await (0, cap_providers_1.computeTopology)(this.agentRegistry, this.addonRegistry);
|
|
92
|
+
const nodes = await (0, cap_providers_1.computeTopology)(this.agentRegistry, this.addonRegistry, this.getNodeRootPackage);
|
|
89
93
|
this.eventBus.emit({
|
|
90
94
|
id: (0, node_crypto_1.randomUUID)(),
|
|
91
95
|
timestamp: new Date(),
|
package/dist/main.js
CHANGED
|
@@ -81,11 +81,13 @@ const addon_upload_1 = require("./api/addon-upload");
|
|
|
81
81
|
const auth_whoami_1 = require("./api/auth-whoami");
|
|
82
82
|
const session_cookie_js_1 = require("./auth/session-cookie.js");
|
|
83
83
|
const health_routes_1 = require("./api/health/health.routes");
|
|
84
|
+
const spa_static_1 = require("./api/static/spa-static");
|
|
84
85
|
const oauth2_routes_js_1 = require("./api/oauth2/oauth2-routes.js");
|
|
85
86
|
const system_2 = require("@camstack/system");
|
|
86
87
|
const boot_config_1 = require("./boot/boot-config");
|
|
87
88
|
const manual_boot_1 = require("./manual-boot");
|
|
88
89
|
const post_boot_service_1 = require("./boot/post-boot.service");
|
|
90
|
+
const server_update_service_1 = require("./core/server-update/server-update.service");
|
|
89
91
|
const integration_id_backfill_1 = require("./boot/integration-id-backfill");
|
|
90
92
|
// ---- Process-level error handlers ----
|
|
91
93
|
process.on('uncaughtException', (err) => {
|
|
@@ -363,6 +365,7 @@ async function bootstrap() {
|
|
|
363
365
|
toastService,
|
|
364
366
|
capabilityRegistry,
|
|
365
367
|
streamProbe: app.get(stream_probe_service_1.StreamProbeService),
|
|
368
|
+
serverUpdateService: app.get(server_update_service_1.ServerUpdateService),
|
|
366
369
|
});
|
|
367
370
|
await fastify.register(fastify_1.fastifyTRPCPlugin, {
|
|
368
371
|
prefix: '/trpc',
|
|
@@ -418,7 +421,10 @@ async function bootstrap() {
|
|
|
418
421
|
? 'text/html'
|
|
419
422
|
: 'application/octet-stream';
|
|
420
423
|
const stream = fs.createReadStream(resolved);
|
|
421
|
-
return reply
|
|
424
|
+
return reply
|
|
425
|
+
.header('cache-control', addonBundleCacheControl(resolved))
|
|
426
|
+
.type(contentType)
|
|
427
|
+
.send(stream);
|
|
422
428
|
});
|
|
423
429
|
// Register addon-widgets static file endpoint — same shape as
|
|
424
430
|
// addon-pages but reads bundles from each addon's
|
|
@@ -447,7 +453,10 @@ async function bootstrap() {
|
|
|
447
453
|
? 'text/html'
|
|
448
454
|
: 'application/octet-stream';
|
|
449
455
|
const stream = fs.createReadStream(resolved);
|
|
450
|
-
return reply
|
|
456
|
+
return reply
|
|
457
|
+
.header('cache-control', addonBundleCacheControl(resolved))
|
|
458
|
+
.type(contentType)
|
|
459
|
+
.send(stream);
|
|
451
460
|
});
|
|
452
461
|
// Serve static assets (e.g. SVG icons) from an addon's package directory
|
|
453
462
|
fastify.get('/api/addon-assets/:addonId/*', async (request, reply) => {
|
|
@@ -968,7 +977,12 @@ async function bootstrap() {
|
|
|
968
977
|
if (url.startsWith('/trpc') ||
|
|
969
978
|
url.startsWith('/api/') ||
|
|
970
979
|
url.startsWith('/agent') ||
|
|
971
|
-
url.startsWith('/health')
|
|
980
|
+
url.startsWith('/health') ||
|
|
981
|
+
url.startsWith('/discovery') ||
|
|
982
|
+
// The viewer SPA owns /viewer/** via its own catch-all (registered
|
|
983
|
+
// below). Fall through here so admin-ui never serves it — and so
|
|
984
|
+
// /viewer 404s cleanly when the viewer-ui addon isn't registered.
|
|
985
|
+
url.startsWith('/viewer')) {
|
|
972
986
|
return reply.callNotFound();
|
|
973
987
|
}
|
|
974
988
|
// A request whose last path segment has a file extension is a static
|
|
@@ -989,16 +1003,7 @@ async function bootstrap() {
|
|
|
989
1003
|
// client (the SW keeps serving the old precache). Content-hashed
|
|
990
1004
|
// build assets (assets/index-<hash>.js) are immutable. Everything
|
|
991
1005
|
// else gets a short cache.
|
|
992
|
-
|
|
993
|
-
if (/^(sw\.js|registerSW\.js|workbox-.*\.js|manifest\.webmanifest)$/.test(base)) {
|
|
994
|
-
reply.header('cache-control', 'no-cache, must-revalidate');
|
|
995
|
-
}
|
|
996
|
-
else if (rel.startsWith('assets/') && /-[A-Za-z0-9_-]{8,}\./.test(base)) {
|
|
997
|
-
reply.header('cache-control', 'public, max-age=31536000, immutable');
|
|
998
|
-
}
|
|
999
|
-
else {
|
|
1000
|
-
reply.header('cache-control', 'no-cache');
|
|
1001
|
-
}
|
|
1006
|
+
reply.header('cache-control', (0, spa_static_1.spaAssetCacheControl)(rel));
|
|
1002
1007
|
return reply.sendFile(rel);
|
|
1003
1008
|
}
|
|
1004
1009
|
return reply.callNotFound();
|
|
@@ -1022,6 +1027,82 @@ async function bootstrap() {
|
|
|
1022
1027
|
catch (err) {
|
|
1023
1028
|
console.error('[bootstrap] Failed to set up admin UI static serving:', err);
|
|
1024
1029
|
}
|
|
1030
|
+
// Serve the CamStack viewer SPA from the viewer-ui singleton capability.
|
|
1031
|
+
// The viewer is built (Expo web export) with EXPO_PUBLIC_WEB_BASE_PATH=/viewer,
|
|
1032
|
+
// so its baseUrl — and therefore every asset URL — is /viewer/camstack/**. We
|
|
1033
|
+
// mount it at exactly that prefix and redirect /viewer → the SPA root. Mirrors
|
|
1034
|
+
// the admin-ui poll+serve, but streams assets directly from its own dist root
|
|
1035
|
+
// so it stays independent of the admin-ui @fastify/static decorator.
|
|
1036
|
+
try {
|
|
1037
|
+
const bootAddonRegistry = app.get(addon_registry_service_1.AddonRegistryService);
|
|
1038
|
+
const capRegistry = bootAddonRegistry.getCapabilityRegistry();
|
|
1039
|
+
let viewerUI = capRegistry?.getSingleton('viewer-ui');
|
|
1040
|
+
const skipViewerUIWait = process.env['CAMSTACK_SKIP_VIEWER_UI_WAIT'] === '1';
|
|
1041
|
+
if (!viewerUI && capRegistry && !skipViewerUIWait) {
|
|
1042
|
+
// The viewer-ui addon runs in its own forked runner and registers a few
|
|
1043
|
+
// seconds after boot. Poll briefly (shorter than admin-ui's 60s — the
|
|
1044
|
+
// viewer is non-critical and admin-ui already gated the slow cold boot).
|
|
1045
|
+
const VIEWER_UI_WAIT_MS = 30_000;
|
|
1046
|
+
const POLL_MS = 200;
|
|
1047
|
+
const deadline = Date.now() + VIEWER_UI_WAIT_MS;
|
|
1048
|
+
while (!viewerUI && Date.now() < deadline) {
|
|
1049
|
+
await new Promise((r) => setTimeout(r, POLL_MS));
|
|
1050
|
+
viewerUI = capRegistry.getSingleton('viewer-ui');
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
if (viewerUI) {
|
|
1054
|
+
const { staticDir } = await viewerUI.getStaticDir();
|
|
1055
|
+
const indexPath = path.join(staticDir, 'index.html');
|
|
1056
|
+
if (fs.existsSync(staticDir) && fs.existsSync(indexPath)) {
|
|
1057
|
+
// The Expo baseUrl (`${webBasePath}/camstack`) is the on-hub mount.
|
|
1058
|
+
const VIEWER_MOUNT = '/viewer/camstack';
|
|
1059
|
+
// Clean entry point: /viewer and /viewer/ → the SPA root.
|
|
1060
|
+
fastify.get('/viewer', async (_request, reply) => reply.redirect(`${VIEWER_MOUNT}/`));
|
|
1061
|
+
fastify.get('/viewer/', async (_request, reply) => reply.redirect(`${VIEWER_MOUNT}/`));
|
|
1062
|
+
// Single catch-all owns every GET under /viewer/**: assets stream LIVE
|
|
1063
|
+
// from the current dist; everything else is the SPA shell.
|
|
1064
|
+
fastify.get('/viewer/*', async (request, reply) => {
|
|
1065
|
+
const pathOnly = request.url.split('?')[0] ?? request.url;
|
|
1066
|
+
// Map the request path to a dist-relative path by stripping the mount
|
|
1067
|
+
// prefix. Anything not under the mount is treated as the SPA shell.
|
|
1068
|
+
let rel = '';
|
|
1069
|
+
if (pathOnly === VIEWER_MOUNT || pathOnly === `${VIEWER_MOUNT}/`) {
|
|
1070
|
+
rel = '';
|
|
1071
|
+
}
|
|
1072
|
+
else if (pathOnly.startsWith(`${VIEWER_MOUNT}/`)) {
|
|
1073
|
+
rel = pathOnly.slice(VIEWER_MOUNT.length + 1);
|
|
1074
|
+
}
|
|
1075
|
+
// A path whose last segment has a file extension is a static asset:
|
|
1076
|
+
// serve it, or 404 — NEVER the SPA shell (serving HTML under a
|
|
1077
|
+
// .js/.css URL pins text/html in caches and breaks the module MIME
|
|
1078
|
+
// check long after the file is actually available).
|
|
1079
|
+
if (rel && /\.[a-zA-Z0-9]+$/.test(pathOnly)) {
|
|
1080
|
+
const abs = path.join(staticDir, rel);
|
|
1081
|
+
if ((abs === staticDir || abs.startsWith(staticDir + path.sep)) && fs.existsSync(abs)) {
|
|
1082
|
+
reply.header('cache-control', (0, spa_static_1.spaAssetCacheControl)(rel));
|
|
1083
|
+
reply.type((0, spa_static_1.contentTypeForPath)(rel));
|
|
1084
|
+
return reply.send(fs.createReadStream(abs));
|
|
1085
|
+
}
|
|
1086
|
+
return reply.callNotFound();
|
|
1087
|
+
}
|
|
1088
|
+
// SPA shell — never cached (it references content-hashed bundles).
|
|
1089
|
+
reply.header('cache-control', 'no-cache, must-revalidate');
|
|
1090
|
+
return reply.type('text/html').send(fs.createReadStream(indexPath));
|
|
1091
|
+
});
|
|
1092
|
+
const { version } = await viewerUI.getVersion();
|
|
1093
|
+
console.log(`[bootstrap] Viewer UI served from: ${staticDir} at ${VIEWER_MOUNT} (v${version})`);
|
|
1094
|
+
}
|
|
1095
|
+
else {
|
|
1096
|
+
console.warn(`[bootstrap] Viewer UI dist not found at: ${staticDir} — run 'npm run build' in addon-viewer-ui`);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
else {
|
|
1100
|
+
console.warn('[bootstrap] viewer-ui capability not registered — viewer not served');
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
catch (err) {
|
|
1104
|
+
console.error('[bootstrap] Failed to set up viewer UI static serving:', err);
|
|
1105
|
+
}
|
|
1025
1106
|
try {
|
|
1026
1107
|
await app.listen(port, host);
|
|
1027
1108
|
}
|
|
@@ -1041,6 +1122,22 @@ async function bootstrap() {
|
|
|
1041
1122
|
// Post-boot: fork workers, register device streams, emit system.boot
|
|
1042
1123
|
const postBoot = app.get(post_boot_service_1.PostBootService);
|
|
1043
1124
|
await postBoot.run({ port, host, dataPath, trpcRegistered });
|
|
1125
|
+
// The hub reached ready — when this boot is the PROBATION boot of a freshly
|
|
1126
|
+
// staged @camstack/server root, promote it (currentVersion ← pending, N-1
|
|
1127
|
+
// retained for rollback) and disarm the starter's crash-loop auto-rollback.
|
|
1128
|
+
try {
|
|
1129
|
+
const confirm = app.get(server_update_service_1.ServerUpdateService).confirmBootHealthy();
|
|
1130
|
+
if (confirm.promoted !== null) {
|
|
1131
|
+
logger.info('Server root package update confirmed healthy', {
|
|
1132
|
+
meta: { version: confirm.promoted },
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
catch (err) {
|
|
1137
|
+
logger.warn('Server root boot confirmation failed', {
|
|
1138
|
+
meta: { error: err instanceof Error ? err.message : String(err) },
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1044
1141
|
// One-time backfill: stamp integrationId on devices created before the
|
|
1045
1142
|
// device-manager forwarder started stamping it (legacy camera providers),
|
|
1046
1143
|
// so deleting their integration cascades them. Idempotent — only touches
|
|
@@ -1095,6 +1192,22 @@ function readHubVersion() {
|
|
|
1095
1192
|
/**
|
|
1096
1193
|
* Build an AddonHttpReply wrapper around a Fastify reply.
|
|
1097
1194
|
*/
|
|
1195
|
+
/**
|
|
1196
|
+
* Cache-Control for an addon MF bundle file. The entry point
|
|
1197
|
+
* (`remoteEntry.js` / `widgets.mjs`) has a STABLE filename — only its
|
|
1198
|
+
* `?v=` query changes across builds — so it MUST revalidate, otherwise a
|
|
1199
|
+
* caching proxy (Cloudflare edge, whose default browser-cache TTL was
|
|
1200
|
+
* serving these 4h) keeps handing out a stale entry whose export map
|
|
1201
|
+
* lacks the current widgets ("bundle does not export this stableId",
|
|
1202
|
+
* tunnel-only). The sibling chunks are CONTENT-HASHED in their filenames
|
|
1203
|
+
* (`dist-<hash>.mjs`, `_virtual_mf-<hash>.mjs`), so they are safely
|
|
1204
|
+
* immutable. The revalidation cost is a cheap 304 on a ~4KB entry.
|
|
1205
|
+
*/
|
|
1206
|
+
function addonBundleCacheControl(filePath) {
|
|
1207
|
+
const base = path.basename(filePath);
|
|
1208
|
+
const isMfEntry = base === 'remoteEntry.js' || base === 'widgets.mjs';
|
|
1209
|
+
return isMfEntry ? 'no-cache, must-revalidate' : 'public, max-age=31536000, immutable';
|
|
1210
|
+
}
|
|
1098
1211
|
function buildAddonReply(reply) {
|
|
1099
1212
|
const wrapper = {
|
|
1100
1213
|
status(code) {
|
package/dist/manual-boot.js
CHANGED
|
@@ -72,7 +72,9 @@ const addon_registry_service_1 = require("./core/addon/addon-registry.service");
|
|
|
72
72
|
const addon_search_service_1 = require("./core/addon/addon-search.service");
|
|
73
73
|
const addon_package_service_1 = require("./core/addon/addon-package.service");
|
|
74
74
|
const repl_engine_service_1 = require("./core/repl/repl-engine.service");
|
|
75
|
+
const server_update_service_1 = require("./core/server-update/server-update.service");
|
|
75
76
|
const topology_emitter_service_1 = require("./core/topology/topology-emitter.service");
|
|
77
|
+
const cap_providers_1 = require("./api/core/cap-providers");
|
|
76
78
|
const post_boot_service_1 = require("./boot/post-boot.service");
|
|
77
79
|
// ---------------------------------------------------------------------------
|
|
78
80
|
// Service container — narrowing via `instanceof`, no casts.
|
|
@@ -200,7 +202,14 @@ async function bootManual(opts) {
|
|
|
200
202
|
},
|
|
201
203
|
});
|
|
202
204
|
const replEngineService = new repl_engine_service_1.ReplEngineService(addonRegistryService, eventBusService, loggingService);
|
|
203
|
-
|
|
205
|
+
// Runtime-updatable root package (phase 1: hub only). The service owns the
|
|
206
|
+
// data-dir server-root staging/rollback lifecycle; restarts route through
|
|
207
|
+
// AddonPackageService.restartServer for the marker + reconnect toast.
|
|
208
|
+
const serverUpdateService = new server_update_service_1.ServerUpdateService({
|
|
209
|
+
logger: loggingService.createLogger('ServerUpdate'),
|
|
210
|
+
restartServer: (requestedBy) => addonPackageService.restartServer(requestedBy),
|
|
211
|
+
});
|
|
212
|
+
const topologyEmitterService = new topology_emitter_service_1.TopologyEmitterService(eventBusService, agentRegistryService, addonRegistryService, (0, cap_providers_1.createNodeRootPackageLookup)(moleculerService, serverUpdateService));
|
|
204
213
|
const postBootService = new post_boot_service_1.PostBootService(addonRegistryService, eventBusService, loggingService);
|
|
205
214
|
// ---- Container ---------------------------------------------------------
|
|
206
215
|
const container = new ServiceContainer();
|
|
@@ -224,6 +233,7 @@ async function bootManual(opts) {
|
|
|
224
233
|
container.register(addon_search_service_1.AddonSearchService, addonSearchService);
|
|
225
234
|
container.register(addon_package_service_1.AddonPackageService, addonPackageService);
|
|
226
235
|
container.register(repl_engine_service_1.ReplEngineService, replEngineService);
|
|
236
|
+
container.register(server_update_service_1.ServerUpdateService, serverUpdateService);
|
|
227
237
|
container.register(topology_emitter_service_1.TopologyEmitterService, topologyEmitterService);
|
|
228
238
|
container.register(post_boot_service_1.PostBootService, postBootService);
|
|
229
239
|
// ---- Fastify instance --------------------------------------------------
|