@camstack/system 1.2.316 → 1.2.317
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/addon-runner.js +1 -1
- package/dist/addon-runner.mjs +1 -1
- package/dist/auth/auth-manager.d.ts +11 -0
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/camera-grid/index.js +1 -1
- package/dist/builtins/camera-grid/index.mjs +1 -1
- package/dist/builtins/composer/composer.addon.js +347 -181
- package/dist/builtins/composer/composer.addon.mjs +347 -181
- package/dist/builtins/composer/composition-evaluation.d.ts +37 -0
- package/dist/builtins/composer/composition-runtime.d.ts +11 -52
- package/dist/builtins/composer/composition-target.d.ts +50 -0
- package/dist/builtins/composer/existing-target.d.ts +19 -1
- package/dist/builtins/composer/own-field-reads.d.ts +19 -0
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
- package/dist/builtins/device-manager/device-manager.addon.js +265 -29
- package/dist/builtins/device-manager/device-manager.addon.mjs +265 -29
- package/dist/builtins/device-manager/device-provider-context.d.ts +6 -0
- package/dist/builtins/device-manager/device-source-activation.d.ts +26 -1
- package/dist/builtins/device-manager/device-state-mirror.d.ts +41 -9
- package/dist/builtins/device-manager/mirror-row-writer.d.ts +59 -1
- package/dist/builtins/doorbell/binding-mirror.d.ts +45 -81
- package/dist/builtins/doorbell/doorbell-extension-slot.d.ts +4 -3
- package/dist/builtins/doorbell/virtual-doorbell.addon.d.ts +53 -11
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +207 -82
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +207 -82
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +47 -3
- package/dist/builtins/local-auth/local-auth.addon.mjs +46 -4
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +4 -4
- package/dist/builtins/system-config/system-config.addon.mjs +4 -4
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{child-cap-dispatch-AAltfBUH.mjs → child-cap-dispatch-C2VINHu1.mjs} +70 -18
- package/dist/{child-cap-dispatch-DzcUd_ct.js → child-cap-dispatch-C8uts1P4.js} +70 -18
- package/dist/{composition-sources-fUyF4z39.mjs → composition-sources-DVClu006.mjs} +1 -1
- package/dist/{composition-sources-BwE1POnF.js → composition-sources-Dep7nBwu.js} +1 -1
- package/dist/{dist-BPvku43i.mjs → dist-Dori4pf3.mjs} +85 -6
- package/dist/{dist-RAl10EfG.js → dist-a6p0V-IL.js} +85 -6
- package/dist/index.js +739 -350
- package/dist/index.mjs +737 -352
- package/dist/kernel/bootstrap-config.d.ts +8 -0
- package/dist/kernel/capability-registry.d.ts +30 -0
- package/dist/kernel/config-manager.d.ts +75 -129
- package/dist/kernel/index.d.ts +5 -2
- package/dist/kernel/moleculer/runtime-state-load.d.ts +19 -0
- package/dist/kernel/settings-door-view.d.ts +24 -1
- package/dist/kernel/settings-read-error.d.ts +37 -0
- package/dist/kernel/settings-view-types.d.ts +105 -0
- package/dist/kernel/store-settings-view.d.ts +17 -0
- package/dist/kernel/system-settings-mirror.d.ts +34 -0
- package/dist/kernel/system-settings-policy.d.ts +47 -0
- package/dist/{retired-settings-keys-D3rGTGq5.js → retired-settings-keys-DC1uHWPZ.js} +1 -1
- package/dist/{retired-settings-keys-DqVWSrVw.mjs → retired-settings-keys-DVcKRgzm.mjs} +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_chunk = require("./chunk-Cek0wNdY.js");
|
|
3
|
-
const require_dist = require("./dist-
|
|
3
|
+
const require_dist = require("./dist-a6p0V-IL.js");
|
|
4
4
|
const require_builtins_alerts_alerts_addon = require("./builtins/alerts/alerts.addon.js");
|
|
5
5
|
require("./builtins/alerts/index.js");
|
|
6
6
|
const require_builtins_composer_composer_addon = require("./builtins/composer/composer.addon.js");
|
|
@@ -29,7 +29,7 @@ require("./builtins/system-config/index.js");
|
|
|
29
29
|
const require_builtins_winston_logging_index = require("./builtins/winston-logging/index.js");
|
|
30
30
|
const require_event_loop_stall_monitor = require("./event-loop-stall-monitor-ey4mbofz.js");
|
|
31
31
|
const require_tls$1 = require("./tls-D_wUeOTD.js");
|
|
32
|
-
const require_child_cap_dispatch = require("./child-cap-dispatch-
|
|
32
|
+
const require_child_cap_dispatch = require("./child-cap-dispatch-C8uts1P4.js");
|
|
33
33
|
const require_resource_monitor = require("./resource-monitor-DzNn_IZK.js");
|
|
34
34
|
const require_custom_action_registry = require("./custom-action-registry-acBY3mHA.js");
|
|
35
35
|
let zod = require("zod");
|
|
@@ -2752,7 +2752,7 @@ async function ensureAddonNativePrebuilds(addonDir, options) {
|
|
|
2752
2752
|
}
|
|
2753
2753
|
//#endregion
|
|
2754
2754
|
//#region src/kernel/addon-loader.ts
|
|
2755
|
-
function isRecord$
|
|
2755
|
+
function isRecord$3(value) {
|
|
2756
2756
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2757
2757
|
}
|
|
2758
2758
|
/**
|
|
@@ -2850,9 +2850,9 @@ async function importAddonModuleFresh(entryPath) {
|
|
|
2850
2850
|
* AddonPackageManifest + AddonDeclaration.
|
|
2851
2851
|
*/
|
|
2852
2852
|
function toAddonPackageManifest(value) {
|
|
2853
|
-
if (!isRecord$
|
|
2853
|
+
if (!isRecord$3(value)) return void 0;
|
|
2854
2854
|
if (!Array.isArray(value.addons)) return void 0;
|
|
2855
|
-
if (!value.addons.every((a) => isRecord$
|
|
2855
|
+
if (!value.addons.every((a) => isRecord$3(a) && typeof a.id === "string")) return void 0;
|
|
2856
2856
|
return value;
|
|
2857
2857
|
}
|
|
2858
2858
|
/** Minimal no-op logger for default parameter */
|
|
@@ -3002,7 +3002,7 @@ var AddonLoader = class {
|
|
|
3002
3002
|
}
|
|
3003
3003
|
if (!node_fs.existsSync(entryPath)) throw new Error(`Entry not found: ${entryPath}`);
|
|
3004
3004
|
const modUnknown = await importAddonModuleFresh(entryPath);
|
|
3005
|
-
const mod = isRecord$
|
|
3005
|
+
const mod = isRecord$3(modUnknown) ? modUnknown : {};
|
|
3006
3006
|
const AddonClass = require_child_cap_dispatch.resolveAddonClass(mod);
|
|
3007
3007
|
if (!AddonClass) throw new Error(`No addon class in ${entryPath}`);
|
|
3008
3008
|
this.addons.set(declaration.id, {
|
|
@@ -3018,7 +3018,7 @@ var AddonLoader = class {
|
|
|
3018
3018
|
/** Load addon from a direct path (for development/testing) */
|
|
3019
3019
|
async loadFromPath(addonId, modulePath, packageName, declaration, packageVersion = "0.0.0") {
|
|
3020
3020
|
const modUnknown = await importAddonModuleFresh(modulePath);
|
|
3021
|
-
const mod = isRecord$
|
|
3021
|
+
const mod = isRecord$3(modUnknown) ? modUnknown : {};
|
|
3022
3022
|
const AddonClass = require_child_cap_dispatch.resolveAddonClass(mod);
|
|
3023
3023
|
if (!AddonClass) throw new Error(`Module ${modulePath} has no default export`);
|
|
3024
3024
|
this.addons.set(addonId, {
|
|
@@ -6315,6 +6315,60 @@ var CapabilityRegistry = class CapabilityRegistry {
|
|
|
6315
6315
|
addonId
|
|
6316
6316
|
});
|
|
6317
6317
|
}
|
|
6318
|
+
/**
|
|
6319
|
+
* Re-apply the operator's PERSISTED preferences to providers that
|
|
6320
|
+
* registered before those preferences could be read — once, when the boot
|
|
6321
|
+
* read of them settles (D675).
|
|
6322
|
+
*
|
|
6323
|
+
* `registerProvider` consults the preference readers at the moment a
|
|
6324
|
+
* provider registers. With the settings engine isolated (D231) the
|
|
6325
|
+
* preferences arrive over an async door, so a provider can register before
|
|
6326
|
+
* they are known; the readers then answer "no preference" and the default
|
|
6327
|
+
* wins. This replays the answer the readers give NOW over every registered
|
|
6328
|
+
* provider: a singleton whose persisted choice is registered and not active
|
|
6329
|
+
* is switched to it, and a collection provider the operator disabled is
|
|
6330
|
+
* disabled again. It is one pass, triggered by the read settling — not a
|
|
6331
|
+
* wait, a poll or a retry (D3).
|
|
6332
|
+
*
|
|
6333
|
+
* `skip` names the capabilities that must not be switched live (infra caps
|
|
6334
|
+
* need a restart — the same rule `capabilities.setPreference` applies).
|
|
6335
|
+
*/
|
|
6336
|
+
applyPersistedPreferences(skip) {
|
|
6337
|
+
const singletons = [];
|
|
6338
|
+
const disabled = [];
|
|
6339
|
+
for (const [capability, state] of this.capabilities) {
|
|
6340
|
+
if (skip(capability)) continue;
|
|
6341
|
+
if (state.definition.mode === "singleton") {
|
|
6342
|
+
const choice = this.configReader?.(capability);
|
|
6343
|
+
if (choice === void 0 || !state.providers.has(choice)) continue;
|
|
6344
|
+
if (state.activeAddonId === choice) continue;
|
|
6345
|
+
state.activeAddonId = choice;
|
|
6346
|
+
singletons.push({
|
|
6347
|
+
capability,
|
|
6348
|
+
addonId: choice
|
|
6349
|
+
});
|
|
6350
|
+
this.emitEvent("capability:singleton-changed", {
|
|
6351
|
+
capability,
|
|
6352
|
+
addonId: choice
|
|
6353
|
+
});
|
|
6354
|
+
} else if (state.definition.mode === "collection") for (const addonId of this.collectionConfigReader?.(capability) ?? []) {
|
|
6355
|
+
if (!state.providers.has(addonId) || state.disabledProviders.has(addonId)) continue;
|
|
6356
|
+
state.disabledProviders.add(addonId);
|
|
6357
|
+
disabled.push({
|
|
6358
|
+
capability,
|
|
6359
|
+
addonId
|
|
6360
|
+
});
|
|
6361
|
+
this.emitEvent("capability:collection-provider-disabled", {
|
|
6362
|
+
capability,
|
|
6363
|
+
addonId
|
|
6364
|
+
});
|
|
6365
|
+
}
|
|
6366
|
+
}
|
|
6367
|
+
return {
|
|
6368
|
+
singletons,
|
|
6369
|
+
disabled
|
|
6370
|
+
};
|
|
6371
|
+
}
|
|
6318
6372
|
/** Enable a previously disabled collection provider. */
|
|
6319
6373
|
enableCollectionProvider(capability, addonId) {
|
|
6320
6374
|
const state = this.capabilities.get(capability);
|
|
@@ -7457,14 +7511,14 @@ async function runHubAddonBoot(steps) {
|
|
|
7457
7511
|
}
|
|
7458
7512
|
//#endregion
|
|
7459
7513
|
//#region src/kernel/settings-door-view.ts
|
|
7460
|
-
function isRecord$
|
|
7514
|
+
function isRecord$2(value) {
|
|
7461
7515
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7462
7516
|
}
|
|
7463
7517
|
function asBlob(value) {
|
|
7464
|
-
return isRecord$
|
|
7518
|
+
return isRecord$2(value) ? value : {};
|
|
7465
7519
|
}
|
|
7466
7520
|
function unwrapValue(parsed) {
|
|
7467
|
-
if (!isRecord$
|
|
7521
|
+
if (!isRecord$2(parsed) || !("value" in parsed)) return parsed;
|
|
7468
7522
|
return parsed.value;
|
|
7469
7523
|
}
|
|
7470
7524
|
/**
|
|
@@ -7514,7 +7568,7 @@ var PREFIX_PAGE_BUDGET = 512;
|
|
|
7514
7568
|
* A throw is the point: a caller that cannot be answered completely must not
|
|
7515
7569
|
* be handed something that looks like a complete answer.
|
|
7516
7570
|
*/
|
|
7517
|
-
async function loadPrefixed(door, collection, prefix) {
|
|
7571
|
+
async function loadPrefixed(door, collection, prefix, decode = unwrapValue) {
|
|
7518
7572
|
const range = require_builtins_sqlite_storage_sqlite_settings_addon.prefixRange(prefix);
|
|
7519
7573
|
const result = {};
|
|
7520
7574
|
let cursor = range?.lo ?? null;
|
|
@@ -7534,7 +7588,7 @@ async function loadPrefixed(door, collection, prefix) {
|
|
|
7534
7588
|
for (const row of rows) {
|
|
7535
7589
|
if (range !== null && (row.id < range.lo || row.id >= range.hi)) continue;
|
|
7536
7590
|
if (!row.id.startsWith(prefix)) continue;
|
|
7537
|
-
result[row.id.slice(prefix.length)] =
|
|
7591
|
+
result[row.id.slice(prefix.length)] = decode(row.data);
|
|
7538
7592
|
}
|
|
7539
7593
|
if (rows.length < PREFIX_PAGE_ROWS) return result;
|
|
7540
7594
|
const last = rows[rows.length - 1];
|
|
@@ -7544,6 +7598,32 @@ async function loadPrefixed(door, collection, prefix) {
|
|
|
7544
7598
|
cursor = last.id;
|
|
7545
7599
|
}
|
|
7546
7600
|
}
|
|
7601
|
+
/**
|
|
7602
|
+
* Every `system-settings` row under `prefix`, keyed by the suffix — or a throw.
|
|
7603
|
+
*
|
|
7604
|
+
* `system-settings` stores the value ITSELF (`setSystem` / `door.set` write
|
|
7605
|
+
* `JSON.stringify(value)`), mostly scalars. The engine's `query` hands a KV row
|
|
7606
|
+
* back parsed only when its text is an object or an array; a scalar comes back
|
|
7607
|
+
* as `{ value: <its JSON text> }` — `"12h"` arrives as `{ value: '"12h"' }`.
|
|
7608
|
+
* That is not a value anyone stored, and guessing it back would misread a
|
|
7609
|
+
* stored object that happens to be `{ value: … }`. So the range read only
|
|
7610
|
+
* ENUMERATES the keys (same keyset walk and refusal as every prefix read
|
|
7611
|
+
* here), and each value is taken from `get`, which parses the row exactly
|
|
7612
|
+
* (D675). These reads are small and rare: a section's handful of keys when a
|
|
7613
|
+
* form opens, and the capability preferences once at boot.
|
|
7614
|
+
*/
|
|
7615
|
+
async function loadSystemSettingsPrefix(door, prefix, mayRead = () => true) {
|
|
7616
|
+
const listed = await loadPrefixed(door, "system-settings", prefix, () => null);
|
|
7617
|
+
const result = {};
|
|
7618
|
+
for (const suffix of Object.keys(listed).filter(mayRead)) {
|
|
7619
|
+
const value = await door.get({
|
|
7620
|
+
collection: "system-settings",
|
|
7621
|
+
key: `${prefix}${suffix}`
|
|
7622
|
+
});
|
|
7623
|
+
if (value !== void 0) result[suffix] = value;
|
|
7624
|
+
}
|
|
7625
|
+
return result;
|
|
7626
|
+
}
|
|
7547
7627
|
async function replacePrefixed(door, collection, prefix, values, wrap) {
|
|
7548
7628
|
const range = require_builtins_sqlite_storage_sqlite_settings_addon.prefixRange(prefix);
|
|
7549
7629
|
if (range !== null) await door.deleteWhere({
|
|
@@ -7565,6 +7645,24 @@ function createDoorSettingsView(addonId, door, sections) {
|
|
|
7565
7645
|
async readAddonStore() {
|
|
7566
7646
|
return loadPrefixed(door, "addon-settings", addonPrefix);
|
|
7567
7647
|
},
|
|
7648
|
+
async readAddonStoreKeyResult(key) {
|
|
7649
|
+
try {
|
|
7650
|
+
const row = await door.get({
|
|
7651
|
+
collection: "addon-settings",
|
|
7652
|
+
key: `${addonPrefix}${key}`
|
|
7653
|
+
});
|
|
7654
|
+
return row === void 0 ? { kind: "absent" } : {
|
|
7655
|
+
kind: "present",
|
|
7656
|
+
value: unwrapValue(row)
|
|
7657
|
+
};
|
|
7658
|
+
} catch (err) {
|
|
7659
|
+
return {
|
|
7660
|
+
kind: "unreadable",
|
|
7661
|
+
reason: "store-read-failed",
|
|
7662
|
+
error: err instanceof Error ? err.message : String(err)
|
|
7663
|
+
};
|
|
7664
|
+
}
|
|
7665
|
+
},
|
|
7568
7666
|
async writeAddonStore(patch) {
|
|
7569
7667
|
await replacePrefixed(door, "addon-settings", addonPrefix, {
|
|
7570
7668
|
...await loadPrefixed(door, "addon-settings", addonPrefix),
|
|
@@ -7643,6 +7741,121 @@ function createDoorSettingsView(addonId, door, sections) {
|
|
|
7643
7741
|
};
|
|
7644
7742
|
}
|
|
7645
7743
|
//#endregion
|
|
7744
|
+
//#region src/kernel/config-schema.ts
|
|
7745
|
+
/** Default data directory — used when CAMSTACK_DATA env var is not set */
|
|
7746
|
+
var DEFAULT_DATA_PATH = "camstack-data";
|
|
7747
|
+
var serverSchema = zod.z.object({
|
|
7748
|
+
port: zod.z.number().default(4443),
|
|
7749
|
+
host: zod.z.string().default("0.0.0.0"),
|
|
7750
|
+
dataPath: zod.z.string().default(DEFAULT_DATA_PATH)
|
|
7751
|
+
});
|
|
7752
|
+
var authSchema = zod.z.object({
|
|
7753
|
+
jwtSecret: zod.z.string().nullable().default(null),
|
|
7754
|
+
adminUsername: zod.z.string().default("admin"),
|
|
7755
|
+
adminPassword: zod.z.string().default("changeme")
|
|
7756
|
+
});
|
|
7757
|
+
var hubSchema = zod.z.object({
|
|
7758
|
+
url: zod.z.string().default("ws://localhost:4443/agent"),
|
|
7759
|
+
token: zod.z.string().default("")
|
|
7760
|
+
});
|
|
7761
|
+
var agentSchema = zod.z.object({
|
|
7762
|
+
name: zod.z.string().default(""),
|
|
7763
|
+
/** Port for the agent status page (minimal HTML) */
|
|
7764
|
+
statusPort: zod.z.number().default(4444)
|
|
7765
|
+
});
|
|
7766
|
+
var tlsSchema = zod.z.object({
|
|
7767
|
+
/** Enable HTTPS (default: true) */
|
|
7768
|
+
enabled: zod.z.boolean().default(true),
|
|
7769
|
+
/** Path to custom cert file (PEM). If not set, auto-generates self-signed. */
|
|
7770
|
+
certPath: zod.z.string().optional(),
|
|
7771
|
+
/** Path to custom key file (PEM). Required if certPath is set. */
|
|
7772
|
+
keyPath: zod.z.string().optional()
|
|
7773
|
+
});
|
|
7774
|
+
/**
|
|
7775
|
+
* Bootstrap-time addon configuration — read PURELY from YAML, before the
|
|
7776
|
+
* kernel can talk to the settings-store. This breaks the chicken-and-egg
|
|
7777
|
+
* where the settings store itself is implemented by an addon: the kernel
|
|
7778
|
+
* needs to know WHICH addon to load first to provide the store, and that
|
|
7779
|
+
* decision can't itself live in the store.
|
|
7780
|
+
*
|
|
7781
|
+
* Backward-compatible: every field is optional + defaults reproduce the
|
|
7782
|
+
* current hard-coded behavior. Existing config.yaml files keep working.
|
|
7783
|
+
*/
|
|
7784
|
+
var bootstrapAddonsSchema = zod.z.object({
|
|
7785
|
+
/**
|
|
7786
|
+
* Addon implementing the `settings-store` capability. Loaded FIRST,
|
|
7787
|
+
* before any other addon is enabled — every other addon's enable
|
|
7788
|
+
* state, settings, and ordering lives in the store it provides.
|
|
7789
|
+
*
|
|
7790
|
+
* Default: '@camstack/addon-sqlite-storage' (the workspace's
|
|
7791
|
+
* default SQLite-backed store). Future alternatives could swap this
|
|
7792
|
+
* for postgres/redis without touching kernel code.
|
|
7793
|
+
*/
|
|
7794
|
+
settingsStoreAddon: zod.z.string().default("@camstack/addon-sqlite-storage"),
|
|
7795
|
+
/**
|
|
7796
|
+
* How the launcher provisions REQUIRED_PACKAGES into `data/addons/`
|
|
7797
|
+
* on startup.
|
|
7798
|
+
*
|
|
7799
|
+
* - `'npm'` — pull from the npm registry (production default).
|
|
7800
|
+
* - `'local'` — copy `packages/<X>/` from the workspace into
|
|
7801
|
+
* `data/addons/<X>/`. Use for the Electron-
|
|
7802
|
+
* packaged bundle and the e2e harness.
|
|
7803
|
+
* - `'symlink'` — point `data/addons/<X>` at the workspace source
|
|
7804
|
+
* dir via a directory symlink. Dev-only: a
|
|
7805
|
+
* workspace rebuild (`npm run build -w <X>`) is
|
|
7806
|
+
* immediately reflected at the next addon load,
|
|
7807
|
+
* no copy step. Falls back to `'local'` on
|
|
7808
|
+
* platforms that reject symlink creation (Windows
|
|
7809
|
+
* without developer-mode).
|
|
7810
|
+
*
|
|
7811
|
+
* The `CAMSTACK_INSTALL_SOURCE` env var, when set, wins over this
|
|
7812
|
+
* field. Leave undefined to keep the launcher's default behaviour
|
|
7813
|
+
* (npm when no workspace, local when a bundled addons dir is
|
|
7814
|
+
* present, …).
|
|
7815
|
+
*/
|
|
7816
|
+
installSource: zod.z.enum([
|
|
7817
|
+
"npm",
|
|
7818
|
+
"local",
|
|
7819
|
+
"symlink"
|
|
7820
|
+
]).optional(),
|
|
7821
|
+
/**
|
|
7822
|
+
* Override the kernel's hard-coded `AddonInstaller.REQUIRED_PACKAGES`.
|
|
7823
|
+
* Passing an array here REPLACES the default list entirely — useful
|
|
7824
|
+
* for stripped-down deployments (only the bundle of addons the
|
|
7825
|
+
* operator actually wants) and for forcing a different storage
|
|
7826
|
+
* backend at first boot.
|
|
7827
|
+
*
|
|
7828
|
+
* Leave undefined to use the kernel-shipped defaults.
|
|
7829
|
+
*/
|
|
7830
|
+
requiredAddons: zod.z.array(zod.z.string()).optional(),
|
|
7831
|
+
/**
|
|
7832
|
+
* Disable list applied AFTER the settings-store reports the enabled
|
|
7833
|
+
* set. Operator escape hatch — boot without a known-broken addon
|
|
7834
|
+
* without having to mutate the store.
|
|
7835
|
+
*/
|
|
7836
|
+
forceDisable: zod.z.array(zod.z.string()).default([])
|
|
7837
|
+
});
|
|
7838
|
+
/** Bootstrap config -- loaded from config.yaml ONLY at startup.
|
|
7839
|
+
* All other settings live in SQL (system_settings table). */
|
|
7840
|
+
var bootstrapSchema = zod.z.object({
|
|
7841
|
+
/** Server mode: 'hub' (full server) or 'agent' (worker node) */
|
|
7842
|
+
mode: zod.z.enum(["hub", "agent"]).default("hub"),
|
|
7843
|
+
server: serverSchema.default(serverSchema.parse({})),
|
|
7844
|
+
auth: authSchema.default(authSchema.parse({})),
|
|
7845
|
+
/** Hub connection config — only used when mode='agent' */
|
|
7846
|
+
hub: hubSchema.default(hubSchema.parse({})),
|
|
7847
|
+
/** Agent-specific config — only used when mode='agent' */
|
|
7848
|
+
agent: agentSchema.default(agentSchema.parse({})),
|
|
7849
|
+
/** TLS configuration */
|
|
7850
|
+
tls: tlsSchema.default(tlsSchema.parse({})),
|
|
7851
|
+
/**
|
|
7852
|
+
* Addon bootstrap directives — minimum addon set needed before the
|
|
7853
|
+
* kernel can reach the settings-store. See bootstrapAddonsSchema.
|
|
7854
|
+
* Spec: docs/superpowers/specs/2026-05-15-yaml-bootstrap-expansion.md
|
|
7855
|
+
*/
|
|
7856
|
+
bootstrap: bootstrapAddonsSchema.default(bootstrapAddonsSchema.parse({}))
|
|
7857
|
+
});
|
|
7858
|
+
//#endregion
|
|
7646
7859
|
//#region ../../node_modules/js-yaml/dist/js-yaml.mjs
|
|
7647
7860
|
/*! js-yaml 4.2.0 https://github.com/nodeca/js-yaml @license MIT */
|
|
7648
7861
|
var __create = Object.create;
|
|
@@ -9972,125 +10185,22 @@ var import_js_yaml = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((ex
|
|
|
9972
10185
|
var { Type, Schema, FAILSAFE_SCHEMA, JSON_SCHEMA, CORE_SCHEMA, DEFAULT_SCHEMA, load: load$1, loadAll, dump, YAMLException, types: types$3, safeLoad, safeLoadAll, safeDump } = import_js_yaml.default;
|
|
9973
10186
|
import_js_yaml.default;
|
|
9974
10187
|
//#endregion
|
|
9975
|
-
//#region src/kernel/config
|
|
9976
|
-
/** Default data directory — used when CAMSTACK_DATA env var is not set */
|
|
9977
|
-
var DEFAULT_DATA_PATH = "camstack-data";
|
|
9978
|
-
var serverSchema = zod.z.object({
|
|
9979
|
-
port: zod.z.number().default(4443),
|
|
9980
|
-
host: zod.z.string().default("0.0.0.0"),
|
|
9981
|
-
dataPath: zod.z.string().default(DEFAULT_DATA_PATH)
|
|
9982
|
-
});
|
|
9983
|
-
var authSchema = zod.z.object({
|
|
9984
|
-
jwtSecret: zod.z.string().nullable().default(null),
|
|
9985
|
-
adminUsername: zod.z.string().default("admin"),
|
|
9986
|
-
adminPassword: zod.z.string().default("changeme")
|
|
9987
|
-
});
|
|
9988
|
-
var hubSchema = zod.z.object({
|
|
9989
|
-
url: zod.z.string().default("ws://localhost:4443/agent"),
|
|
9990
|
-
token: zod.z.string().default("")
|
|
9991
|
-
});
|
|
9992
|
-
var agentSchema = zod.z.object({
|
|
9993
|
-
name: zod.z.string().default(""),
|
|
9994
|
-
/** Port for the agent status page (minimal HTML) */
|
|
9995
|
-
statusPort: zod.z.number().default(4444)
|
|
9996
|
-
});
|
|
9997
|
-
var tlsSchema = zod.z.object({
|
|
9998
|
-
/** Enable HTTPS (default: true) */
|
|
9999
|
-
enabled: zod.z.boolean().default(true),
|
|
10000
|
-
/** Path to custom cert file (PEM). If not set, auto-generates self-signed. */
|
|
10001
|
-
certPath: zod.z.string().optional(),
|
|
10002
|
-
/** Path to custom key file (PEM). Required if certPath is set. */
|
|
10003
|
-
keyPath: zod.z.string().optional()
|
|
10004
|
-
});
|
|
10188
|
+
//#region src/kernel/bootstrap-config.ts
|
|
10005
10189
|
/**
|
|
10006
|
-
*
|
|
10007
|
-
*
|
|
10008
|
-
*
|
|
10009
|
-
*
|
|
10010
|
-
* decision can't itself live in the store.
|
|
10011
|
-
*
|
|
10012
|
-
* Backward-compatible: every field is optional + defaults reproduce the
|
|
10013
|
-
* current hard-coded behavior. Existing config.yaml files keep working.
|
|
10190
|
+
* config.yaml + env: loading, the section writer and the legacy `raw` view —
|
|
10191
|
+
* `ConfigManager`'s bootstrap layer, split out of `config-manager.ts` (D675)
|
|
10192
|
+
* so that file stays one class. The `console.*` calls are the documented
|
|
10193
|
+
* residuals described on `ConfigManager` (no logger exists yet at boot).
|
|
10014
10194
|
*/
|
|
10015
|
-
|
|
10016
|
-
/**
|
|
10017
|
-
* Addon implementing the `settings-store` capability. Loaded FIRST,
|
|
10018
|
-
* before any other addon is enabled — every other addon's enable
|
|
10019
|
-
* state, settings, and ordering lives in the store it provides.
|
|
10020
|
-
*
|
|
10021
|
-
* Default: '@camstack/addon-sqlite-storage' (the workspace's
|
|
10022
|
-
* default SQLite-backed store). Future alternatives could swap this
|
|
10023
|
-
* for postgres/redis without touching kernel code.
|
|
10024
|
-
*/
|
|
10025
|
-
settingsStoreAddon: zod.z.string().default("@camstack/addon-sqlite-storage"),
|
|
10026
|
-
/**
|
|
10027
|
-
* How the launcher provisions REQUIRED_PACKAGES into `data/addons/`
|
|
10028
|
-
* on startup.
|
|
10029
|
-
*
|
|
10030
|
-
* - `'npm'` — pull from the npm registry (production default).
|
|
10031
|
-
* - `'local'` — copy `packages/<X>/` from the workspace into
|
|
10032
|
-
* `data/addons/<X>/`. Use for the Electron-
|
|
10033
|
-
* packaged bundle and the e2e harness.
|
|
10034
|
-
* - `'symlink'` — point `data/addons/<X>` at the workspace source
|
|
10035
|
-
* dir via a directory symlink. Dev-only: a
|
|
10036
|
-
* workspace rebuild (`npm run build -w <X>`) is
|
|
10037
|
-
* immediately reflected at the next addon load,
|
|
10038
|
-
* no copy step. Falls back to `'local'` on
|
|
10039
|
-
* platforms that reject symlink creation (Windows
|
|
10040
|
-
* without developer-mode).
|
|
10041
|
-
*
|
|
10042
|
-
* The `CAMSTACK_INSTALL_SOURCE` env var, when set, wins over this
|
|
10043
|
-
* field. Leave undefined to keep the launcher's default behaviour
|
|
10044
|
-
* (npm when no workspace, local when a bundled addons dir is
|
|
10045
|
-
* present, …).
|
|
10046
|
-
*/
|
|
10047
|
-
installSource: zod.z.enum([
|
|
10048
|
-
"npm",
|
|
10049
|
-
"local",
|
|
10050
|
-
"symlink"
|
|
10051
|
-
]).optional(),
|
|
10052
|
-
/**
|
|
10053
|
-
* Override the kernel's hard-coded `AddonInstaller.REQUIRED_PACKAGES`.
|
|
10054
|
-
* Passing an array here REPLACES the default list entirely — useful
|
|
10055
|
-
* for stripped-down deployments (only the bundle of addons the
|
|
10056
|
-
* operator actually wants) and for forcing a different storage
|
|
10057
|
-
* backend at first boot.
|
|
10058
|
-
*
|
|
10059
|
-
* Leave undefined to use the kernel-shipped defaults.
|
|
10060
|
-
*/
|
|
10061
|
-
requiredAddons: zod.z.array(zod.z.string()).optional(),
|
|
10062
|
-
/**
|
|
10063
|
-
* Disable list applied AFTER the settings-store reports the enabled
|
|
10064
|
-
* set. Operator escape hatch — boot without a known-broken addon
|
|
10065
|
-
* without having to mutate the store.
|
|
10066
|
-
*/
|
|
10067
|
-
forceDisable: zod.z.array(zod.z.string()).default([])
|
|
10068
|
-
});
|
|
10069
|
-
/** Bootstrap config -- loaded from config.yaml ONLY at startup.
|
|
10070
|
-
* All other settings live in SQL (system_settings table). */
|
|
10071
|
-
var bootstrapSchema = zod.z.object({
|
|
10072
|
-
/** Server mode: 'hub' (full server) or 'agent' (worker node) */
|
|
10073
|
-
mode: zod.z.enum(["hub", "agent"]).default("hub"),
|
|
10074
|
-
server: serverSchema.default(serverSchema.parse({})),
|
|
10075
|
-
auth: authSchema.default(authSchema.parse({})),
|
|
10076
|
-
/** Hub connection config — only used when mode='agent' */
|
|
10077
|
-
hub: hubSchema.default(hubSchema.parse({})),
|
|
10078
|
-
/** Agent-specific config — only used when mode='agent' */
|
|
10079
|
-
agent: agentSchema.default(agentSchema.parse({})),
|
|
10080
|
-
/** TLS configuration */
|
|
10081
|
-
tls: tlsSchema.default(tlsSchema.parse({})),
|
|
10082
|
-
/**
|
|
10083
|
-
* Addon bootstrap directives — minimum addon set needed before the
|
|
10084
|
-
* kernel can reach the settings-store. See bootstrapAddonsSchema.
|
|
10085
|
-
* Spec: docs/superpowers/specs/2026-05-15-yaml-bootstrap-expansion.md
|
|
10086
|
-
*/
|
|
10087
|
-
bootstrap: bootstrapAddonsSchema.default(bootstrapAddonsSchema.parse({}))
|
|
10088
|
-
});
|
|
10089
|
-
//#endregion
|
|
10090
|
-
//#region src/kernel/config-manager.ts
|
|
10091
|
-
function isRecord(value) {
|
|
10195
|
+
function isRecord$1(value) {
|
|
10092
10196
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10093
10197
|
}
|
|
10198
|
+
/** Sections that live in config.yaml. Everything else goes to the settings-store. */
|
|
10199
|
+
var BOOTSTRAP_SECTIONS = new Set([
|
|
10200
|
+
"server",
|
|
10201
|
+
"auth",
|
|
10202
|
+
"mode"
|
|
10203
|
+
]);
|
|
10094
10204
|
/**
|
|
10095
10205
|
* Maps environment variable names to dot-notation config paths.
|
|
10096
10206
|
* Only bootstrap-level env vars -- runtime settings come from the
|
|
@@ -10104,17 +10214,360 @@ var ENV_VAR_MAP = {
|
|
|
10104
10214
|
CAMSTACK_ADMIN_USER: "auth.adminUsername",
|
|
10105
10215
|
CAMSTACK_ADMIN_PASS: "auth.adminPassword"
|
|
10106
10216
|
};
|
|
10107
|
-
|
|
10217
|
+
/** config.yaml, env overrides applied, parsed. */
|
|
10218
|
+
function loadBootstrapConfig(configPath) {
|
|
10219
|
+
const merged = applyEnvOverrides((0, _camstack_types_addon.asJsonObject)(loadYaml(configPath)) ?? {});
|
|
10220
|
+
const config = bootstrapSchema.parse(merged);
|
|
10221
|
+
warnDefaultCredentials(config);
|
|
10222
|
+
return config;
|
|
10223
|
+
}
|
|
10224
|
+
function writeBootstrapSection(configPath, section, data) {
|
|
10225
|
+
if (!BOOTSTRAP_SECTIONS.has(section)) throw new Error(`[ConfigManager] Section "${section}" is a runtime setting — use setSection() to persist via the settings-store, not update() which writes to config.yaml`);
|
|
10226
|
+
let raw = {};
|
|
10227
|
+
if (node_fs.existsSync(configPath)) raw = (0, _camstack_types_addon.asJsonObject)(load$1(node_fs.readFileSync(configPath, "utf-8"))) ?? {};
|
|
10228
|
+
const existing = (0, _camstack_types_addon.asJsonObject)(raw[section]) ?? {};
|
|
10229
|
+
raw[section] = {
|
|
10230
|
+
...existing,
|
|
10231
|
+
...data
|
|
10232
|
+
};
|
|
10233
|
+
const validation = bootstrapSchema.safeParse(raw);
|
|
10234
|
+
if (!validation.success) throw new Error(`[ConfigManager] Invalid config update for section "${section}": ${validation.error.message}`);
|
|
10235
|
+
const tmpPath = `${configPath}.tmp`;
|
|
10236
|
+
node_fs.writeFileSync(tmpPath, dump(raw, {
|
|
10237
|
+
lineWidth: 120,
|
|
10238
|
+
indent: 2,
|
|
10239
|
+
quotingType: "\""
|
|
10240
|
+
}), "utf-8");
|
|
10241
|
+
node_fs.renameSync(tmpPath, configPath);
|
|
10242
|
+
return validation.data;
|
|
10243
|
+
}
|
|
10244
|
+
/**
|
|
10245
|
+
* Returns a merged view of bootstrap config + runtime defaults for backward compat.
|
|
10246
|
+
*/
|
|
10247
|
+
function buildRawAppConfig(bootstrapConfig) {
|
|
10248
|
+
const features = {
|
|
10249
|
+
streaming: require_dist.RUNTIME_DEFAULTS["features.streaming"],
|
|
10250
|
+
notifications: require_dist.RUNTIME_DEFAULTS["features.notifications"],
|
|
10251
|
+
objectDetection: require_dist.RUNTIME_DEFAULTS["features.objectDetection"],
|
|
10252
|
+
remoteAccess: require_dist.RUNTIME_DEFAULTS["features.remoteAccess"],
|
|
10253
|
+
agentCluster: require_dist.RUNTIME_DEFAULTS["features.agentCluster"],
|
|
10254
|
+
smartHome: require_dist.RUNTIME_DEFAULTS["features.smartHome"],
|
|
10255
|
+
recordings: require_dist.RUNTIME_DEFAULTS["features.recordings"],
|
|
10256
|
+
backup: require_dist.RUNTIME_DEFAULTS["features.backup"],
|
|
10257
|
+
repl: require_dist.RUNTIME_DEFAULTS["features.repl"]
|
|
10258
|
+
};
|
|
10259
|
+
return {
|
|
10260
|
+
...bootstrapConfig,
|
|
10261
|
+
features,
|
|
10262
|
+
storage: {
|
|
10263
|
+
provider: require_dist.RUNTIME_DEFAULTS["storage.provider"],
|
|
10264
|
+
locations: { ...require_dist.RUNTIME_DEFAULTS["storage.locations"] }
|
|
10265
|
+
},
|
|
10266
|
+
logging: {
|
|
10267
|
+
level: require_dist.RUNTIME_DEFAULTS["logging.level"],
|
|
10268
|
+
retentionDays: require_dist.RUNTIME_DEFAULTS["logging.retentionDays"]
|
|
10269
|
+
},
|
|
10270
|
+
eventBus: { ringBufferSize: require_dist.RUNTIME_DEFAULTS["eventBus.ringBufferSize"] },
|
|
10271
|
+
retention: {
|
|
10272
|
+
detectionEventsDays: require_dist.RUNTIME_DEFAULTS["retention.detectionEventsDays"],
|
|
10273
|
+
audioLevelsDays: require_dist.RUNTIME_DEFAULTS["retention.audioLevelsDays"]
|
|
10274
|
+
},
|
|
10275
|
+
providers: [...require_dist.RUNTIME_DEFAULTS["providers"]]
|
|
10276
|
+
};
|
|
10277
|
+
}
|
|
10278
|
+
/**
|
|
10279
|
+
* Deep-set a value in a nested plain object using a dot-notation path.
|
|
10280
|
+
* Returns a new object (immutable).
|
|
10281
|
+
*/
|
|
10282
|
+
function setNested(obj, configPath, value) {
|
|
10283
|
+
const [head, ...rest] = configPath.split(".");
|
|
10284
|
+
if (!head) return obj;
|
|
10285
|
+
if (rest.length === 0) return {
|
|
10286
|
+
...obj,
|
|
10287
|
+
[head]: value
|
|
10288
|
+
};
|
|
10289
|
+
const existing = obj[head];
|
|
10290
|
+
const child = isRecord$1(existing) ? existing : {};
|
|
10291
|
+
return {
|
|
10292
|
+
...obj,
|
|
10293
|
+
[head]: setNested(child, rest.join("."), value)
|
|
10294
|
+
};
|
|
10295
|
+
}
|
|
10296
|
+
/**
|
|
10297
|
+
* Apply env var overrides onto the raw YAML object.
|
|
10298
|
+
* Only bootstrap-level env vars are applied.
|
|
10299
|
+
*/
|
|
10300
|
+
function applyEnvOverrides(raw) {
|
|
10301
|
+
let result = { ...raw };
|
|
10302
|
+
for (const [envKey, configPath] of Object.entries(ENV_VAR_MAP)) {
|
|
10303
|
+
const envValue = process.env[envKey];
|
|
10304
|
+
if (envValue === void 0 || envValue === "") continue;
|
|
10305
|
+
const coerced = configPath === "server.port" ? Number(envValue) : envValue;
|
|
10306
|
+
result = setNested(result, configPath, coerced);
|
|
10307
|
+
console.log(`[ConfigManager] Env override applied: ${envKey} → ${configPath}`);
|
|
10308
|
+
}
|
|
10309
|
+
return result;
|
|
10310
|
+
}
|
|
10311
|
+
function loadYaml(configPath) {
|
|
10312
|
+
if (!node_fs.existsSync(configPath)) {
|
|
10313
|
+
console.warn(`[ConfigManager] Config file not found at: ${configPath}\n → Using built-in defaults. Set CONFIG_PATH env var or create the file.\n → Example path from project root: ./server/backend/data/config.yaml`);
|
|
10314
|
+
return {};
|
|
10315
|
+
}
|
|
10316
|
+
const parsed = load$1(node_fs.readFileSync(configPath, "utf-8")) ?? {};
|
|
10317
|
+
console.log(`[ConfigManager] Loaded config from: ${configPath}`);
|
|
10318
|
+
return parsed;
|
|
10319
|
+
}
|
|
10320
|
+
function warnDefaultCredentials(config) {
|
|
10321
|
+
if (config.auth.adminPassword === "changeme") console.warn("[ConfigManager] Warning: Using default admin password \"changeme\". Set auth.adminPassword in your config.yaml or the CAMSTACK_ADMIN_PASS env var.");
|
|
10322
|
+
}
|
|
10323
|
+
//#endregion
|
|
10324
|
+
//#region src/kernel/store-settings-view.ts
|
|
10325
|
+
function createStoreSettingsView(addonId, cm) {
|
|
10326
|
+
const viewAddonConfig = () => !cm.hasSyncStore() ? (0, _camstack_types_addon.asJsonObject)(cm.getBootstrap(`addons.${addonId}`)) ?? {} : cm.getAddonConfig(addonId);
|
|
10327
|
+
const viewAddonDevice = (deviceId) => !cm.hasSyncStore() ? {} : cm.getAddonDevice(addonId, deviceId);
|
|
10328
|
+
return {
|
|
10329
|
+
async readAddonStore() {
|
|
10330
|
+
return viewAddonConfig();
|
|
10331
|
+
},
|
|
10332
|
+
async readAddonStoreKeyResult(key) {
|
|
10333
|
+
if (!cm.hasSyncStore()) return {
|
|
10334
|
+
kind: "unreadable",
|
|
10335
|
+
reason: "store-read-failed",
|
|
10336
|
+
error: "settings store not wired"
|
|
10337
|
+
};
|
|
10338
|
+
try {
|
|
10339
|
+
const all = cm.getAddonConfig(addonId);
|
|
10340
|
+
return key in all ? {
|
|
10341
|
+
kind: "present",
|
|
10342
|
+
value: all[key]
|
|
10343
|
+
} : { kind: "absent" };
|
|
10344
|
+
} catch (err) {
|
|
10345
|
+
return {
|
|
10346
|
+
kind: "unreadable",
|
|
10347
|
+
reason: "store-read-failed",
|
|
10348
|
+
error: err instanceof Error ? err.message : String(err)
|
|
10349
|
+
};
|
|
10350
|
+
}
|
|
10351
|
+
},
|
|
10352
|
+
async writeAddonStore(patch) {
|
|
10353
|
+
const existing = viewAddonConfig();
|
|
10354
|
+
cm.setAddonConfig(addonId, {
|
|
10355
|
+
...existing,
|
|
10356
|
+
...patch
|
|
10357
|
+
});
|
|
10358
|
+
},
|
|
10359
|
+
async readDeviceStore(deviceId) {
|
|
10360
|
+
return viewAddonDevice(String(deviceId));
|
|
10361
|
+
},
|
|
10362
|
+
async readDeviceStoreBatch(deviceIds) {
|
|
10363
|
+
const out = /* @__PURE__ */ new Map();
|
|
10364
|
+
for (const deviceId of deviceIds) try {
|
|
10365
|
+
out.set(deviceId, viewAddonDevice(String(deviceId)));
|
|
10366
|
+
} catch {
|
|
10367
|
+
out.set(deviceId, {});
|
|
10368
|
+
}
|
|
10369
|
+
return out;
|
|
10370
|
+
},
|
|
10371
|
+
async writeDeviceStore(deviceId, patch) {
|
|
10372
|
+
const key = String(deviceId);
|
|
10373
|
+
const existing = viewAddonDevice(key);
|
|
10374
|
+
cm.setAddonDevice(addonId, key, {
|
|
10375
|
+
...existing,
|
|
10376
|
+
...patch
|
|
10377
|
+
});
|
|
10378
|
+
},
|
|
10379
|
+
async clearDeviceStore(deviceId) {
|
|
10380
|
+
cm.clearAddonDevice(addonId, String(deviceId));
|
|
10381
|
+
},
|
|
10382
|
+
async readDeviceRuntimeState(deviceId) {
|
|
10383
|
+
return cm.getDeviceRuntimeState(String(deviceId));
|
|
10384
|
+
},
|
|
10385
|
+
async writeDeviceRuntimeState(deviceId, data) {
|
|
10386
|
+
cm.setDeviceRuntimeState(String(deviceId), data);
|
|
10387
|
+
},
|
|
10388
|
+
async clearDeviceRuntimeState(deviceId) {
|
|
10389
|
+
cm.clearDeviceRuntimeState(String(deviceId));
|
|
10390
|
+
},
|
|
10391
|
+
async getSection(section) {
|
|
10392
|
+
return cm.getSection(section);
|
|
10393
|
+
},
|
|
10394
|
+
async setSection(section, patch) {
|
|
10395
|
+
const existing = cm.getSection(section);
|
|
10396
|
+
cm.setSection(section, {
|
|
10397
|
+
...existing,
|
|
10398
|
+
...patch
|
|
10399
|
+
});
|
|
10400
|
+
}
|
|
10401
|
+
};
|
|
10402
|
+
}
|
|
10403
|
+
//#endregion
|
|
10404
|
+
//#region src/kernel/system-settings-policy.ts
|
|
10405
|
+
/**
|
|
10406
|
+
* Which `system-settings` keys the STORE may answer, and which the kernel
|
|
10407
|
+
* loads from the door into memory at boot — D675.
|
|
10408
|
+
*
|
|
10409
|
+
* ## The store never outranks env + config.yaml for a bootstrap section
|
|
10410
|
+
*
|
|
10411
|
+
* `system-settings` holds, among other things, `auth.jwtSecret`,
|
|
10412
|
+
* `auth.adminUsername` and `auth.adminPassword` — written there by
|
|
10413
|
+
* `local-auth` (and by older builds) next to `auth.tokenExpiry`. The live
|
|
10414
|
+
* container supplies the same three from env (`CAMSTACK_JWT_SECRET`,
|
|
10415
|
+
* `CAMSTACK_ADMIN_USER`, `CAMSTACK_ADMIN_PASS`). While the store layer was
|
|
10416
|
+
* skipped (every read since D231) env won by accident. Merging the store back
|
|
10417
|
+
* in "highest priority" would let a stale DB copy replace the JWT secret —
|
|
10418
|
+
* invalidating every token — and change the login password.
|
|
10419
|
+
*
|
|
10420
|
+
* So a bootstrap-owned section is answered from env + config.yaml ONLY, and
|
|
10421
|
+
* the store may add exactly the keys in {@link STORE_READABLE_BOOTSTRAP_KEYS}:
|
|
10422
|
+
* today `auth.tokenExpiry`, the one value the operator edits in the UI
|
|
10423
|
+
* (System → Authentication). Widening this list is a security decision, not a
|
|
10424
|
+
* convenience.
|
|
10425
|
+
*/
|
|
10426
|
+
/** Top-level sections whose authority is env + config.yaml (`bootstrapSchema`). */
|
|
10427
|
+
var BOOTSTRAP_OWNED_SECTIONS = new Set([
|
|
10428
|
+
"mode",
|
|
10429
|
+
"server",
|
|
10430
|
+
"auth",
|
|
10431
|
+
"hub",
|
|
10432
|
+
"agent",
|
|
10433
|
+
"tls",
|
|
10434
|
+
"bootstrap"
|
|
10435
|
+
]);
|
|
10436
|
+
/** The only keys under a bootstrap-owned section the store may answer. */
|
|
10437
|
+
var STORE_READABLE_BOOTSTRAP_KEYS = new Set(["auth.tokenExpiry"]);
|
|
10438
|
+
/** May the settings store answer `key`, or is it env + config.yaml only? */
|
|
10439
|
+
function storeMayAnswer(key) {
|
|
10440
|
+
const dot = key.indexOf(".");
|
|
10441
|
+
const section = dot === -1 ? key : key.slice(0, dot);
|
|
10442
|
+
if (!BOOTSTRAP_OWNED_SECTIONS.has(section)) return true;
|
|
10443
|
+
return STORE_READABLE_BOOTSTRAP_KEYS.has(key);
|
|
10444
|
+
}
|
|
10445
|
+
/**
|
|
10446
|
+
* Key prefixes loaded from the door into memory ONCE at boot, so the
|
|
10447
|
+
* synchronous readers that need them (the capability registry's preference
|
|
10448
|
+
* readers, `capabilities.getPreference`) answer the persisted value after a
|
|
10449
|
+
* reboot. Capability preferences are the only prefix: every other synchronous
|
|
10450
|
+
* caller of `ConfigManager.get` either reads a bootstrap key or runs before the
|
|
10451
|
+
* door exists.
|
|
10452
|
+
*/
|
|
10453
|
+
var PRELOADED_SYSTEM_PREFIXES = ["capabilities."];
|
|
10454
|
+
/**
|
|
10455
|
+
* Single keys loaded the same way. `auth.tokenExpiry` is read synchronously on
|
|
10456
|
+
* every mint by the hub's `AuthService` (`AuthManager.signToken`).
|
|
10457
|
+
*/
|
|
10458
|
+
var PRELOADED_SYSTEM_KEYS = ["auth.tokenExpiry"];
|
|
10459
|
+
/** Is `key` served from the boot-time mirror when the engine is isolated? */
|
|
10460
|
+
function isPreloadedSystemKey(key) {
|
|
10461
|
+
if (PRELOADED_SYSTEM_KEYS.includes(key)) return true;
|
|
10462
|
+
return PRELOADED_SYSTEM_PREFIXES.some((prefix) => key.startsWith(prefix));
|
|
10463
|
+
}
|
|
10464
|
+
/**
|
|
10465
|
+
* The stored keys of `section` the store may answer (`storeMayAnswer`,
|
|
10466
|
+
* D675): everything for an ordinary section, only the allow-listed keys for a
|
|
10467
|
+
* bootstrap-owned one.
|
|
10468
|
+
*/
|
|
10469
|
+
function filterStoreSection(section, stored) {
|
|
10470
|
+
const out = {};
|
|
10471
|
+
for (const [key, value] of Object.entries(stored)) if (storeMayAnswer(`${section}.${key}`)) out[key] = value;
|
|
10472
|
+
return out;
|
|
10473
|
+
}
|
|
10474
|
+
//#endregion
|
|
10475
|
+
//#region src/kernel/system-settings-mirror.ts
|
|
10476
|
+
function errorText$1(err) {
|
|
10477
|
+
return err instanceof Error ? err.message : String(err);
|
|
10478
|
+
}
|
|
10479
|
+
var SystemSettingsMirror = class {
|
|
10480
|
+
state = { kind: "pending" };
|
|
10481
|
+
/** The door the current read belongs to; a replaced door owns the mirror. */
|
|
10482
|
+
door = null;
|
|
10483
|
+
preload = null;
|
|
10484
|
+
/**
|
|
10485
|
+
* Writes to preloaded keys made through THIS process. They win over the boot
|
|
10486
|
+
* read (they are newer) and survive a failed one, so an operator's Save is
|
|
10487
|
+
* reported back even when the boot read was lost.
|
|
10488
|
+
*/
|
|
10489
|
+
writes = /* @__PURE__ */ new Map();
|
|
10490
|
+
/** Start the ONE read for `door`. Called by `ConfigManager.setSettingsDoor`. */
|
|
10491
|
+
load(door) {
|
|
10492
|
+
this.door = door;
|
|
10493
|
+
this.state = { kind: "pending" };
|
|
10494
|
+
this.preload = this.read(door);
|
|
10495
|
+
}
|
|
10496
|
+
/** The in-flight / settled boot read, or `null` before any door. */
|
|
10497
|
+
whenLoaded() {
|
|
10498
|
+
return this.preload;
|
|
10499
|
+
}
|
|
10500
|
+
/**
|
|
10501
|
+
* A preloaded key's value: `undefined` when the store holds none, or a THROW
|
|
10502
|
+
* (`preload-pending` / `preload-failed`) when it is not known.
|
|
10503
|
+
*/
|
|
10504
|
+
get(key) {
|
|
10505
|
+
if (this.writes.has(key)) return this.writes.get(key);
|
|
10506
|
+
const state = this.state;
|
|
10507
|
+
switch (state.kind) {
|
|
10508
|
+
case "loaded": return state.values.get(key);
|
|
10509
|
+
case "pending": throw new require_builtins_local_auth_local_auth_addon.SettingsReadUnavailableError("ConfigManager.get", "preload-pending", `"${key}" is read from the boot-time mirror, whose one read has not settled yet`);
|
|
10510
|
+
case "failed": throw new require_builtins_local_auth_local_auth_addon.SettingsReadUnavailableError("ConfigManager.get", "preload-failed", `"${key}" is unknown: the boot-time read of the settings store failed (${state.error})`);
|
|
10511
|
+
}
|
|
10512
|
+
}
|
|
10513
|
+
/** Record a write to a preloaded key so the mirror reports it. */
|
|
10514
|
+
noteWrite(key, value) {
|
|
10515
|
+
if (isPreloadedSystemKey(key)) this.writes.set(key, value);
|
|
10516
|
+
}
|
|
10517
|
+
/** Every row under the preloaded prefixes plus each preloaded key. Never rejects. */
|
|
10518
|
+
async read(door) {
|
|
10519
|
+
try {
|
|
10520
|
+
const values = /* @__PURE__ */ new Map();
|
|
10521
|
+
for (const prefix of PRELOADED_SYSTEM_PREFIXES) {
|
|
10522
|
+
const rows = await loadSystemSettingsPrefix(door, prefix);
|
|
10523
|
+
for (const [suffix, value] of Object.entries(rows)) values.set(`${prefix}${suffix}`, value);
|
|
10524
|
+
}
|
|
10525
|
+
for (const key of PRELOADED_SYSTEM_KEYS) {
|
|
10526
|
+
const value = await door.get({
|
|
10527
|
+
collection: "system-settings",
|
|
10528
|
+
key
|
|
10529
|
+
});
|
|
10530
|
+
if (value !== void 0) values.set(key, value);
|
|
10531
|
+
}
|
|
10532
|
+
if (this.door === door) this.state = {
|
|
10533
|
+
kind: "loaded",
|
|
10534
|
+
values
|
|
10535
|
+
};
|
|
10536
|
+
return {
|
|
10537
|
+
kind: "loaded",
|
|
10538
|
+
keyCount: values.size
|
|
10539
|
+
};
|
|
10540
|
+
} catch (err) {
|
|
10541
|
+
const error = errorText$1(err);
|
|
10542
|
+
if (this.door === door) this.state = {
|
|
10543
|
+
kind: "failed",
|
|
10544
|
+
error
|
|
10545
|
+
};
|
|
10546
|
+
return {
|
|
10547
|
+
kind: "failed",
|
|
10548
|
+
error
|
|
10549
|
+
};
|
|
10550
|
+
}
|
|
10551
|
+
}
|
|
10552
|
+
};
|
|
10553
|
+
//#endregion
|
|
10554
|
+
//#region src/kernel/config-manager.ts
|
|
10555
|
+
function isRecord(value) {
|
|
10556
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10557
|
+
}
|
|
10558
|
+
function errorText(err) {
|
|
10559
|
+
return err instanceof Error ? err.message : String(err);
|
|
10560
|
+
}
|
|
10561
|
+
var ConfigManager = class {
|
|
10108
10562
|
configPath;
|
|
10109
10563
|
bootstrapConfig;
|
|
10110
10564
|
settingsStore = null;
|
|
10111
10565
|
settingsDoor = null;
|
|
10566
|
+
/** Boot-time mirror of the preloaded `system-settings` keys (door mode, D675). */
|
|
10567
|
+
systemMirror = new SystemSettingsMirror();
|
|
10112
10568
|
constructor(configPath) {
|
|
10113
10569
|
this.configPath = configPath;
|
|
10114
|
-
|
|
10115
|
-
const merged = this.applyEnvOverrides((0, _camstack_types_addon.asJsonObject)(rawYaml) ?? {});
|
|
10116
|
-
this.bootstrapConfig = bootstrapSchema.parse(merged);
|
|
10117
|
-
this.warnDefaultCredentials();
|
|
10570
|
+
this.bootstrapConfig = loadBootstrapConfig(configPath);
|
|
10118
10571
|
}
|
|
10119
10572
|
/**
|
|
10120
10573
|
* Wire the settings-store backend. Called once, after the `settings-store`
|
|
@@ -10131,6 +10584,25 @@ var ConfigManager = class ConfigManager {
|
|
|
10131
10584
|
*/
|
|
10132
10585
|
setSettingsDoor(door) {
|
|
10133
10586
|
this.settingsDoor = door;
|
|
10587
|
+
this.systemMirror.load(door);
|
|
10588
|
+
}
|
|
10589
|
+
/**
|
|
10590
|
+
* Resolves once the boot read of the preloaded `system-settings` keys has
|
|
10591
|
+
* settled — the hook the addon registry uses to log it and to replay the
|
|
10592
|
+
* capability preferences onto providers that registered before it (D675).
|
|
10593
|
+
* `null` when no door has been wired (a sync store needs no preload).
|
|
10594
|
+
*/
|
|
10595
|
+
whenSystemSettingsPreloaded() {
|
|
10596
|
+
return this.systemMirror.whenLoaded();
|
|
10597
|
+
}
|
|
10598
|
+
backendMode() {
|
|
10599
|
+
if (this.settingsStore !== null) return "store";
|
|
10600
|
+
if (this.settingsDoor !== null) return "door";
|
|
10601
|
+
return "unwired";
|
|
10602
|
+
}
|
|
10603
|
+
/** The refusal every sync addon-scoped reader raises without a sync store. */
|
|
10604
|
+
refuseSyncRead(reader, subject) {
|
|
10605
|
+
return this.backendMode() === "door" ? new require_builtins_local_auth_local_auth_addon.SettingsReadUnavailableError(`ConfigManager.${reader}`, "door-only", `${subject}: the settings engine is isolated and has no synchronous handle — read it through the settings view (createSettingsView / readAddonStoreResult)`) : new require_builtins_local_auth_local_auth_addon.SettingsReadUnavailableError(`ConfigManager.${reader}`, "not-wired", `${subject}: no settings backend is wired yet`);
|
|
10134
10606
|
}
|
|
10135
10607
|
get(configPath) {
|
|
10136
10608
|
return this.resolveConfigValue(configPath);
|
|
@@ -10138,35 +10610,52 @@ var ConfigManager = class ConfigManager {
|
|
|
10138
10610
|
resolveConfigValue(configPath) {
|
|
10139
10611
|
const bootstrapValue = this.getFromBootstrap(configPath);
|
|
10140
10612
|
if (bootstrapValue !== void 0) return bootstrapValue;
|
|
10141
|
-
|
|
10142
|
-
|
|
10143
|
-
if (storeValue !== void 0) return storeValue;
|
|
10144
|
-
const nested = this.getNestedFromSystemSettings(configPath);
|
|
10145
|
-
if (nested !== null) return nested;
|
|
10146
|
-
}
|
|
10613
|
+
const storeValue = this.readStoreLayer(configPath);
|
|
10614
|
+
if (storeValue !== void 0) return storeValue;
|
|
10147
10615
|
if (configPath in require_dist.RUNTIME_DEFAULTS) return require_dist.RUNTIME_DEFAULTS[configPath];
|
|
10148
10616
|
return this.getFromRuntimeDefaults(configPath) ?? void 0;
|
|
10149
10617
|
}
|
|
10150
10618
|
/**
|
|
10151
|
-
*
|
|
10152
|
-
*
|
|
10619
|
+
* The settings-store layer of {@link get}: the stored value, `undefined`
|
|
10620
|
+
* when the store holds nothing (or may not answer this key), or a THROW
|
|
10621
|
+
* when the store cannot be asked.
|
|
10622
|
+
*/
|
|
10623
|
+
readStoreLayer(configPath) {
|
|
10624
|
+
if (!storeMayAnswer(configPath)) return void 0;
|
|
10625
|
+
switch (this.backendMode()) {
|
|
10626
|
+
case "store": {
|
|
10627
|
+
const store = this.settingsStore;
|
|
10628
|
+
if (store === null) return void 0;
|
|
10629
|
+
const storeValue = store.getSystem(configPath);
|
|
10630
|
+
if (storeValue !== void 0) return storeValue;
|
|
10631
|
+
return this.getNestedFromSystemSettings(configPath) ?? void 0;
|
|
10632
|
+
}
|
|
10633
|
+
case "door":
|
|
10634
|
+
if (isPreloadedSystemKey(configPath)) return this.systemMirror.get(configPath);
|
|
10635
|
+
throw new require_builtins_local_auth_local_auth_addon.SettingsReadUnavailableError("ConfigManager.get", "door-only", `"${configPath}" may be stored, and the isolated engine cannot be read synchronously — only ${[...PRELOADED_SYSTEM_PREFIXES, ...PRELOADED_SYSTEM_KEYS].join(", ")} are mirrored at boot`);
|
|
10636
|
+
case "unwired": return;
|
|
10637
|
+
}
|
|
10638
|
+
}
|
|
10639
|
+
/**
|
|
10640
|
+
* Write a value to the settings-store. Resolves only once the write LANDED:
|
|
10641
|
+
* on the isolated engine it awaits the door, and a failed write REJECTS and
|
|
10642
|
+
* is not reported back by the mirror (D675 — it used to be fire-and-forget,
|
|
10643
|
+
* so a lost write read as saved for the life of the process). Rejects when
|
|
10644
|
+
* no store is wired.
|
|
10153
10645
|
*/
|
|
10154
|
-
set(key, value) {
|
|
10646
|
+
async set(key, value) {
|
|
10155
10647
|
if (this.settingsStore !== null) {
|
|
10156
10648
|
this.settingsStore.setSystem(key, value);
|
|
10157
10649
|
return;
|
|
10158
10650
|
}
|
|
10159
|
-
|
|
10160
|
-
|
|
10161
|
-
|
|
10162
|
-
|
|
10163
|
-
|
|
10164
|
-
|
|
10165
|
-
|
|
10166
|
-
|
|
10167
|
-
return;
|
|
10168
|
-
}
|
|
10169
|
-
throw new Error("[ConfigManager] SettingsStore not initialized -- call setSettingsStore() first");
|
|
10651
|
+
const door = this.settingsDoor;
|
|
10652
|
+
if (door === null) throw new require_builtins_local_auth_local_auth_addon.SettingsReadUnavailableError("ConfigManager.set", "not-wired", `"${key}" cannot be written: no settings backend is wired yet`);
|
|
10653
|
+
await door.set({
|
|
10654
|
+
collection: "system-settings",
|
|
10655
|
+
key,
|
|
10656
|
+
value
|
|
10657
|
+
});
|
|
10658
|
+
this.systemMirror.noteWrite(key, value);
|
|
10170
10659
|
}
|
|
10171
10660
|
/**
|
|
10172
10661
|
* Bulk-read all keys that belong to a logical section.
|
|
@@ -10182,17 +10671,40 @@ var ConfigManager = class ConfigManager {
|
|
|
10182
10671
|
* shadowing the rest of the section that only lives in config.yaml.
|
|
10183
10672
|
* The kernel is agnostic about the store backend — any `ISettingsStore`
|
|
10184
10673
|
* implementation (sqlite, redis, in-memory, …) behaves identically here.
|
|
10674
|
+
*
|
|
10675
|
+
* D675: a bootstrap-owned section takes ONLY allow-listed keys from the
|
|
10676
|
+
* store (a stored `auth.jwtSecret` never replaces env), and with only the
|
|
10677
|
+
* door wired this sync read THROWS — the settings view's `getSection` reads
|
|
10678
|
+
* the door.
|
|
10185
10679
|
*/
|
|
10186
10680
|
getSection(section) {
|
|
10681
|
+
const mode = this.backendMode();
|
|
10682
|
+
if (mode === "door") throw new require_builtins_local_auth_local_auth_addon.SettingsReadUnavailableError("ConfigManager.getSection", "door-only", `section "${section}": the settings engine is isolated — read it through the settings view (ctx.settings.getSection)`);
|
|
10683
|
+
const merged = this.sectionBase(section);
|
|
10684
|
+
if (mode === "store") {
|
|
10685
|
+
const nested = this.getNestedFromSystemSettings(section);
|
|
10686
|
+
if (nested) Object.assign(merged, filterStoreSection(section, nested));
|
|
10687
|
+
}
|
|
10688
|
+
return merged;
|
|
10689
|
+
}
|
|
10690
|
+
/** Runtime defaults, then config.yaml + env — the layers under the store. */
|
|
10691
|
+
sectionBase(section) {
|
|
10187
10692
|
const merged = {};
|
|
10188
10693
|
const defaults = (0, _camstack_types_addon.asJsonObject)(this.getFromRuntimeDefaults(section));
|
|
10189
10694
|
if (defaults) Object.assign(merged, defaults);
|
|
10190
10695
|
const bootstrapSection = (0, _camstack_types_addon.asJsonObject)({ ...this.bootstrapConfig }[section]);
|
|
10191
10696
|
if (bootstrapSection) Object.assign(merged, bootstrapSection);
|
|
10192
|
-
|
|
10193
|
-
|
|
10194
|
-
|
|
10195
|
-
|
|
10697
|
+
return merged;
|
|
10698
|
+
}
|
|
10699
|
+
/**
|
|
10700
|
+
* {@link getSection} through the async door — what the door-backed settings
|
|
10701
|
+
* view answers (D675). Same layers, same allow-list; a failed door read
|
|
10702
|
+
* THROWS rather than handing back the defaults as if nothing were stored.
|
|
10703
|
+
*/
|
|
10704
|
+
async readSectionThroughDoor(door, section) {
|
|
10705
|
+
const merged = this.sectionBase(section);
|
|
10706
|
+
const stored = await loadSystemSettingsPrefix(door, `${section}.`, (key) => storeMayAnswer(`${section}.${key}`));
|
|
10707
|
+
Object.assign(merged, filterStoreSection(section, stored));
|
|
10196
10708
|
return merged;
|
|
10197
10709
|
}
|
|
10198
10710
|
/**
|
|
@@ -10203,44 +10715,58 @@ var ConfigManager = class ConfigManager {
|
|
|
10203
10715
|
if (this.settingsStore === null) throw new Error("[ConfigManager] SettingsStore not initialized -- call setSettingsStore() first");
|
|
10204
10716
|
for (const [key, value] of Object.entries(data)) this.settingsStore.setSystem(`${section}.${key}`, value);
|
|
10205
10717
|
}
|
|
10206
|
-
/**
|
|
10718
|
+
/**
|
|
10719
|
+
* Read all config for an addon — SYNC store only. THROWS without one: its
|
|
10720
|
+
* old bootstrap fallback answered `{}` for every addon on every
|
|
10721
|
+
* isolated-engine hub (D675). Async three-way: {@link readAddonGlobalStrict}.
|
|
10722
|
+
*/
|
|
10207
10723
|
getAddonConfig(addonId) {
|
|
10208
10724
|
if (this.settingsStore !== null) return this.settingsStore.getAllAddon(addonId);
|
|
10209
|
-
|
|
10725
|
+
throw this.refuseSyncRead("getAddonConfig", `addon "${addonId}"`);
|
|
10726
|
+
}
|
|
10727
|
+
/**
|
|
10728
|
+
* One addon's global store, three-way (D675): the record, `{}` when the
|
|
10729
|
+
* store ANSWERED with nothing, or a THROW when it could not be asked.
|
|
10730
|
+
* Behind `addonSettingsRaw.getGlobal` and `$hub.getAddonConfig`.
|
|
10731
|
+
*/
|
|
10732
|
+
async readAddonGlobalStrict(addonId) {
|
|
10733
|
+
const read = await this.createSettingsView(addonId).readAddonStoreResult();
|
|
10734
|
+
switch (read.kind) {
|
|
10735
|
+
case "value": return read.value;
|
|
10736
|
+
case "missing": return {};
|
|
10737
|
+
case "failed": throw new require_builtins_local_auth_local_auth_addon.SettingsReadUnavailableError("ConfigManager.readAddonGlobalStrict", this.backendMode() === "unwired" ? "not-wired" : "read-failed", `addon "${addonId}": ${read.error}`);
|
|
10738
|
+
}
|
|
10739
|
+
}
|
|
10740
|
+
/** One addon's per-device overrides, three-way like {@link readAddonGlobalStrict}. */
|
|
10741
|
+
async readAddonDeviceStrict(addonId, deviceId) {
|
|
10742
|
+
const door = this.settingsDoor;
|
|
10743
|
+
switch (this.backendMode()) {
|
|
10744
|
+
case "store": return this.getAddonDevice(addonId, String(deviceId));
|
|
10745
|
+
case "door":
|
|
10746
|
+
if (door === null) break;
|
|
10747
|
+
try {
|
|
10748
|
+
return await this.createDoorBackedSettingsView(addonId, door).readDeviceStore(deviceId);
|
|
10749
|
+
} catch (err) {
|
|
10750
|
+
throw new require_builtins_local_auth_local_auth_addon.SettingsReadUnavailableError("ConfigManager.readAddonDeviceStrict", "read-failed", `addon "${addonId}" device ${deviceId}: ${errorText(err)}`);
|
|
10751
|
+
}
|
|
10752
|
+
case "unwired": break;
|
|
10753
|
+
}
|
|
10754
|
+
throw this.refuseSyncRead("readAddonDeviceStrict", `addon "${addonId}" device ${deviceId}`);
|
|
10210
10755
|
}
|
|
10211
10756
|
/** Write (bulk-replace) config for an addon to addon_settings. */
|
|
10212
10757
|
setAddonConfig(addonId, config) {
|
|
10213
10758
|
if (this.settingsStore === null) throw new Error("[ConfigManager] SettingsStore not initialized -- call setSettingsStore() first");
|
|
10214
10759
|
this.settingsStore.setAllAddon(addonId, config);
|
|
10215
10760
|
}
|
|
10216
|
-
/** Read all config for a provider from provider_settings. */
|
|
10217
|
-
getProviderConfig(providerId) {
|
|
10218
|
-
if (this.settingsStore !== null) return this.settingsStore.getAllProvider(providerId);
|
|
10219
|
-
return {};
|
|
10220
|
-
}
|
|
10221
|
-
/** Write (upsert) a single key for a provider to provider_settings. */
|
|
10222
|
-
setProviderConfig(providerId, key, value) {
|
|
10223
|
-
if (this.settingsStore === null) throw new Error("[ConfigManager] SettingsStore not initialized -- call setSettingsStore() first");
|
|
10224
|
-
this.settingsStore.setProvider(providerId, key, value);
|
|
10225
|
-
}
|
|
10226
|
-
/** Read all config for a device from device_settings. */
|
|
10227
|
-
getDeviceConfig(deviceId) {
|
|
10228
|
-
if (this.settingsStore !== null) return this.settingsStore.getAllDevice(deviceId);
|
|
10229
|
-
return {};
|
|
10230
|
-
}
|
|
10231
|
-
/** Write (upsert) a single key for a device to device_settings. */
|
|
10232
|
-
setDeviceConfig(deviceId, key, value) {
|
|
10233
|
-
if (this.settingsStore === null) throw new Error("[ConfigManager] SettingsStore not initialized -- call setSettingsStore() first");
|
|
10234
|
-
this.settingsStore.setDevice(deviceId, key, value);
|
|
10235
|
-
}
|
|
10236
10761
|
/**
|
|
10237
10762
|
* Read per-device overrides for a specific addon. Returns a flat record
|
|
10238
10763
|
* of field → value. Missing keys fall back to the addon-global value via
|
|
10239
10764
|
* `getAddonConfig(addonId)` — that composition is the resolver service's
|
|
10240
10765
|
* responsibility, not ConfigManager's.
|
|
10766
|
+
* SYNC store only — THROWS without one (D675); async: {@link readAddonDeviceStrict}.
|
|
10241
10767
|
*/
|
|
10242
10768
|
getAddonDevice(addonId, deviceId) {
|
|
10243
|
-
if (this.settingsStore === null)
|
|
10769
|
+
if (this.settingsStore === null) throw this.refuseSyncRead("getAddonDevice", `addon "${addonId}" device "${deviceId}"`);
|
|
10244
10770
|
return this.settingsStore.getAddonDevice(addonId, deviceId);
|
|
10245
10771
|
}
|
|
10246
10772
|
/**
|
|
@@ -10261,12 +10787,11 @@ var ConfigManager = class ConfigManager {
|
|
|
10261
10787
|
this.settingsStore.clearAddonDevice(addonId, deviceId);
|
|
10262
10788
|
}
|
|
10263
10789
|
/**
|
|
10264
|
-
* The device's persisted runtime-state blob.
|
|
10265
|
-
*
|
|
10266
|
-
* which is what every caller already handles.
|
|
10790
|
+
* The device's persisted runtime-state blob. THROWS before the store is
|
|
10791
|
+
* wired: a store not asked is not an empty row (D677, D675).
|
|
10267
10792
|
*/
|
|
10268
10793
|
getDeviceRuntimeState(deviceId) {
|
|
10269
|
-
if (this.settingsStore === null)
|
|
10794
|
+
if (this.settingsStore === null) throw this.refuseSyncRead("getDeviceRuntimeState", `runtime state of device ${deviceId}`);
|
|
10270
10795
|
return this.settingsStore.getDeviceRuntimeState(deviceId);
|
|
10271
10796
|
}
|
|
10272
10797
|
/**
|
|
@@ -10334,6 +10859,7 @@ var ConfigManager = class ConfigManager {
|
|
|
10334
10859
|
};
|
|
10335
10860
|
}
|
|
10336
10861
|
},
|
|
10862
|
+
readAddonStoreKeyResult: (key) => active().readAddonStoreKeyResult(key),
|
|
10337
10863
|
writeAddonStore: (patch) => active().writeAddonStore(patch),
|
|
10338
10864
|
readDeviceStore: (deviceId) => active().readDeviceStore(deviceId),
|
|
10339
10865
|
readDeviceStoreBatch: (deviceIds) => active().readDeviceStoreBatch(deviceIds),
|
|
@@ -10349,81 +10875,39 @@ var ConfigManager = class ConfigManager {
|
|
|
10349
10875
|
/** The async-door backend of {@link createSettingsView}. */
|
|
10350
10876
|
createDoorBackedSettingsView(addonId, door) {
|
|
10351
10877
|
return createDoorSettingsView(addonId, door, {
|
|
10352
|
-
getSection: (section) => this.
|
|
10878
|
+
getSection: (section) => this.readSectionThroughDoor(door, section),
|
|
10353
10879
|
setSection: async (section, patch) => {
|
|
10354
|
-
for (const [key, value] of Object.entries(patch))
|
|
10355
|
-
|
|
10356
|
-
|
|
10357
|
-
|
|
10358
|
-
|
|
10880
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
10881
|
+
const fullKey = `${section}.${key}`;
|
|
10882
|
+
await door.set({
|
|
10883
|
+
collection: "system-settings",
|
|
10884
|
+
key: fullKey,
|
|
10885
|
+
value
|
|
10886
|
+
});
|
|
10887
|
+
this.systemMirror.noteWrite(fullKey, value);
|
|
10888
|
+
}
|
|
10359
10889
|
}
|
|
10360
10890
|
});
|
|
10361
10891
|
}
|
|
10362
10892
|
/** The sync-`ISettingsStore` backend of {@link createSettingsView}. */
|
|
10363
10893
|
createStoreSettingsView(addonId) {
|
|
10364
|
-
|
|
10365
|
-
|
|
10366
|
-
|
|
10367
|
-
|
|
10368
|
-
|
|
10369
|
-
async writeAddonStore(patch) {
|
|
10370
|
-
const existing = cm.getAddonConfig(addonId);
|
|
10371
|
-
cm.setAddonConfig(addonId, {
|
|
10372
|
-
...existing,
|
|
10373
|
-
...patch
|
|
10374
|
-
});
|
|
10375
|
-
},
|
|
10376
|
-
async readDeviceStore(deviceId) {
|
|
10377
|
-
return cm.getAddonDevice(addonId, String(deviceId));
|
|
10378
|
-
},
|
|
10379
|
-
async readDeviceStoreBatch(deviceIds) {
|
|
10380
|
-
const out = /* @__PURE__ */ new Map();
|
|
10381
|
-
for (const deviceId of deviceIds) try {
|
|
10382
|
-
out.set(deviceId, cm.getAddonDevice(addonId, String(deviceId)));
|
|
10383
|
-
} catch {
|
|
10384
|
-
out.set(deviceId, {});
|
|
10385
|
-
}
|
|
10386
|
-
return out;
|
|
10387
|
-
},
|
|
10388
|
-
async writeDeviceStore(deviceId, patch) {
|
|
10389
|
-
const key = String(deviceId);
|
|
10390
|
-
const existing = cm.getAddonDevice(addonId, key);
|
|
10391
|
-
cm.setAddonDevice(addonId, key, {
|
|
10392
|
-
...existing,
|
|
10393
|
-
...patch
|
|
10394
|
-
});
|
|
10395
|
-
},
|
|
10396
|
-
async clearDeviceStore(deviceId) {
|
|
10397
|
-
cm.clearAddonDevice(addonId, String(deviceId));
|
|
10398
|
-
},
|
|
10399
|
-
async readDeviceRuntimeState(deviceId) {
|
|
10400
|
-
return cm.getDeviceRuntimeState(String(deviceId));
|
|
10401
|
-
},
|
|
10402
|
-
async writeDeviceRuntimeState(deviceId, data) {
|
|
10403
|
-
cm.setDeviceRuntimeState(String(deviceId), data);
|
|
10404
|
-
},
|
|
10405
|
-
async clearDeviceRuntimeState(deviceId) {
|
|
10406
|
-
cm.clearDeviceRuntimeState(String(deviceId));
|
|
10407
|
-
},
|
|
10408
|
-
async getSection(section) {
|
|
10409
|
-
return cm.getSection(section);
|
|
10410
|
-
},
|
|
10411
|
-
async setSection(section, patch) {
|
|
10412
|
-
const existing = cm.getSection(section);
|
|
10413
|
-
cm.setSection(section, {
|
|
10414
|
-
...existing,
|
|
10415
|
-
...patch
|
|
10416
|
-
});
|
|
10417
|
-
}
|
|
10418
|
-
};
|
|
10894
|
+
return createStoreSettingsView(addonId, this);
|
|
10895
|
+
}
|
|
10896
|
+
/** Is a SYNC `ISettingsStore` wired? (The store-backed view's D294-window test.) */
|
|
10897
|
+
hasSyncStore() {
|
|
10898
|
+
return this.settingsStore !== null;
|
|
10419
10899
|
}
|
|
10420
10900
|
getBootstrap(configPath) {
|
|
10421
10901
|
return this.getFromBootstrap(configPath);
|
|
10422
10902
|
}
|
|
10423
|
-
/**
|
|
10903
|
+
/**
|
|
10904
|
+
* Features — the sync store when wired, else RUNTIME_DEFAULTS DELIBERATELY
|
|
10905
|
+
* (not `get()`, which refuses in door mode, D675): nothing writes
|
|
10906
|
+
* `features.*` beyond sqlite-settings' first-boot seed of these defaults.
|
|
10907
|
+
*/
|
|
10424
10908
|
get features() {
|
|
10425
10909
|
const g = (key) => {
|
|
10426
|
-
const storeValue = this.get(`features.${key}`);
|
|
10910
|
+
const storeValue = this.settingsStore !== null ? this.get(`features.${key}`) : void 0;
|
|
10427
10911
|
if (typeof storeValue === "boolean") return storeValue;
|
|
10428
10912
|
return require_dist.RUNTIME_DEFAULTS[`features.${key}`];
|
|
10429
10913
|
};
|
|
@@ -10439,115 +10923,16 @@ var ConfigManager = class ConfigManager {
|
|
|
10439
10923
|
repl: g("repl")
|
|
10440
10924
|
};
|
|
10441
10925
|
}
|
|
10442
|
-
/**
|
|
10443
|
-
* Returns a merged view of bootstrap config + runtime defaults for backward compat.
|
|
10444
|
-
*/
|
|
10926
|
+
/** Bootstrap config + runtime defaults, for backward compat. */
|
|
10445
10927
|
get raw() {
|
|
10446
|
-
|
|
10447
|
-
streaming: require_dist.RUNTIME_DEFAULTS["features.streaming"],
|
|
10448
|
-
notifications: require_dist.RUNTIME_DEFAULTS["features.notifications"],
|
|
10449
|
-
objectDetection: require_dist.RUNTIME_DEFAULTS["features.objectDetection"],
|
|
10450
|
-
remoteAccess: require_dist.RUNTIME_DEFAULTS["features.remoteAccess"],
|
|
10451
|
-
agentCluster: require_dist.RUNTIME_DEFAULTS["features.agentCluster"],
|
|
10452
|
-
smartHome: require_dist.RUNTIME_DEFAULTS["features.smartHome"],
|
|
10453
|
-
recordings: require_dist.RUNTIME_DEFAULTS["features.recordings"],
|
|
10454
|
-
backup: require_dist.RUNTIME_DEFAULTS["features.backup"],
|
|
10455
|
-
repl: require_dist.RUNTIME_DEFAULTS["features.repl"]
|
|
10456
|
-
};
|
|
10457
|
-
return {
|
|
10458
|
-
...this.bootstrapConfig,
|
|
10459
|
-
features,
|
|
10460
|
-
storage: {
|
|
10461
|
-
provider: require_dist.RUNTIME_DEFAULTS["storage.provider"],
|
|
10462
|
-
locations: { ...require_dist.RUNTIME_DEFAULTS["storage.locations"] }
|
|
10463
|
-
},
|
|
10464
|
-
logging: {
|
|
10465
|
-
level: require_dist.RUNTIME_DEFAULTS["logging.level"],
|
|
10466
|
-
retentionDays: require_dist.RUNTIME_DEFAULTS["logging.retentionDays"]
|
|
10467
|
-
},
|
|
10468
|
-
eventBus: { ringBufferSize: require_dist.RUNTIME_DEFAULTS["eventBus.ringBufferSize"] },
|
|
10469
|
-
retention: {
|
|
10470
|
-
detectionEventsDays: require_dist.RUNTIME_DEFAULTS["retention.detectionEventsDays"],
|
|
10471
|
-
audioLevelsDays: require_dist.RUNTIME_DEFAULTS["retention.audioLevelsDays"]
|
|
10472
|
-
},
|
|
10473
|
-
providers: [...require_dist.RUNTIME_DEFAULTS["providers"]]
|
|
10474
|
-
};
|
|
10928
|
+
return buildRawAppConfig(this.bootstrapConfig);
|
|
10475
10929
|
}
|
|
10476
|
-
/** Sections that live in config.yaml. Everything else goes to the settings-store. */
|
|
10477
|
-
static BOOTSTRAP_SECTIONS = new Set([
|
|
10478
|
-
"server",
|
|
10479
|
-
"auth",
|
|
10480
|
-
"mode"
|
|
10481
|
-
]);
|
|
10482
10930
|
/**
|
|
10483
|
-
* Atomically update one top-level section
|
|
10484
|
-
*
|
|
10485
|
-
* Runtime settings must use setSection() which writes to the settings-store.
|
|
10931
|
+
* Atomically update one top-level bootstrap section (server, auth, mode) of
|
|
10932
|
+
* config.yaml and sync in-memory. Runtime settings use setSection().
|
|
10486
10933
|
*/
|
|
10487
10934
|
update(section, data) {
|
|
10488
|
-
|
|
10489
|
-
let raw = {};
|
|
10490
|
-
if (node_fs.existsSync(this.configPath)) raw = (0, _camstack_types_addon.asJsonObject)(load$1(node_fs.readFileSync(this.configPath, "utf-8"))) ?? {};
|
|
10491
|
-
const existing = (0, _camstack_types_addon.asJsonObject)(raw[section]) ?? {};
|
|
10492
|
-
raw[section] = {
|
|
10493
|
-
...existing,
|
|
10494
|
-
...data
|
|
10495
|
-
};
|
|
10496
|
-
const validation = bootstrapSchema.safeParse(raw);
|
|
10497
|
-
if (!validation.success) throw new Error(`[ConfigManager] Invalid config update for section "${section}": ${validation.error.message}`);
|
|
10498
|
-
const tmpPath = `${this.configPath}.tmp`;
|
|
10499
|
-
node_fs.writeFileSync(tmpPath, dump(raw, {
|
|
10500
|
-
lineWidth: 120,
|
|
10501
|
-
indent: 2,
|
|
10502
|
-
quotingType: "\""
|
|
10503
|
-
}), "utf-8");
|
|
10504
|
-
node_fs.renameSync(tmpPath, this.configPath);
|
|
10505
|
-
this.bootstrapConfig = validation.data;
|
|
10506
|
-
}
|
|
10507
|
-
/**
|
|
10508
|
-
* Deep-set a value in a nested plain object using a dot-notation path.
|
|
10509
|
-
* Returns a new object (immutable).
|
|
10510
|
-
*/
|
|
10511
|
-
setNested(obj, configPath, value) {
|
|
10512
|
-
const [head, ...rest] = configPath.split(".");
|
|
10513
|
-
if (!head) return obj;
|
|
10514
|
-
if (rest.length === 0) return {
|
|
10515
|
-
...obj,
|
|
10516
|
-
[head]: value
|
|
10517
|
-
};
|
|
10518
|
-
const existing = obj[head];
|
|
10519
|
-
const child = isRecord(existing) ? existing : {};
|
|
10520
|
-
return {
|
|
10521
|
-
...obj,
|
|
10522
|
-
[head]: this.setNested(child, rest.join("."), value)
|
|
10523
|
-
};
|
|
10524
|
-
}
|
|
10525
|
-
/**
|
|
10526
|
-
* Apply env var overrides onto the raw YAML object.
|
|
10527
|
-
* Only bootstrap-level env vars are applied.
|
|
10528
|
-
*/
|
|
10529
|
-
applyEnvOverrides(raw) {
|
|
10530
|
-
let result = { ...raw };
|
|
10531
|
-
for (const [envKey, configPath] of Object.entries(ENV_VAR_MAP)) {
|
|
10532
|
-
const envValue = process.env[envKey];
|
|
10533
|
-
if (envValue === void 0 || envValue === "") continue;
|
|
10534
|
-
const coerced = configPath === "server.port" ? Number(envValue) : envValue;
|
|
10535
|
-
result = this.setNested(result, configPath, coerced);
|
|
10536
|
-
console.log(`[ConfigManager] Env override applied: ${envKey} → ${configPath}`);
|
|
10537
|
-
}
|
|
10538
|
-
return result;
|
|
10539
|
-
}
|
|
10540
|
-
loadYaml() {
|
|
10541
|
-
if (!node_fs.existsSync(this.configPath)) {
|
|
10542
|
-
console.warn(`[ConfigManager] Config file not found at: ${this.configPath}\n → Using built-in defaults. Set CONFIG_PATH env var or create the file.\n → Example path from project root: ./server/backend/data/config.yaml`);
|
|
10543
|
-
return {};
|
|
10544
|
-
}
|
|
10545
|
-
const parsed = load$1(node_fs.readFileSync(this.configPath, "utf-8")) ?? {};
|
|
10546
|
-
console.log(`[ConfigManager] Loaded config from: ${this.configPath}`);
|
|
10547
|
-
return parsed;
|
|
10548
|
-
}
|
|
10549
|
-
warnDefaultCredentials() {
|
|
10550
|
-
if (this.bootstrapConfig.auth.adminPassword === "changeme") console.warn("[ConfigManager] Warning: Using default admin password \"changeme\". Set auth.adminPassword in your config.yaml or the CAMSTACK_ADMIN_PASS env var.");
|
|
10935
|
+
this.bootstrapConfig = writeBootstrapSection(this.configPath, section, data);
|
|
10551
10936
|
}
|
|
10552
10937
|
getFromBootstrap(configPath) {
|
|
10553
10938
|
const keys = configPath.split(".");
|
|
@@ -96187,8 +96572,10 @@ exports.SOCKET_PLANE_TOP_N = require_child_cap_dispatch.SOCKET_PLANE_TOP_N;
|
|
|
96187
96572
|
exports.SOCKET_QUEUE_TOP_N = require_child_cap_dispatch.SOCKET_QUEUE_TOP_N;
|
|
96188
96573
|
exports.STALL_REPORT_MS = require_event_loop_stall_monitor.STALL_REPORT_MS;
|
|
96189
96574
|
exports.STALL_TICK_MS = require_event_loop_stall_monitor.STALL_TICK_MS;
|
|
96575
|
+
exports.STORE_READABLE_BOOTSTRAP_KEYS = STORE_READABLE_BOOTSTRAP_KEYS;
|
|
96190
96576
|
exports.ScopedLogger = ScopedLogger;
|
|
96191
96577
|
exports.ScopedTokenManager = require_builtins_local_auth_local_auth_addon.ScopedTokenManager;
|
|
96578
|
+
exports.SettingsReadUnavailableError = require_builtins_local_auth_local_auth_addon.SettingsReadUnavailableError;
|
|
96192
96579
|
exports.SocketChannel = require_child_cap_dispatch.SocketChannel;
|
|
96193
96580
|
exports.SqliteSettingsAddon = require_builtins_sqlite_storage_sqlite_settings_addon.SqliteSettingsAddon;
|
|
96194
96581
|
exports.SqliteSettingsBackend = require_builtins_sqlite_storage_sqlite_settings_addon.SqliteSettingsBackend;
|
|
@@ -96411,6 +96798,7 @@ exports.isClusterSecretMismatchError = isClusterSecretMismatchError;
|
|
|
96411
96798
|
exports.isCollectionArrayMethod = require_dist.isCollectionArrayMethod;
|
|
96412
96799
|
exports.isInfraCapability = isInfraCapability;
|
|
96413
96800
|
exports.isModelDownloaded = require_event_loop_stall_monitor.isModelDownloaded;
|
|
96801
|
+
exports.isSettingsReadUnavailable = require_builtins_local_auth_local_auth_addon.isSettingsReadUnavailable;
|
|
96414
96802
|
exports.isSourceNewer = isSourceNewer;
|
|
96415
96803
|
exports.isolatedBuiltinPhase = isolatedBuiltinPhase;
|
|
96416
96804
|
exports.loadTlsCert = require_tls$1.loadTlsCert;
|
|
@@ -96463,6 +96851,7 @@ exports.socketDirectionMessages = require_child_cap_dispatch.socketDirectionMess
|
|
|
96463
96851
|
exports.startEventLoopStallMonitor = require_event_loop_stall_monitor.startEventLoopStallMonitor;
|
|
96464
96852
|
exports.startHeapWatch = require_child_cap_dispatch.startHeapWatch;
|
|
96465
96853
|
exports.startRunnerHeapWatch = require_child_cap_dispatch.startRunnerHeapWatch;
|
|
96854
|
+
exports.storeMayAnswer = storeMayAnswer;
|
|
96466
96855
|
exports.strandedMb = require_child_cap_dispatch.strandedMb;
|
|
96467
96856
|
exports.stripCamstackDeps = stripCamstackDeps;
|
|
96468
96857
|
exports.subscribePassthrough = require_child_cap_dispatch.subscribePassthrough;
|