@camstack/server 0.2.2 → 1.0.1
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/{src/agent-status-page.ts → dist/agent-status-page.js} +30 -45
- package/dist/api/addon-upload.js +441 -0
- package/dist/api/addons-custom.router.js +91 -0
- package/dist/api/auth-whoami.js +55 -0
- package/dist/api/bridge-addons.router.js +109 -0
- package/dist/api/capabilities.router.js +229 -0
- package/dist/api/core/addon-settings.router.js +117 -0
- package/dist/api/core/agents.router.js +73 -0
- package/dist/api/core/auth.router.js +286 -0
- package/dist/api/core/bulk-update-coordinator.js +229 -0
- package/dist/api/core/cap-providers.js +1124 -0
- package/dist/api/core/capabilities.router.js +138 -0
- package/dist/api/core/collection-preference.js +17 -0
- package/dist/api/core/event-bus-proxy.router.js +45 -0
- package/dist/api/core/hwaccel.router.js +91 -0
- package/dist/api/core/live-events.router.js +61 -0
- package/dist/api/core/logs.router.js +172 -0
- package/dist/api/core/notifications.router.js +67 -0
- package/dist/api/core/repl.router.js +35 -0
- package/dist/api/core/settings-backend.router.js +121 -0
- package/dist/api/core/stream-probe.router.js +58 -0
- package/dist/api/core/system-events.router.js +100 -0
- package/dist/api/health/health.routes.js +68 -0
- package/{src/api/oauth2/consent-page.ts → dist/api/oauth2/consent-page.js} +11 -20
- package/dist/api/oauth2/oauth2-routes.js +219 -0
- package/dist/api/trpc/cap-mount-helpers.js +194 -0
- package/dist/api/trpc/cap-route-error-formatter.js +133 -0
- package/dist/api/trpc/client-ip.js +147 -0
- package/dist/api/trpc/core-cap-bridge.js +115 -0
- package/dist/api/trpc/generated-cap-mounts.js +388 -0
- package/dist/api/trpc/generated-cap-routers.js +7635 -0
- package/dist/api/trpc/scope-access.js +93 -0
- package/dist/api/trpc/trpc.context.js +184 -0
- package/dist/api/trpc/trpc.middleware.js +139 -0
- package/dist/api/trpc/trpc.router.js +188 -0
- package/dist/auth/session-cookie.js +47 -0
- package/dist/boot/boot-config.js +241 -0
- package/dist/boot/integration-id-backfill.js +76 -0
- package/dist/boot/post-boot.service.js +85 -0
- package/dist/core/addon/addon-call-gateway.js +99 -0
- package/dist/core/addon/addon-package.service.js +1560 -0
- package/dist/core/addon/addon-registry.service.js +2739 -0
- package/{src/core/addon/addon-row-manifest.ts → dist/core/addon/addon-row-manifest.js} +5 -5
- package/dist/core/addon/addon-search.service.js +62 -0
- package/dist/core/addon/addon-settings-provider.js +102 -0
- package/dist/core/addon/addon.tokens.js +5 -0
- package/dist/core/addon-bridge/addon-bridge.service.js +145 -0
- package/dist/core/addon-pages/addon-pages.service.js +107 -0
- package/dist/core/addon-widgets/addon-widgets.service.js +120 -0
- package/dist/core/agent/agent-registry.service.js +477 -0
- package/dist/core/auth/auth.service.js +10 -0
- package/dist/core/capability/capability.service.js +58 -0
- package/dist/core/config/config.schema.js +7 -0
- package/dist/core/config/config.service.js +10 -0
- package/dist/core/events/event-bus.service.js +83 -0
- package/dist/core/feature/feature.service.js +10 -0
- package/dist/core/lifecycle/lifecycle-state-machine.js +6 -0
- package/dist/core/logging/log-ring-buffer.js +6 -0
- package/dist/core/logging/logging.service.js +130 -0
- package/dist/core/logging/scoped-logger.js +6 -0
- package/dist/core/moleculer/cap-call-fn.js +50 -0
- package/dist/core/moleculer/cap-route-authority.js +122 -0
- package/dist/core/moleculer/moleculer.service.js +898 -0
- package/dist/core/network/network-quality.service.js +7 -0
- package/dist/core/notification/notification-wrapper.service.js +33 -0
- package/dist/core/notification/toast-wrapper.service.js +25 -0
- package/dist/core/provider/provider.tokens.js +4 -0
- package/dist/core/repl/repl-engine.service.js +140 -0
- package/dist/core/storage/fs-storage-backend.js +6 -0
- package/dist/core/storage/storage-location-manager.js +6 -0
- package/dist/core/storage/storage.service.js +7 -0
- package/dist/core/streaming/stream-probe.service.js +209 -0
- package/dist/core/topology/topology-emitter.service.js +106 -0
- package/dist/launcher.js +325 -0
- package/dist/main.js +1098 -0
- package/dist/manual-boot.js +227 -0
- package/package.json +5 -1
- package/src/__tests__/addon-install-e2e.test.ts +0 -74
- package/src/__tests__/addon-pages-e2e.test.ts +0 -200
- package/src/__tests__/addon-route-session.test.ts +0 -17
- package/src/__tests__/addon-settings-router.spec.ts +0 -67
- package/src/__tests__/addon-upload.spec.ts +0 -475
- package/src/__tests__/agent-registry.spec.ts +0 -179
- package/src/__tests__/agent-status-page.spec.ts +0 -82
- package/src/__tests__/auth-session-cookie.test.ts +0 -48
- package/src/__tests__/bulk-update-coordinator.spec.ts +0 -303
- package/src/__tests__/cap-ownership-authority.spec.ts +0 -431
- package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +0 -206
- package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +0 -37
- package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +0 -110
- package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +0 -292
- package/src/__tests__/cap-providers-bulk-update.spec.ts +0 -408
- package/src/__tests__/cap-route-adapter.spec.ts +0 -302
- package/src/__tests__/cap-routers/_meta.spec.ts +0 -199
- package/src/__tests__/cap-routers/addon-settings.router.spec.ts +0 -115
- package/src/__tests__/cap-routers/broker-routing.router.spec.ts +0 -177
- package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +0 -125
- package/src/__tests__/cap-routers/capabilities-node.spec.ts +0 -68
- package/src/__tests__/cap-routers/device-link-overlay.spec.ts +0 -137
- package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +0 -194
- package/src/__tests__/cap-routers/harness.ts +0 -163
- package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +0 -133
- package/src/__tests__/cap-routers/null-provider-guard.spec.ts +0 -64
- package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +0 -159
- package/src/__tests__/cap-routers/settings-store.router.spec.ts +0 -291
- package/src/__tests__/capability-e2e.test.ts +0 -384
- package/src/__tests__/cli-e2e.test.ts +0 -150
- package/src/__tests__/core-cap-bridge.spec.ts +0 -91
- package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +0 -40
- package/src/__tests__/device-settings-contribution-dispatch.spec.ts +0 -280
- package/src/__tests__/embedded-deps-e2e.test.ts +0 -125
- package/src/__tests__/event-bus-proxy-router.spec.ts +0 -75
- package/src/__tests__/fixtures/mock-analysis-addon-a.ts +0 -37
- package/src/__tests__/fixtures/mock-analysis-addon-b.ts +0 -37
- package/src/__tests__/fixtures/mock-log-addon.ts +0 -37
- package/src/__tests__/fixtures/mock-storage-addon.ts +0 -40
- package/src/__tests__/framework-allowlist.spec.ts +0 -96
- package/src/__tests__/framework-installer-defer-restart.spec.ts +0 -165
- package/src/__tests__/https-e2e.test.ts +0 -124
- package/src/__tests__/lifecycle-e2e.test.ts +0 -189
- package/src/__tests__/live-events-subscription.spec.ts +0 -149
- package/src/__tests__/moleculer/uds-readiness.spec.ts +0 -150
- package/src/__tests__/moleculer/uds-topology.spec.ts +0 -418
- package/src/__tests__/moleculer/uds-unowned-call.spec.ts +0 -383
- package/src/__tests__/moleculer-register-node-idempotency.spec.ts +0 -273
- package/src/__tests__/native-cap-route.spec.ts +0 -427
- package/src/__tests__/oauth2-account-linking.spec.ts +0 -867
- package/src/__tests__/post-boot-restart.spec.ts +0 -161
- package/src/__tests__/singleton-contention.test.ts +0 -499
- package/src/__tests__/streaming-diagnostic.test.ts +0 -615
- package/src/__tests__/streaming-scale.test.ts +0 -314
- package/src/__tests__/uds-addon-call-wiring.spec.ts +0 -242
- package/src/__tests__/uds-log-ingest.spec.ts +0 -183
- package/src/api/__tests__/addons-custom.spec.ts +0 -148
- package/src/api/__tests__/capabilities.router.test.ts +0 -56
- package/src/api/addon-upload.ts +0 -529
- package/src/api/addons-custom.router.ts +0 -101
- package/src/api/auth-whoami.ts +0 -101
- package/src/api/bridge-addons.router.ts +0 -122
- package/src/api/capabilities.router.ts +0 -265
- package/src/api/core/__tests__/auth-router-totp.spec.ts +0 -297
- package/src/api/core/__tests__/integration-markers.spec.ts +0 -10
- package/src/api/core/addon-settings.router.ts +0 -127
- package/src/api/core/agents.router.ts +0 -86
- package/src/api/core/auth.router.ts +0 -322
- package/src/api/core/bulk-update-coordinator.ts +0 -305
- package/src/api/core/cap-providers.ts +0 -1339
- package/src/api/core/capabilities.router.ts +0 -149
- package/src/api/core/collection-preference.ts +0 -40
- package/src/api/core/event-bus-proxy.router.ts +0 -45
- package/src/api/core/hwaccel.router.ts +0 -108
- package/src/api/core/live-events.router.ts +0 -67
- package/src/api/core/logs.router.ts +0 -195
- package/src/api/core/notifications.router.ts +0 -66
- package/src/api/core/repl.router.ts +0 -39
- package/src/api/core/settings-backend.router.ts +0 -140
- package/src/api/core/stream-probe.router.ts +0 -57
- package/src/api/core/system-events.router.ts +0 -125
- package/src/api/health/health.routes.ts +0 -117
- package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +0 -62
- package/src/api/oauth2/oauth2-routes.ts +0 -281
- package/src/api/trpc/__tests__/client-ip.spec.ts +0 -146
- package/src/api/trpc/__tests__/scope-access-device.spec.ts +0 -268
- package/src/api/trpc/__tests__/scope-access.spec.ts +0 -102
- package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +0 -136
- package/src/api/trpc/cap-mount-helpers.ts +0 -245
- package/src/api/trpc/cap-route-error-formatter.ts +0 -171
- package/src/api/trpc/client-ip.ts +0 -147
- package/src/api/trpc/core-cap-bridge.ts +0 -154
- package/src/api/trpc/generated-cap-mounts.ts +0 -1240
- package/src/api/trpc/generated-cap-routers.ts +0 -11523
- package/src/api/trpc/scope-access.ts +0 -110
- package/src/api/trpc/trpc.context.ts +0 -258
- package/src/api/trpc/trpc.middleware.ts +0 -146
- package/src/api/trpc/trpc.router.ts +0 -389
- package/src/auth/session-cookie.ts +0 -54
- package/src/boot/__tests__/integration-id-backfill.spec.ts +0 -131
- package/src/boot/boot-config.ts +0 -259
- package/src/boot/integration-id-backfill.ts +0 -109
- package/src/boot/post-boot.service.ts +0 -105
- package/src/core/addon/__tests__/addon-registry-capability.test.ts +0 -62
- package/src/core/addon/__tests__/addon-row-manifest.spec.ts +0 -62
- package/src/core/addon/addon-call-gateway.ts +0 -171
- package/src/core/addon/addon-package.service.ts +0 -1787
- package/src/core/addon/addon-registry.service.ts +0 -3130
- package/src/core/addon/addon-search.service.ts +0 -91
- package/src/core/addon/addon-settings-provider.ts +0 -220
- package/src/core/addon/addon.tokens.ts +0 -2
- package/src/core/addon-bridge/addon-bridge.service.ts +0 -130
- package/src/core/addon-pages/addon-pages.service.spec.ts +0 -117
- package/src/core/addon-pages/addon-pages.service.ts +0 -82
- package/src/core/addon-widgets/addon-widgets.service.ts +0 -95
- package/src/core/agent/agent-registry.service.ts +0 -529
- package/src/core/auth/auth.service.spec.ts +0 -86
- package/src/core/auth/auth.service.ts +0 -8
- package/src/core/capability/capability.service.ts +0 -66
- package/src/core/config/config.schema.ts +0 -3
- package/src/core/config/config.service.spec.ts +0 -175
- package/src/core/config/config.service.ts +0 -7
- package/src/core/events/event-bus.service.spec.ts +0 -235
- package/src/core/events/event-bus.service.ts +0 -89
- package/src/core/feature/feature.service.spec.ts +0 -99
- package/src/core/feature/feature.service.ts +0 -8
- package/src/core/lifecycle/lifecycle-state-machine.spec.ts +0 -166
- package/src/core/lifecycle/lifecycle-state-machine.ts +0 -3
- package/src/core/logging/log-ring-buffer.ts +0 -3
- package/src/core/logging/logging.service.spec.ts +0 -287
- package/src/core/logging/logging.service.ts +0 -143
- package/src/core/logging/scoped-logger.ts +0 -3
- package/src/core/moleculer/cap-call-fn.spec.ts +0 -173
- package/src/core/moleculer/cap-call-fn.ts +0 -107
- package/src/core/moleculer/cap-route-authority.ts +0 -194
- package/src/core/moleculer/moleculer.service.ts +0 -1072
- package/src/core/network/network-quality.service.spec.ts +0 -53
- package/src/core/network/network-quality.service.ts +0 -5
- package/src/core/notification/notification-wrapper.service.ts +0 -34
- package/src/core/notification/toast-wrapper.service.ts +0 -27
- package/src/core/provider/provider.tokens.ts +0 -1
- package/src/core/repl/repl-engine.service.spec.ts +0 -444
- package/src/core/repl/repl-engine.service.ts +0 -155
- package/src/core/storage/fs-storage-backend.spec.ts +0 -70
- package/src/core/storage/fs-storage-backend.ts +0 -3
- package/src/core/storage/storage-location-manager.spec.ts +0 -130
- package/src/core/storage/storage-location-manager.ts +0 -3
- package/src/core/storage/storage.service.spec.ts +0 -73
- package/src/core/storage/storage.service.ts +0 -3
- package/src/core/streaming/stream-probe.service.ts +0 -221
- package/src/core/topology/topology-emitter.service.ts +0 -105
- package/src/launcher.ts +0 -314
- package/src/main.ts +0 -1245
- package/src/manual-boot.ts +0 -301
- package/tsconfig.build.json +0 -8
- package/tsconfig.json +0 -33
- package/vitest.config.ts +0 -26
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase 1: Bootstrap config loading and infrastructure setup.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from main.ts — pure extraction, no behavior change.
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.loadBootstrapConfig = loadBootstrapConfig;
|
|
42
|
+
exports.autoGenerateJwtSecret = autoGenerateJwtSecret;
|
|
43
|
+
exports.setupInfra = setupInfra;
|
|
44
|
+
const fs = __importStar(require("node:fs"));
|
|
45
|
+
const path = __importStar(require("node:path"));
|
|
46
|
+
const yaml = __importStar(require("js-yaml"));
|
|
47
|
+
const node_crypto_1 = require("node:crypto");
|
|
48
|
+
const types_1 = require("@camstack/types");
|
|
49
|
+
const config_schema_1 = require("../core/config/config.schema");
|
|
50
|
+
const storage_location_manager_1 = require("../core/storage/storage-location-manager");
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Constants
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
const CONFIG_DEFAULTS = {
|
|
55
|
+
server: { port: 4443, host: '0.0.0.0', dataPath: 'camstack-data' },
|
|
56
|
+
auth: {
|
|
57
|
+
jwtSecret: null,
|
|
58
|
+
adminUsername: 'admin',
|
|
59
|
+
adminPassword: 'changeme',
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
const ENV_VAR_MAP = {
|
|
63
|
+
CAMSTACK_PORT: 'server.port',
|
|
64
|
+
CAMSTACK_HOST: 'server.host',
|
|
65
|
+
CAMSTACK_DATA: 'server.dataPath',
|
|
66
|
+
CAMSTACK_JWT_SECRET: 'auth.jwtSecret',
|
|
67
|
+
CAMSTACK_ADMIN_USER: 'auth.adminUsername',
|
|
68
|
+
CAMSTACK_ADMIN_PASS: 'auth.adminPassword',
|
|
69
|
+
CAMSTACK_HUB_URL: 'hub.url',
|
|
70
|
+
CAMSTACK_HUB_TOKEN: 'hub.token',
|
|
71
|
+
CAMSTACK_AGENT_NAME: 'agent.name',
|
|
72
|
+
CAMSTACK_TLS_ENABLED: 'tls.enabled',
|
|
73
|
+
CAMSTACK_TLS_CERT: 'tls.certPath',
|
|
74
|
+
CAMSTACK_TLS_KEY: 'tls.keyPath',
|
|
75
|
+
};
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Helpers
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
function setNested(obj, p, value) {
|
|
80
|
+
const [head, ...rest] = p.split('.');
|
|
81
|
+
if (!head)
|
|
82
|
+
return obj;
|
|
83
|
+
if (rest.length === 0)
|
|
84
|
+
return { ...obj, [head]: value };
|
|
85
|
+
const child = (0, types_1.asJsonObject)(obj[head]) ?? {};
|
|
86
|
+
return { ...obj, [head]: setNested(child, rest.join('.'), value) };
|
|
87
|
+
}
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// loadBootstrapConfig
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
/**
|
|
92
|
+
* Load bootstrap config from a YAML file, apply env-var overrides, and
|
|
93
|
+
* validate via the bootstrap Zod schema.
|
|
94
|
+
*/
|
|
95
|
+
function loadBootstrapConfig(configPath) {
|
|
96
|
+
// Only bootstrap sections live in config.yaml.
|
|
97
|
+
// All runtime settings are stored in the SQL system_settings table.
|
|
98
|
+
let raw;
|
|
99
|
+
if (fs.existsSync(configPath)) {
|
|
100
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
101
|
+
raw = (0, types_1.asJsonObject)(yaml.load(content)) ?? {};
|
|
102
|
+
// Merge in any missing bootstrap sections (server, auth only)
|
|
103
|
+
let updated = false;
|
|
104
|
+
for (const [key, defaults] of Object.entries(CONFIG_DEFAULTS)) {
|
|
105
|
+
if (!(key in raw)) {
|
|
106
|
+
raw[key] = defaults;
|
|
107
|
+
updated = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (updated) {
|
|
111
|
+
try {
|
|
112
|
+
const tmpPath = `${configPath}.tmp`;
|
|
113
|
+
fs.writeFileSync(tmpPath, yaml.dump(raw, { lineWidth: 120, indent: 2, quotingType: '"' }), 'utf-8');
|
|
114
|
+
fs.renameSync(tmpPath, configPath);
|
|
115
|
+
console.log(`[Phase1] Updated config.yaml with missing bootstrap defaults`);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
console.warn(`[Phase1] Could not update config.yaml:`, err);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
console.log(`[Phase1] Loaded bootstrap config from: ${configPath}`);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
console.log(`[Phase1] Config file not found at: ${configPath} — writing defaults`);
|
|
125
|
+
const defaults = { ...CONFIG_DEFAULTS };
|
|
126
|
+
try {
|
|
127
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
128
|
+
const tmpPath = `${configPath}.tmp`;
|
|
129
|
+
fs.writeFileSync(tmpPath, yaml.dump(defaults, { lineWidth: 120, indent: 2, quotingType: '"' }), 'utf-8');
|
|
130
|
+
fs.renameSync(tmpPath, configPath);
|
|
131
|
+
console.log(`[Phase1] Default config.yaml written to: ${configPath}`);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
console.warn(`[Phase1] Could not write default config.yaml:`, err);
|
|
135
|
+
}
|
|
136
|
+
raw = defaults;
|
|
137
|
+
}
|
|
138
|
+
// Apply env var overrides for bootstrap keys
|
|
139
|
+
for (const [envKey, configPath_] of Object.entries(ENV_VAR_MAP)) {
|
|
140
|
+
const envValue = process.env[envKey];
|
|
141
|
+
if (envValue === undefined || envValue === '')
|
|
142
|
+
continue;
|
|
143
|
+
const coerced = configPath_ === 'server.port'
|
|
144
|
+
? Number(envValue)
|
|
145
|
+
: configPath_ === 'tls.enabled'
|
|
146
|
+
? envValue === 'true'
|
|
147
|
+
: envValue;
|
|
148
|
+
raw = setNested(raw, configPath_, coerced);
|
|
149
|
+
console.log(`[Phase1] Env override: ${envKey} → ${configPath_}`);
|
|
150
|
+
}
|
|
151
|
+
return config_schema_1.bootstrapSchema.parse(raw);
|
|
152
|
+
}
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// autoGenerateJwtSecret
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
/**
|
|
157
|
+
* If `jwtSecret` is null in the parsed config, generate a random secret,
|
|
158
|
+
* persist it to the YAML file, and return an updated config.
|
|
159
|
+
*/
|
|
160
|
+
function autoGenerateJwtSecret(configPath, bootstrapConfig) {
|
|
161
|
+
if (bootstrapConfig.auth.jwtSecret !== null) {
|
|
162
|
+
return bootstrapConfig;
|
|
163
|
+
}
|
|
164
|
+
const secret = (0, node_crypto_1.randomBytes)(32).toString('hex');
|
|
165
|
+
console.log('[Phase1] jwtSecret is null — auto-generating and writing to config.yaml');
|
|
166
|
+
let raw = {};
|
|
167
|
+
if (fs.existsSync(configPath)) {
|
|
168
|
+
raw = (0, types_1.asJsonObject)(yaml.load(fs.readFileSync(configPath, 'utf-8'))) ?? {};
|
|
169
|
+
}
|
|
170
|
+
const authSection = (0, types_1.asJsonObject)(raw.auth) ?? {};
|
|
171
|
+
raw.auth = { ...authSection, jwtSecret: secret };
|
|
172
|
+
const tmpPath = `${configPath}.tmp`;
|
|
173
|
+
try {
|
|
174
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
175
|
+
fs.writeFileSync(tmpPath, yaml.dump(raw, { lineWidth: 120, indent: 2, quotingType: '"' }), 'utf-8');
|
|
176
|
+
fs.renameSync(tmpPath, configPath);
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
console.warn('[Phase1] Could not write auto-generated jwtSecret to config.yaml:', err);
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
...bootstrapConfig,
|
|
183
|
+
auth: { ...bootstrapConfig.auth, jwtSecret: secret },
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// setupInfra
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
/**
|
|
190
|
+
* Phase 2: Create StorageLocationManager, ensure directories, configure TLS.
|
|
191
|
+
* Returns the full InfraContext needed by subsequent boot phases.
|
|
192
|
+
*/
|
|
193
|
+
async function setupInfra(configPath, bootstrapConfig) {
|
|
194
|
+
// Auto-generate jwtSecret if not set
|
|
195
|
+
const config = autoGenerateJwtSecret(configPath, bootstrapConfig);
|
|
196
|
+
const dataPath = path.resolve(config.server.dataPath);
|
|
197
|
+
const port = config.server.port;
|
|
198
|
+
const host = config.server.host;
|
|
199
|
+
console.log(`[Phase1] Bootstrap: port=${port}, host=${host}, dataPath=${dataPath}`);
|
|
200
|
+
// --- Phase 2: Init StorageLocationManager → ensure all dirs exist ---
|
|
201
|
+
console.log('[Phase2] Initializing storage locations…');
|
|
202
|
+
const locationManager = new storage_location_manager_1.StorageLocationManager(dataPath);
|
|
203
|
+
await locationManager.initializeDefaults();
|
|
204
|
+
const locationStatus = locationManager.getStatus();
|
|
205
|
+
for (const { name, available, path: locPath } of locationStatus) {
|
|
206
|
+
console.log(`[Phase2] Location "${name}": ${available ? 'OK' : 'UNAVAILABLE'} → ${locPath}`);
|
|
207
|
+
}
|
|
208
|
+
// --- Phase 2c: TLS certificate setup ---
|
|
209
|
+
let tlsOptions;
|
|
210
|
+
if (config.tls.enabled) {
|
|
211
|
+
// Use require() instead of import() — the ESM build of @camstack/core has
|
|
212
|
+
// broken chunks with require("fs") when leaked .js files exist in core/src/.
|
|
213
|
+
// CJS build works correctly and tsx supports require().
|
|
214
|
+
const core = require('@camstack/core');
|
|
215
|
+
const { ensureTlsCert, loadTlsCert } = core;
|
|
216
|
+
if (config.tls.certPath && config.tls.keyPath) {
|
|
217
|
+
// User-provided cert
|
|
218
|
+
console.log(`[Phase2c] Loading custom TLS cert from ${config.tls.certPath}`);
|
|
219
|
+
const pair = loadTlsCert(config.tls.certPath, config.tls.keyPath);
|
|
220
|
+
tlsOptions = { key: pair.key, cert: pair.cert };
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
// Auto-generate self-signed
|
|
224
|
+
const tlsResult = await ensureTlsCert(dataPath);
|
|
225
|
+
if (tlsResult.generated) {
|
|
226
|
+
console.log(`[Phase2c] Generated self-signed TLS cert at ${tlsResult.certPath}`);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
console.log(`[Phase2c] Using existing TLS cert at ${tlsResult.certPath}`);
|
|
230
|
+
}
|
|
231
|
+
const pair = loadTlsCert(tlsResult.certPath, tlsResult.keyPath);
|
|
232
|
+
tlsOptions = { key: pair.key, cert: pair.cert };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
bootstrapConfig: config,
|
|
237
|
+
dataPath,
|
|
238
|
+
locationManager,
|
|
239
|
+
tlsOptions,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* One-time integration-id backfill for devices created before the
|
|
4
|
+
* device-manager forwarder started stamping `integrationId` (camera
|
|
5
|
+
* providers). Maps each addon that hosts EXACTLY ONE integration to that
|
|
6
|
+
* integration id, then stamps top-level untagged devices of those addons.
|
|
7
|
+
* Multi-instance addons (e.g. Home Assistant with several brokers) are
|
|
8
|
+
* ambiguous on `addonId` alone and are skipped — they stamp going forward.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.planIntegrationIdBackfill = planIntegrationIdBackfill;
|
|
12
|
+
exports.planDeleteTimeStamps = planDeleteTimeStamps;
|
|
13
|
+
exports.runIntegrationIdBackfill = runIntegrationIdBackfill;
|
|
14
|
+
function planIntegrationIdBackfill(integrations, devices) {
|
|
15
|
+
const singleByAddon = new Map();
|
|
16
|
+
const ambiguous = new Set();
|
|
17
|
+
for (const integration of integrations) {
|
|
18
|
+
if (ambiguous.has(integration.addonId))
|
|
19
|
+
continue;
|
|
20
|
+
if (singleByAddon.has(integration.addonId)) {
|
|
21
|
+
singleByAddon.delete(integration.addonId);
|
|
22
|
+
ambiguous.add(integration.addonId);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
singleByAddon.set(integration.addonId, integration.id);
|
|
26
|
+
}
|
|
27
|
+
const stamps = [];
|
|
28
|
+
for (const device of devices) {
|
|
29
|
+
if (device.parentDeviceId !== null)
|
|
30
|
+
continue;
|
|
31
|
+
if (device.integrationId !== undefined && device.integrationId !== '')
|
|
32
|
+
continue;
|
|
33
|
+
const integrationId = singleByAddon.get(device.addonId);
|
|
34
|
+
if (integrationId === undefined)
|
|
35
|
+
continue;
|
|
36
|
+
stamps.push({ deviceId: device.id, integrationId });
|
|
37
|
+
}
|
|
38
|
+
return stamps;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Stamps to apply at integration-DELETE time so a cascade removes legacy
|
|
42
|
+
* un-tagged devices too. The boot backfill only runs on startup; a device
|
|
43
|
+
* created before stamping (or whose provider never stamps, e.g. `provider-rtsp`)
|
|
44
|
+
* keeps no `integrationId`, so once its integration is deleted it would orphan
|
|
45
|
+
* forever — `removeByIntegration` matches on `integrationId` and finds nothing.
|
|
46
|
+
*
|
|
47
|
+
* Run this in the delete handler BEFORE deleting the integration record (while
|
|
48
|
+
* it is still present in `integrations`), then stamp the returned devices and
|
|
49
|
+
* let `removeByIntegration` cascade them. Reuses the boot backfill's safety
|
|
50
|
+
* rule (only addons hosting exactly ONE integration are unambiguous) and
|
|
51
|
+
* filters to the integration being deleted so siblings are never touched.
|
|
52
|
+
*/
|
|
53
|
+
function planDeleteTimeStamps(integrationId, integrations, devices) {
|
|
54
|
+
return planIntegrationIdBackfill(integrations, devices).filter((stamp) => stamp.integrationId === integrationId);
|
|
55
|
+
}
|
|
56
|
+
async function runIntegrationIdBackfill(deps) {
|
|
57
|
+
const [integrations, devices] = await Promise.all([deps.listIntegrations(), deps.listDevices()]);
|
|
58
|
+
const stamps = planIntegrationIdBackfill(integrations, devices);
|
|
59
|
+
let stamped = 0;
|
|
60
|
+
for (const stamp of stamps) {
|
|
61
|
+
try {
|
|
62
|
+
await deps.setIntegrationId(stamp.deviceId, stamp.integrationId);
|
|
63
|
+
stamped++;
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
deps.logger.warn('integrationId backfill: stamp failed', {
|
|
67
|
+
deviceId: stamp.deviceId,
|
|
68
|
+
integrationId: stamp.integrationId,
|
|
69
|
+
error: err instanceof Error ? err.message : String(err),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (stamped > 0)
|
|
74
|
+
deps.logger.info('integrationId backfill complete', { stamped });
|
|
75
|
+
return { stamped };
|
|
76
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PostBootService = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const kernel_1 = require("@camstack/kernel");
|
|
6
|
+
const types_1 = require("@camstack/types");
|
|
7
|
+
class PostBootService {
|
|
8
|
+
eventBus;
|
|
9
|
+
logger;
|
|
10
|
+
/**
|
|
11
|
+
* Holds the marker that fired `system.restart-completed` on this boot
|
|
12
|
+
* for `LAST_RESTART_RETENTION_MS`. Lets clients query the marker
|
|
13
|
+
* after they reconnect — the live event itself is emitted before the
|
|
14
|
+
* WS resubscribe has a chance to land.
|
|
15
|
+
*/
|
|
16
|
+
static lastRestart = null;
|
|
17
|
+
/** How long the last-restart marker stays queryable after boot (5 min). */
|
|
18
|
+
static LAST_RESTART_RETENTION_MS = 5 * 60_000;
|
|
19
|
+
constructor(_addonRegistry, eventBus, loggingService) {
|
|
20
|
+
this.eventBus = eventBus;
|
|
21
|
+
this.logger = loggingService.createLogger('PostBoot');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Snapshot of the most-recent restart marker, or `null` after the
|
|
25
|
+
* retention window. The hub's `addons.getLastRestart` cap method
|
|
26
|
+
* delegates here.
|
|
27
|
+
*/
|
|
28
|
+
static getLastRestart() {
|
|
29
|
+
const entry = PostBootService.lastRestart;
|
|
30
|
+
if (entry === null)
|
|
31
|
+
return null;
|
|
32
|
+
if (entry.expiresAt <= Date.now()) {
|
|
33
|
+
PostBootService.lastRestart = null;
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return entry.payload;
|
|
37
|
+
}
|
|
38
|
+
async run(context) {
|
|
39
|
+
const { port, host, dataPath, trpcRegistered } = context;
|
|
40
|
+
// Device stream wiring moved into `addon-stream-broker` — the addon
|
|
41
|
+
// does its own initial sync against `ctx.deviceRegistry` plus listens
|
|
42
|
+
// to `DeviceRegistered` events for future registrations. No kernel
|
|
43
|
+
// orchestration shim needed here anymore.
|
|
44
|
+
// Emit system.boot event
|
|
45
|
+
this.eventBus.emit({
|
|
46
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
47
|
+
timestamp: new Date(),
|
|
48
|
+
source: { type: 'core', id: 'system' },
|
|
49
|
+
category: types_1.EventCategory.SystemBoot,
|
|
50
|
+
data: { port, host, trpcRegistered, dataPath },
|
|
51
|
+
});
|
|
52
|
+
// If the previous shutdown was driven by RestartCoordinator, emit a
|
|
53
|
+
// `system.restart-completed` event so the admin UI can surface a
|
|
54
|
+
// success toast describing what changed (framework update, manual
|
|
55
|
+
// restart, …). `readPendingRestart` clears the marker atomically so
|
|
56
|
+
// we never re-fire on a crash-loop boot.
|
|
57
|
+
this.emitRestartCompletedIfPending(dataPath);
|
|
58
|
+
}
|
|
59
|
+
emitRestartCompletedIfPending(dataDir) {
|
|
60
|
+
const marker = (0, kernel_1.readPendingRestart)(dataDir);
|
|
61
|
+
if (marker === null)
|
|
62
|
+
return;
|
|
63
|
+
const payload = {
|
|
64
|
+
kind: marker.kind,
|
|
65
|
+
requestedAt: marker.requestedAt,
|
|
66
|
+
...(marker.packageName !== undefined ? { packageName: marker.packageName } : {}),
|
|
67
|
+
...(marker.fromVersion !== undefined ? { fromVersion: marker.fromVersion } : {}),
|
|
68
|
+
...(marker.toVersion !== undefined ? { toVersion: marker.toVersion } : {}),
|
|
69
|
+
...(marker.requestedBy !== undefined ? { requestedBy: marker.requestedBy } : {}),
|
|
70
|
+
};
|
|
71
|
+
this.logger.info('Restart completed', { meta: payload });
|
|
72
|
+
PostBootService.lastRestart = {
|
|
73
|
+
payload,
|
|
74
|
+
expiresAt: Date.now() + PostBootService.LAST_RESTART_RETENTION_MS,
|
|
75
|
+
};
|
|
76
|
+
this.eventBus.emit({
|
|
77
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
78
|
+
timestamp: new Date(),
|
|
79
|
+
source: { type: 'core', id: 'system' },
|
|
80
|
+
category: types_1.EventCategory.SystemRestartCompleted,
|
|
81
|
+
data: payload,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
exports.PostBootService = PostBootService;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AddonCallGateway = void 0;
|
|
4
|
+
const REMOTE_TIMEOUT_MS = 10_000;
|
|
5
|
+
class AddonCallGateway {
|
|
6
|
+
deps;
|
|
7
|
+
constructor(deps) {
|
|
8
|
+
this.deps = deps;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Classify where an addon runs. `nodeId: 'hub'` from a caller means "the hub
|
|
12
|
+
* cluster", NOT "force in-process" — a forked hub-local addon is still a
|
|
13
|
+
* `hub-local-child` (UDS), never the in-process path (which is only the
|
|
14
|
+
* `@camstack/core` builtins that have no forked runner).
|
|
15
|
+
*/
|
|
16
|
+
classify(addonId, explicitNodeId) {
|
|
17
|
+
const resolved = this.deps.resolveNode(addonId);
|
|
18
|
+
const onHub = resolved === 'hub' || resolved === this.deps.hubNodeId;
|
|
19
|
+
if (onHub) {
|
|
20
|
+
const childRegistry = this.deps.getChildRegistry();
|
|
21
|
+
if (childRegistry !== null && childRegistry.isChildKnown(addonId)) {
|
|
22
|
+
return { kind: 'hub-local-child' };
|
|
23
|
+
}
|
|
24
|
+
return { kind: 'in-process' };
|
|
25
|
+
}
|
|
26
|
+
const onThisHub = explicitNodeId === 'hub' || explicitNodeId === this.deps.hubNodeId;
|
|
27
|
+
const baseNodeId = explicitNodeId && !onThisHub ? explicitNodeId : resolved;
|
|
28
|
+
return { kind: 'remote-agent', baseNodeId };
|
|
29
|
+
}
|
|
30
|
+
/** True when the addon is an in-process hub builtin (caller invokes directly). */
|
|
31
|
+
isInProcess(addonId, explicitNodeId) {
|
|
32
|
+
return this.classify(addonId, explicitNodeId).kind === 'in-process';
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Dispatch a forked addon-level call to wherever the addon runs:
|
|
36
|
+
* - `hub-local-child` → UDS `LocalChildRegistry.callAddonOnChild`
|
|
37
|
+
* - `remote-agent` → Moleculer `broker.call`
|
|
38
|
+
* Throws for `in-process` — that addon has no forked surface, so the caller
|
|
39
|
+
* must invoke the in-process instance directly (the invocation is
|
|
40
|
+
* surface-specific; only the ROUTING is centralised here).
|
|
41
|
+
*/
|
|
42
|
+
async callForked(addonId, input, explicitNodeId) {
|
|
43
|
+
const dest = this.classify(addonId, explicitNodeId);
|
|
44
|
+
const fullInput = { ...input, addonId };
|
|
45
|
+
switch (dest.kind) {
|
|
46
|
+
case 'hub-local-child': {
|
|
47
|
+
const childRegistry = this.deps.getChildRegistry();
|
|
48
|
+
if (childRegistry === null) {
|
|
49
|
+
throw new Error(`AddonCallGateway: child registry unavailable for "${addonId}"`);
|
|
50
|
+
}
|
|
51
|
+
return childRegistry.callAddonOnChild(addonId, fullInput);
|
|
52
|
+
}
|
|
53
|
+
case 'remote-agent':
|
|
54
|
+
return this.callRemoteAgent(addonId, dest.baseNodeId, fullInput);
|
|
55
|
+
case 'in-process':
|
|
56
|
+
throw new Error(`AddonCallGateway: addon "${addonId}" runs in-process — invoke it directly`);
|
|
57
|
+
default: {
|
|
58
|
+
const _exhaustive = dest;
|
|
59
|
+
throw new Error(`AddonCallGateway: unhandled destination ${JSON.stringify(_exhaustive)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Map an addon-level call to the remote agent's Moleculer action. */
|
|
64
|
+
async callRemoteAgent(addonId, baseNodeId, input) {
|
|
65
|
+
const workerNodeId = this.resolveWorkerNodeId(addonId, baseNodeId);
|
|
66
|
+
const opts = workerNodeId
|
|
67
|
+
? { nodeID: workerNodeId, timeout: REMOTE_TIMEOUT_MS }
|
|
68
|
+
: { timeout: REMOTE_TIMEOUT_MS };
|
|
69
|
+
if (input.target === 'settings') {
|
|
70
|
+
if (input.method == null) {
|
|
71
|
+
throw new Error(`AddonCallGateway: settings call to "${addonId}" missing method`);
|
|
72
|
+
}
|
|
73
|
+
return this.deps.broker.call(`${addonId}.settings.${input.method}`, (input.args ?? {}), opts);
|
|
74
|
+
}
|
|
75
|
+
// routes/custom are hub-local-child surfaces (mounted / invoked on the
|
|
76
|
+
// owning node); they are not proxied to a remote agent through this gateway.
|
|
77
|
+
throw new Error(`AddonCallGateway: target "${input.target}" not supported for remote agent "${baseNodeId}"`);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Resolve the Moleculer nodeID that actually hosts an addon's service.
|
|
81
|
+
* Forkable addons register under `${baseNodeId}/${addonId}`; in-process
|
|
82
|
+
* addons under the base nodeId. The registry is the ground truth — baseNodeId
|
|
83
|
+
* is a hint. (Moved verbatim from `addon-settings-provider.ts`.)
|
|
84
|
+
*/
|
|
85
|
+
resolveWorkerNodeId(addonId, baseNodeId) {
|
|
86
|
+
const registry = this.deps.broker.registry;
|
|
87
|
+
const services = registry.getServiceList({ onlyAvailable: true });
|
|
88
|
+
const exactNode = `${baseNodeId}/${addonId}`;
|
|
89
|
+
const preferred = services.find((s) => s.name === addonId && s.nodeID === exactNode);
|
|
90
|
+
if (preferred)
|
|
91
|
+
return preferred.nodeID;
|
|
92
|
+
const anyForBase = services.find((s) => s.name === addonId && s.nodeID === baseNodeId);
|
|
93
|
+
if (anyForBase)
|
|
94
|
+
return anyForBase.nodeID;
|
|
95
|
+
const anyWithName = services.find((s) => s.name === addonId);
|
|
96
|
+
return anyWithName?.nodeID ?? null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
exports.AddonCallGateway = AddonCallGateway;
|