@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.
Files changed (87) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/auth/auth-manager.d.ts +11 -0
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  5. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  7. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  8. package/dist/builtins/alerts/alerts.addon.js +1 -1
  9. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  10. package/dist/builtins/autotrack/index.js +1 -1
  11. package/dist/builtins/autotrack/index.mjs +1 -1
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  13. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  14. package/dist/builtins/camera-grid/index.js +1 -1
  15. package/dist/builtins/camera-grid/index.mjs +1 -1
  16. package/dist/builtins/composer/composer.addon.js +347 -181
  17. package/dist/builtins/composer/composer.addon.mjs +347 -181
  18. package/dist/builtins/composer/composition-evaluation.d.ts +37 -0
  19. package/dist/builtins/composer/composition-runtime.d.ts +11 -52
  20. package/dist/builtins/composer/composition-target.d.ts +50 -0
  21. package/dist/builtins/composer/existing-target.d.ts +19 -1
  22. package/dist/builtins/composer/own-field-reads.d.ts +19 -0
  23. package/dist/builtins/console-logging/index.js +1 -1
  24. package/dist/builtins/console-logging/index.mjs +1 -1
  25. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  26. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  27. package/dist/builtins/device-manager/device-manager.addon.js +265 -29
  28. package/dist/builtins/device-manager/device-manager.addon.mjs +265 -29
  29. package/dist/builtins/device-manager/device-provider-context.d.ts +6 -0
  30. package/dist/builtins/device-manager/device-source-activation.d.ts +26 -1
  31. package/dist/builtins/device-manager/device-state-mirror.d.ts +41 -9
  32. package/dist/builtins/device-manager/mirror-row-writer.d.ts +59 -1
  33. package/dist/builtins/doorbell/binding-mirror.d.ts +45 -81
  34. package/dist/builtins/doorbell/doorbell-extension-slot.d.ts +4 -3
  35. package/dist/builtins/doorbell/virtual-doorbell.addon.d.ts +53 -11
  36. package/dist/builtins/doorbell/virtual-doorbell.addon.js +207 -82
  37. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +207 -82
  38. package/dist/builtins/hub-forwarder/index.js +1 -1
  39. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  40. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  41. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  42. package/dist/builtins/local-auth/local-auth.addon.js +47 -3
  43. package/dist/builtins/local-auth/local-auth.addon.mjs +46 -4
  44. package/dist/builtins/local-network/local-network.addon.js +1 -1
  45. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  46. package/dist/builtins/loki-logging/index.js +1 -1
  47. package/dist/builtins/loki-logging/index.mjs +1 -1
  48. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  49. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  50. package/dist/builtins/platform-probe/index.js +1 -1
  51. package/dist/builtins/platform-probe/index.mjs +1 -1
  52. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  53. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  54. package/dist/builtins/snapshot/index.js +1 -1
  55. package/dist/builtins/snapshot/index.mjs +1 -1
  56. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  57. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  58. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  59. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  60. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  61. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  62. package/dist/builtins/system-config/system-config.addon.js +4 -4
  63. package/dist/builtins/system-config/system-config.addon.mjs +4 -4
  64. package/dist/builtins/winston-logging/index.js +1 -1
  65. package/dist/builtins/winston-logging/index.mjs +1 -1
  66. package/dist/{child-cap-dispatch-AAltfBUH.mjs → child-cap-dispatch-C2VINHu1.mjs} +70 -18
  67. package/dist/{child-cap-dispatch-DzcUd_ct.js → child-cap-dispatch-C8uts1P4.js} +70 -18
  68. package/dist/{composition-sources-fUyF4z39.mjs → composition-sources-DVClu006.mjs} +1 -1
  69. package/dist/{composition-sources-BwE1POnF.js → composition-sources-Dep7nBwu.js} +1 -1
  70. package/dist/{dist-BPvku43i.mjs → dist-Dori4pf3.mjs} +85 -6
  71. package/dist/{dist-RAl10EfG.js → dist-a6p0V-IL.js} +85 -6
  72. package/dist/index.js +739 -350
  73. package/dist/index.mjs +737 -352
  74. package/dist/kernel/bootstrap-config.d.ts +8 -0
  75. package/dist/kernel/capability-registry.d.ts +30 -0
  76. package/dist/kernel/config-manager.d.ts +75 -129
  77. package/dist/kernel/index.d.ts +5 -2
  78. package/dist/kernel/moleculer/runtime-state-load.d.ts +19 -0
  79. package/dist/kernel/settings-door-view.d.ts +24 -1
  80. package/dist/kernel/settings-read-error.d.ts +37 -0
  81. package/dist/kernel/settings-view-types.d.ts +105 -0
  82. package/dist/kernel/store-settings-view.d.ts +17 -0
  83. package/dist/kernel/system-settings-mirror.d.ts +34 -0
  84. package/dist/kernel/system-settings-policy.d.ts +47 -0
  85. package/dist/{retired-settings-keys-D3rGTGq5.js → retired-settings-keys-DC1uHWPZ.js} +1 -1
  86. package/dist/{retired-settings-keys-DqVWSrVw.mjs → retired-settings-keys-DVcKRgzm.mjs} +1 -1
  87. 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-RAl10EfG.js");
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-DzcUd_ct.js");
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$2(value) {
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$2(value)) return void 0;
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$2(a) && typeof a.id === "string")) return void 0;
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$2(modUnknown) ? modUnknown : {};
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$2(modUnknown) ? modUnknown : {};
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$1(value) {
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$1(value) ? value : {};
7518
+ return isRecord$2(value) ? value : {};
7465
7519
  }
7466
7520
  function unwrapValue(parsed) {
7467
- if (!isRecord$1(parsed) || !("value" in parsed)) return parsed;
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)] = unwrapValue(row.data);
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-schema.ts
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
- * Bootstrap-time addon configuration — read PURELY from YAML, before the
10007
- * kernel can talk to the settings-store. This breaks the chicken-and-egg
10008
- * where the settings store itself is implemented by an addon: the kernel
10009
- * needs to know WHICH addon to load first to provide the store, and that
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
- var bootstrapAddonsSchema = zod.z.object({
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
- var ConfigManager = class ConfigManager {
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
- const rawYaml = this.loadYaml();
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
- if (this.settingsStore !== null) {
10142
- const storeValue = this.settingsStore.getSystem(configPath);
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
- * Write a value to the settings-store.
10152
- * Throws if the settings store is not yet wired.
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
- if (this.settingsDoor !== null) {
10160
- this.settingsDoor.set({
10161
- collection: "system-settings",
10162
- key,
10163
- value
10164
- }).catch((err) => {
10165
- console.error("[ConfigManager] isolated settings-store write failed", err);
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
- if (this.settingsStore !== null) {
10193
- const nested = this.getNestedFromSystemSettings(section);
10194
- if (nested) Object.assign(merged, nested);
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
- /** Read all config for an addon from addon_settings. */
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
- return (0, _camstack_types_addon.asJsonObject)(this.getFromBootstrap(`addons.${addonId}`)) ?? {};
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) return {};
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. `{}` before the settings store
10265
- * is wired — the same answer as "this device has never persisted a slice",
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) return {};
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.getSection(section),
10878
+ getSection: (section) => this.readSectionThroughDoor(door, section),
10353
10879
  setSection: async (section, patch) => {
10354
- for (const [key, value] of Object.entries(patch)) await door.set({
10355
- collection: "system-settings",
10356
- key: `${section}.${key}`,
10357
- value
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
- const cm = this;
10365
- return {
10366
- async readAddonStore() {
10367
- return cm.getAddonConfig(addonId);
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
- /** Features accessor -- reads from settings-store when available, falls back to RUNTIME_DEFAULTS */
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
- const features = {
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 of config.yaml and sync in-memory.
10484
- * Only bootstrap sections (server, auth, mode) are written to YAML.
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
- if (!ConfigManager.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`);
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;