@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
@@ -0,0 +1,8 @@
1
+ import { AppConfig, BootstrapConfig } from './config-schema.js';
2
+ /** config.yaml, env overrides applied, parsed. */
3
+ export declare function loadBootstrapConfig(configPath: string): BootstrapConfig;
4
+ export declare function writeBootstrapSection(configPath: string, section: string, data: Record<string, unknown>): BootstrapConfig;
5
+ /**
6
+ * Returns a merged view of bootstrap config + runtime defaults for backward compat.
7
+ */
8
+ export declare function buildRawAppConfig(bootstrapConfig: BootstrapConfig): AppConfig;
@@ -31,6 +31,17 @@ export type CollectionConfigReader = (capability: string) => readonly string[] |
31
31
  * cannot present it. See docs/decisions/adr-0188-an-unregister-carries-proof-of-ownership.md.
32
32
  */
33
33
  export type ProviderOwnerToken = string;
34
+ /** What {@link CapabilityRegistry.applyPersistedPreferences} changed. */
35
+ export interface PersistedPreferenceReplay {
36
+ readonly singletons: ReadonlyArray<{
37
+ readonly capability: string;
38
+ readonly addonId: string;
39
+ }>;
40
+ readonly disabled: ReadonlyArray<{
41
+ readonly capability: string;
42
+ readonly addonId: string;
43
+ }>;
44
+ }
34
45
  export declare class CapabilityRegistry {
35
46
  private readonly capabilities;
36
47
  private readonly logger;
@@ -234,6 +245,25 @@ export declare class CapabilityRegistry {
234
245
  * successor's live registration ([D188](../../../../docs/decisions/adr-0188-an-unregister-carries-proof-of-ownership.md)).
235
246
  */
236
247
  unregisterProvider(capabilityName: string, addonId: string, owner?: ProviderOwnerToken): void;
248
+ /**
249
+ * Re-apply the operator's PERSISTED preferences to providers that
250
+ * registered before those preferences could be read — once, when the boot
251
+ * read of them settles (D675).
252
+ *
253
+ * `registerProvider` consults the preference readers at the moment a
254
+ * provider registers. With the settings engine isolated (D231) the
255
+ * preferences arrive over an async door, so a provider can register before
256
+ * they are known; the readers then answer "no preference" and the default
257
+ * wins. This replays the answer the readers give NOW over every registered
258
+ * provider: a singleton whose persisted choice is registered and not active
259
+ * is switched to it, and a collection provider the operator disabled is
260
+ * disabled again. It is one pass, triggered by the read settling — not a
261
+ * wait, a poll or a retry (D3).
262
+ *
263
+ * `skip` names the capabilities that must not be switched live (infra caps
264
+ * need a restart — the same rule `capabilities.setPreference` applies).
265
+ */
266
+ applyPersistedPreferences(skip: (capability: string) => boolean): PersistedPreferenceReplay;
237
267
  /** Enable a previously disabled collection provider. */
238
268
  enableCollectionProvider(capability: string, addonId: string): void;
239
269
  /** Disable a collection provider (keeps it registered but excluded from active list). */
@@ -1,101 +1,16 @@
1
1
  import { AppConfig } from './config-schema.js';
2
- import { AddonStoreRead, FeatureManifest, ISettingsStoreProvider } from '@camstack/types';
3
- export interface AddonSettingsView {
4
- readAddonStore(): Promise<Record<string, unknown>>;
5
- /** See `IAddonContext['settings'].readAddonStoreResult` (D651). */
6
- readAddonStoreResult?(): Promise<AddonStoreRead>;
7
- writeAddonStore(patch: Record<string, unknown>): Promise<void>;
8
- readDeviceStore(deviceId: number): Promise<Record<string, unknown>>;
9
- /** See `IAddonContext['settings'].readDeviceStoreBatch` — optional there and
10
- * here for the same reason: only the door-backed view can answer it in one
11
- * range scan, and a caller must fall back rather than refuse. */
12
- readDeviceStoreBatch?(deviceIds: readonly number[]): Promise<ReadonlyMap<number, Record<string, unknown>>>;
13
- writeDeviceStore(deviceId: number, patch: Record<string, unknown>): Promise<void>;
14
- clearDeviceStore(deviceId: number): Promise<void>;
15
- getSection(section: string): Promise<Record<string, unknown>>;
16
- setSection(section: string, patch: Record<string, unknown>): Promise<void>;
17
- /**
18
- * Per-device runtime state — separate namespace from
19
- * `readDeviceStore` / `writeDeviceStore` (which is the addon's
20
- * config blob: host, password, deviceCache, …). Holds mutable
21
- * runtime signals discovered after boot: battery snapshot, sleep
22
- * flag, last-seen timestamps. Stored under a kernel-internal
23
- * `__device-state` namespace so the device's config schema
24
- * never has to know about it (clean separation of operator
25
- * intent from runtime telemetry).
26
- *
27
- * Patch semantics: shallow merge. Pass the FULL persistent slice
28
- * each call (the kernel doesn't try to compute deltas) — concrete
29
- * implementations of `DeviceRuntimeState` already coalesce in
30
- * memory, so the kernel just persists the latest blob.
31
- */
32
- readDeviceRuntimeState(deviceId: number): Promise<Record<string, unknown>>;
33
- writeDeviceRuntimeState(deviceId: number, data: Record<string, unknown>): Promise<void>;
34
- clearDeviceRuntimeState(deviceId: number): Promise<void>;
35
- }
36
- /**
37
- * A CONCRETE backend behind {@link ConfigManager.createSettingsView} — the
38
- * sync-`ISettingsStore` one or the async-door one.
39
- *
40
- * `readDeviceStoreBatch` is required here and optional on
41
- * {@link AddonSettingsView} because both of these can answer it, so the view
42
- * the kernel hands an addon never has to decide whether the method exists.
43
- * The optionality on the public interface stays for the implementation that
44
- * genuinely cannot batch: a forked runner's UDS-backed view.
45
- */
46
- export interface SettingsViewBackend extends AddonSettingsView {
47
- readDeviceStoreBatch(deviceIds: readonly number[]): Promise<ReadonlyMap<number, Record<string, unknown>>>;
48
- }
49
- export interface ISettingsStore {
50
- getSystem(key: string): unknown;
51
- setSystem(key: string, value: unknown): void;
52
- getAllSystem(): Record<string, unknown>;
53
- /** Addon-level global settings (apply to every device unless overridden). */
54
- getAllAddon(addonId: string): Record<string, unknown>;
55
- setAllAddon(addonId: string, config: Record<string, unknown>): void;
56
- /** Legacy per-provider settings (used by some older wiring paths). */
57
- getAllProvider(providerId: string): Record<string, unknown>;
58
- setProvider(providerId: string, key: string, value: unknown): void;
59
- /** Legacy flat per-device settings. Kept for backward compatibility
60
- * with pre-addon wiring; new code should use the addon-device API. */
61
- getAllDevice(deviceId: string): Record<string, unknown>;
62
- setDevice(deviceId: string, key: string, value: unknown): void;
63
- /**
64
- * Multi-level addon settings: per-device overrides for a specific addon.
65
- *
66
- * Resolution chain when the frontend asks for the effective value of a
67
- * field on a given device:
68
- * 1. schema default (from `ConfigUISchema.field.default`)
69
- * 2. `getAllAddon(addonId)` — addon global value
70
- * 3. `getAddonDevice(addonId, devId)` — per-device override (wins)
71
- *
72
- * Only fields declared with `scope: 'device'` in the addon's
73
- * `ConfigUISchema` should ever be persisted here.
74
- */
75
- getAddonDevice(addonId: string, deviceId: string): Record<string, unknown>;
76
- setAddonDevice(addonId: string, deviceId: string, values: Record<string, unknown>): void;
77
- clearAddonDevice(addonId: string, deviceId: string): void;
78
- /**
79
- * Per-device runtime state — the whole cap-slice map as ONE row in the
80
- * `device-runtime-state` collection.
81
- *
82
- * These are separate from `getAddonDevice`/`setAddonDevice` because runtime
83
- * state is not an addon's config: it belongs to the device, it is replaced
84
- * wholesale on every flush, and it is written orders of magnitude more often.
85
- * It USED to share that keyspace under a synthetic `__device-state` addonId,
86
- * which cost one row (and one INSERT) per cap slice per flush and left the
87
- * collection it is named for empty on every installation. The backend keeps
88
- * reading the legacy prefix; see `SqliteSettingsBackend.getDeviceRuntimeState`.
89
- */
90
- getDeviceRuntimeState(deviceId: string): Record<string, unknown>;
91
- setDeviceRuntimeState(deviceId: string, blob: Record<string, unknown>): void;
92
- clearDeviceRuntimeState(deviceId: string): void;
93
- }
2
+ import { FeatureManifest, ISettingsStoreProvider } from '@camstack/types';
3
+ import { ISettingsStore, KernelAddonSettingsView } from './settings-view-types.js';
4
+ import { SystemSettingsPreload } from './system-settings-mirror.js';
5
+ export type { AddonSettingsView, ISettingsStore, KernelAddonSettingsView, SettingsViewBackend, } from './settings-view-types.js';
6
+ export type { SystemSettingsPreload } from './system-settings-mirror.js';
94
7
  export declare class ConfigManager {
95
8
  private readonly configPath;
96
9
  private bootstrapConfig;
97
10
  private settingsStore;
98
11
  private settingsDoor;
12
+ /** Boot-time mirror of the preloaded `system-settings` keys (door mode, D675). */
13
+ private readonly systemMirror;
99
14
  constructor(configPath: string);
100
15
  /**
101
16
  * Wire the settings-store backend. Called once, after the `settings-store`
@@ -109,9 +24,25 @@ export declare class ConfigManager {
109
24
  * prefers the sync store when both are present.
110
25
  */
111
26
  setSettingsDoor(door: ISettingsStoreProvider): void;
27
+ /**
28
+ * Resolves once the boot read of the preloaded `system-settings` keys has
29
+ * settled — the hook the addon registry uses to log it and to replay the
30
+ * capability preferences onto providers that registered before it (D675).
31
+ * `null` when no door has been wired (a sync store needs no preload).
32
+ */
33
+ whenSystemSettingsPreloaded(): Promise<SystemSettingsPreload> | null;
34
+ private backendMode;
35
+ /** The refusal every sync addon-scoped reader raises without a sync store. */
36
+ private refuseSyncRead;
112
37
  /**
113
38
  * Get a config value by dot-notation path.
114
- * Priority: bootstrap config (yaml) → settings-store → RUNTIME_DEFAULTS fallback.
39
+ * Priority: bootstrap config (yaml + env) → settings-store → RUNTIME_DEFAULTS.
40
+ *
41
+ * The store layer (D675) never answers a bootstrap-owned key outside the
42
+ * allow-list (`storeMayAnswer`). With only the door (production) it answers
43
+ * the PRELOADED keys from the boot mirror and REFUSES the rest
44
+ * ({@link SettingsReadUnavailableError}) — it used to skip the store in
45
+ * silence. Before any backend is wired it is absent (boot-time readers).
115
46
  *
116
47
  * Returns `unknown` — callers must narrow via `typeof`/type guards, OR use
117
48
  * the generic overload at their own risk. The generic overload exists only
@@ -122,10 +53,19 @@ export declare class ConfigManager {
122
53
  get<T>(configPath: string): T | undefined;
123
54
  private resolveConfigValue;
124
55
  /**
125
- * Write a value to the settings-store.
126
- * Throws if the settings store is not yet wired.
56
+ * The settings-store layer of {@link get}: the stored value, `undefined`
57
+ * when the store holds nothing (or may not answer this key), or a THROW
58
+ * when the store cannot be asked.
127
59
  */
128
- set(key: string, value: unknown): void;
60
+ private readStoreLayer;
61
+ /**
62
+ * Write a value to the settings-store. Resolves only once the write LANDED:
63
+ * on the isolated engine it awaits the door, and a failed write REJECTS and
64
+ * is not reported back by the mirror (D675 — it used to be fire-and-forget,
65
+ * so a lost write read as saved for the life of the process). Rejects when
66
+ * no store is wired.
67
+ */
68
+ set(key: string, value: unknown): Promise<void>;
129
69
  /**
130
70
  * Bulk-read all keys that belong to a logical section.
131
71
  * A "section" is the first segment of a dot-notation key (e.g. 'features', 'logging').
@@ -140,30 +80,48 @@ export declare class ConfigManager {
140
80
  * shadowing the rest of the section that only lives in config.yaml.
141
81
  * The kernel is agnostic about the store backend — any `ISettingsStore`
142
82
  * implementation (sqlite, redis, in-memory, …) behaves identically here.
83
+ *
84
+ * D675: a bootstrap-owned section takes ONLY allow-listed keys from the
85
+ * store (a stored `auth.jwtSecret` never replaces env), and with only the
86
+ * door wired this sync read THROWS — the settings view's `getSection` reads
87
+ * the door.
143
88
  */
144
89
  getSection(section: string): Record<string, unknown>;
90
+ /** Runtime defaults, then config.yaml + env — the layers under the store. */
91
+ private sectionBase;
92
+ /**
93
+ * {@link getSection} through the async door — what the door-backed settings
94
+ * view answers (D675). Same layers, same allow-list; a failed door read
95
+ * THROWS rather than handing back the defaults as if nothing were stored.
96
+ */
97
+ private readSectionThroughDoor;
145
98
  /**
146
99
  * Bulk-write a logical section to the settings-store.
147
100
  * Each entry in `data` is persisted under the key `section.<entryKey>`.
148
101
  */
149
102
  setSection(section: string, data: Record<string, unknown>): void;
150
- /** Read all config for an addon from addon_settings. */
103
+ /**
104
+ * Read all config for an addon — SYNC store only. THROWS without one: its
105
+ * old bootstrap fallback answered `{}` for every addon on every
106
+ * isolated-engine hub (D675). Async three-way: {@link readAddonGlobalStrict}.
107
+ */
151
108
  getAddonConfig(addonId: string): Record<string, unknown>;
109
+ /**
110
+ * One addon's global store, three-way (D675): the record, `{}` when the
111
+ * store ANSWERED with nothing, or a THROW when it could not be asked.
112
+ * Behind `addonSettingsRaw.getGlobal` and `$hub.getAddonConfig`.
113
+ */
114
+ readAddonGlobalStrict(addonId: string): Promise<Record<string, unknown>>;
115
+ /** One addon's per-device overrides, three-way like {@link readAddonGlobalStrict}. */
116
+ readAddonDeviceStrict(addonId: string, deviceId: number): Promise<Record<string, unknown>>;
152
117
  /** Write (bulk-replace) config for an addon to addon_settings. */
153
118
  setAddonConfig(addonId: string, config: Record<string, unknown>): void;
154
- /** Read all config for a provider from provider_settings. */
155
- getProviderConfig(providerId: string): Record<string, unknown>;
156
- /** Write (upsert) a single key for a provider to provider_settings. */
157
- setProviderConfig(providerId: string, key: string, value: unknown): void;
158
- /** Read all config for a device from device_settings. */
159
- getDeviceConfig(deviceId: string): Record<string, unknown>;
160
- /** Write (upsert) a single key for a device to device_settings. */
161
- setDeviceConfig(deviceId: string, key: string, value: unknown): void;
162
119
  /**
163
120
  * Read per-device overrides for a specific addon. Returns a flat record
164
121
  * of field → value. Missing keys fall back to the addon-global value via
165
122
  * `getAddonConfig(addonId)` — that composition is the resolver service's
166
123
  * responsibility, not ConfigManager's.
124
+ * SYNC store only — THROWS without one (D675); async: {@link readAddonDeviceStrict}.
167
125
  */
168
126
  getAddonDevice(addonId: string, deviceId: string): Record<string, unknown>;
169
127
  /**
@@ -178,9 +136,8 @@ export declare class ConfigManager {
178
136
  */
179
137
  clearAddonDevice(addonId: string, deviceId: string): void;
180
138
  /**
181
- * The device's persisted runtime-state blob. `{}` before the settings store
182
- * is wired — the same answer as "this device has never persisted a slice",
183
- * which is what every caller already handles.
139
+ * The device's persisted runtime-state blob. THROWS before the store is
140
+ * wired: a store not asked is not an empty row (D677, D675).
184
141
  */
185
142
  getDeviceRuntimeState(deviceId: string): Record<string, unknown>;
186
143
  /**
@@ -208,42 +165,31 @@ export declare class ConfigManager {
208
165
  * That is what made `storageMigration.start` fail on its first durable write
209
166
  * while every other addon's persistence worked. See D294.
210
167
  */
211
- createSettingsView(addonId: string): AddonSettingsView;
168
+ createSettingsView(addonId: string): KernelAddonSettingsView;
212
169
  /** The async-door backend of {@link createSettingsView}. */
213
170
  private createDoorBackedSettingsView;
214
171
  /** The sync-`ISettingsStore` backend of {@link createSettingsView}. */
215
172
  private createStoreSettingsView;
173
+ /** Is a SYNC `ISettingsStore` wired? (The store-backed view's D294-window test.) */
174
+ hasSyncStore(): boolean;
216
175
  /** Get a value from the parsed bootstrap config.
217
176
  * Generic overload is a documented type-level bridge — callers are responsible
218
177
  * for passing a T that matches the config.yaml shape. */
219
178
  getBootstrap(configPath: string): unknown;
220
179
  getBootstrap<T>(configPath: string): T | undefined;
221
- /** Features accessor -- reads from settings-store when available, falls back to RUNTIME_DEFAULTS */
222
- get features(): FeatureManifest;
223
180
  /**
224
- * Returns a merged view of bootstrap config + runtime defaults for backward compat.
181
+ * Features — the sync store when wired, else RUNTIME_DEFAULTS DELIBERATELY
182
+ * (not `get()`, which refuses in door mode, D675): nothing writes
183
+ * `features.*` beyond sqlite-settings' first-boot seed of these defaults.
225
184
  */
185
+ get features(): FeatureManifest;
186
+ /** Bootstrap config + runtime defaults, for backward compat. */
226
187
  get raw(): AppConfig;
227
- /** Sections that live in config.yaml. Everything else goes to the settings-store. */
228
- private static readonly BOOTSTRAP_SECTIONS;
229
188
  /**
230
- * Atomically update one top-level section of config.yaml and sync in-memory.
231
- * Only bootstrap sections (server, auth, mode) are written to YAML.
232
- * Runtime settings must use setSection() which writes to the settings-store.
189
+ * Atomically update one top-level bootstrap section (server, auth, mode) of
190
+ * config.yaml and sync in-memory. Runtime settings use setSection().
233
191
  */
234
192
  update(section: string, data: Record<string, unknown>): void;
235
- /**
236
- * Deep-set a value in a nested plain object using a dot-notation path.
237
- * Returns a new object (immutable).
238
- */
239
- private setNested;
240
- /**
241
- * Apply env var overrides onto the raw YAML object.
242
- * Only bootstrap-level env vars are applied.
243
- */
244
- private applyEnvOverrides;
245
- private loadYaml;
246
- private warnDefaultCredentials;
247
193
  private getFromBootstrap;
248
194
  private getFromRuntimeDefaults;
249
195
  /**
@@ -19,7 +19,7 @@ export { overlayGetStatus, UNCLAIMED } from './status-overlay.js';
19
19
  export type { ClaimedStatusResolver, StatusOverlay, StatusOverlayAnswer } from './status-overlay.js';
20
20
  export { describeProviderKindDrift } from './provider-kind-drift.js';
21
21
  export type { ProviderKindHint } from './provider-kind-drift.js';
22
- export type { CapabilityRouter, CapabilityRouterFactory, ConfigReader, ProviderOwnerToken, } from './capability-registry.js';
22
+ export type { CapabilityRouter, CapabilityRouterFactory, ConfigReader, PersistedPreferenceReplay, ProviderOwnerToken, } from './capability-registry.js';
23
23
  export { CustomActionRegistry, type CustomActionEntry } from './custom-action-registry.js';
24
24
  export { INFRA_CAPABILITIES, isInfraCapability } from './infra-capabilities.js';
25
25
  export type { InfraCapability } from './infra-capabilities.js';
@@ -27,7 +27,10 @@ export { isolatedBuiltinPhase, partitionIsolatedBuiltinIds, runHubAddonBoot, wai
27
27
  export type { IsolatedBuiltinPhase, IsolatedBuiltinWaves, CapabilityName, WaitUntilReadyOptions, HubAddonBootSteps, } from './isolated-builtin-phase.js';
28
28
  export { createDoorSettingsView } from './settings-door-view.js';
29
29
  export { ConfigManager } from './config-manager.js';
30
- export type { ISettingsStore } from './config-manager.js';
30
+ export type { ISettingsStore, KernelAddonSettingsView, SystemSettingsPreload, } from './config-manager.js';
31
+ export { SettingsReadUnavailableError, isSettingsReadUnavailable } from './settings-read-error.js';
32
+ export type { SettingsReadRefusal } from './settings-read-error.js';
33
+ export { storeMayAnswer, STORE_READABLE_BOOTSTRAP_KEYS } from './system-settings-policy.js';
31
34
  export { bootstrapSchema, RUNTIME_DEFAULTS, DEFAULT_DATA_PATH } from './config-schema.js';
32
35
  export type { BootstrapConfig, AppConfig, ServerMode } from './config-schema.js';
33
36
  export type { AddonSettingsView } from './config-manager.js';
@@ -0,0 +1,19 @@
1
+ import { IScopedLogger } from '@camstack/types';
2
+ /** Where the load was for — named on the refusal line. */
3
+ export type RuntimeStateLoadPhase = 'create' | 'accessory-child' | 'rebuild' | 'context';
4
+ /** A device build refused because its runtime state could not be loaded. */
5
+ export declare class RuntimeStateLoadError extends Error {
6
+ readonly deviceId: number;
7
+ constructor(deviceId: number, reason: string, options?: ErrorOptions);
8
+ }
9
+ export interface RuntimeStateLoadTarget {
10
+ readonly deviceId: number;
11
+ readonly stableId: string;
12
+ readonly phase: RuntimeStateLoadPhase;
13
+ }
14
+ /**
15
+ * Load the device's runtime state, or throw {@link RuntimeStateLoadError}
16
+ * after one warn naming the device. An answer that is not an object is a
17
+ * failed load too — it would hydrate as nothing.
18
+ */
19
+ export declare function loadRuntimeStateOrRefuse(load: () => Promise<unknown>, logger: IScopedLogger, target: RuntimeStateLoadTarget): Promise<Record<string, unknown>>;
@@ -1,7 +1,30 @@
1
1
  import { ISettingsStoreProvider } from '@camstack/types';
2
2
  import { SettingsViewBackend } from './config-manager.js';
3
+ /**
4
+ * Every `system-settings` row under `prefix`, keyed by the suffix — or a throw.
5
+ *
6
+ * `system-settings` stores the value ITSELF (`setSystem` / `door.set` write
7
+ * `JSON.stringify(value)`), mostly scalars. The engine's `query` hands a KV row
8
+ * back parsed only when its text is an object or an array; a scalar comes back
9
+ * as `{ value: <its JSON text> }` — `"12h"` arrives as `{ value: '"12h"' }`.
10
+ * That is not a value anyone stored, and guessing it back would misread a
11
+ * stored object that happens to be `{ value: … }`. So the range read only
12
+ * ENUMERATES the keys (same keyset walk and refusal as every prefix read
13
+ * here), and each value is taken from `get`, which parses the row exactly
14
+ * (D675). These reads are small and rare: a section's handful of keys when a
15
+ * form opens, and the capability preferences once at boot.
16
+ */
17
+ export declare function loadSystemSettingsPrefix(door: ISettingsStoreProvider, prefix: string,
18
+ /** Suffixes the caller may read. A refused one is never FETCHED — the
19
+ * `auth` section's credential rows never enter this process (D675). */
20
+ mayRead?: (suffix: string) => boolean): Promise<Record<string, unknown>>;
21
+ /**
22
+ * The `system-settings` section surface a door-backed view delegates to.
23
+ * `getSection` is ASYNC: it reads the section through the door (D675) — a
24
+ * synchronous reader has no store to ask when the engine is isolated.
25
+ */
3
26
  export interface DoorSectionAccess {
4
- getSection(section: string): Record<string, unknown>;
27
+ getSection(section: string): Promise<Record<string, unknown>>;
5
28
  setSection(section: string, patch: Record<string, unknown>): void | Promise<void>;
6
29
  }
7
30
  export declare function createDoorSettingsView(addonId: string, door: ISettingsStoreProvider, sections: DoorSectionAccess): SettingsViewBackend;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The refusal a SYNCHRONOUS settings reader raises when it cannot answer — D675.
3
+ *
4
+ * `ConfigManager`'s sync readers (`get`, `getSection`, `getAddonConfig`, …)
5
+ * used to answer `{}` / a runtime default whenever no sync `ISettingsStore`
6
+ * was wired. Since D231 isolated `sqlite-settings`, hub-main never gets one:
7
+ * only the async door. So every such read answered "nothing stored" for the
8
+ * life of the process, and "nothing stored" is exactly what a store holding
9
+ * nothing looks like (D315, D393). The readers now refuse instead, and this
10
+ * is what they throw, so a caller can tell "the store could not be asked"
11
+ * from every other failure and from "never written".
12
+ */
13
+ /**
14
+ * Why the read was refused.
15
+ *
16
+ * - `door-only` — the engine is isolated: only the async door is wired, and a
17
+ * synchronous caller cannot await it. Use the settings view instead.
18
+ * - `not-wired` — no backend at all yet (the D294 window, before
19
+ * `setSettingsStore` / `setSettingsDoor`).
20
+ * - `preload-pending` — the key is one of the few the kernel loads from the
21
+ * door into memory at boot, and that one read has not settled yet.
22
+ * - `preload-failed` — that boot read failed; the value is unknown until the
23
+ * next boot (one read, no polling — D3).
24
+ * - `read-failed` — the async read itself threw.
25
+ */
26
+ export type SettingsReadRefusal = 'door-only' | 'not-wired' | 'preload-pending' | 'preload-failed' | 'read-failed';
27
+ export declare class SettingsReadUnavailableError extends Error {
28
+ /** The reader that refused, e.g. `ConfigManager.getAddonConfig`. */
29
+ readonly reader: string;
30
+ readonly reason: SettingsReadRefusal;
31
+ readonly name = "SettingsReadUnavailableError";
32
+ constructor(
33
+ /** The reader that refused, e.g. `ConfigManager.getAddonConfig`. */
34
+ reader: string, reason: SettingsReadRefusal, detail: string);
35
+ }
36
+ /** Narrow an unknown throw to {@link SettingsReadUnavailableError}. */
37
+ export declare function isSettingsReadUnavailable(err: unknown): err is SettingsReadUnavailableError;
@@ -0,0 +1,105 @@
1
+ import { AddonStoreRead, StoredKeyRead } from '@camstack/types';
2
+ export interface AddonSettingsView {
3
+ readAddonStore(): Promise<Record<string, unknown>>;
4
+ /** See `IAddonContext['settings'].readAddonStoreResult` (D651). */
5
+ readAddonStoreResult?(): Promise<AddonStoreRead>;
6
+ /** See `IAddonContext['settings'].readAddonStoreKeyResult` (D675). */
7
+ readAddonStoreKeyResult?(key: string): Promise<StoredKeyRead>;
8
+ writeAddonStore(patch: Record<string, unknown>): Promise<void>;
9
+ readDeviceStore(deviceId: number): Promise<Record<string, unknown>>;
10
+ /** See `IAddonContext['settings'].readDeviceStoreBatch` — optional there and
11
+ * here for the same reason: only the door-backed view can answer it in one
12
+ * range scan, and a caller must fall back rather than refuse. */
13
+ readDeviceStoreBatch?(deviceIds: readonly number[]): Promise<ReadonlyMap<number, Record<string, unknown>>>;
14
+ writeDeviceStore(deviceId: number, patch: Record<string, unknown>): Promise<void>;
15
+ clearDeviceStore(deviceId: number): Promise<void>;
16
+ getSection(section: string): Promise<Record<string, unknown>>;
17
+ setSection(section: string, patch: Record<string, unknown>): Promise<void>;
18
+ /**
19
+ * Per-device runtime state — separate namespace from
20
+ * `readDeviceStore` / `writeDeviceStore` (which is the addon's
21
+ * config blob: host, password, deviceCache, …). Holds mutable
22
+ * runtime signals discovered after boot: battery snapshot, sleep
23
+ * flag, last-seen timestamps. Stored under a kernel-internal
24
+ * `__device-state` namespace so the device's config schema
25
+ * never has to know about it (clean separation of operator
26
+ * intent from runtime telemetry).
27
+ *
28
+ * Patch semantics: shallow merge. Pass the FULL persistent slice
29
+ * each call (the kernel doesn't try to compute deltas) — concrete
30
+ * implementations of `DeviceRuntimeState` already coalesce in
31
+ * memory, so the kernel just persists the latest blob.
32
+ */
33
+ readDeviceRuntimeState(deviceId: number): Promise<Record<string, unknown>>;
34
+ writeDeviceRuntimeState(deviceId: number, data: Record<string, unknown>): Promise<void>;
35
+ clearDeviceRuntimeState(deviceId: number): Promise<void>;
36
+ }
37
+ /**
38
+ * A CONCRETE backend behind {@link ConfigManager.createSettingsView} — the
39
+ * sync-`ISettingsStore` one or the async-door one.
40
+ *
41
+ * `readDeviceStoreBatch` is required here and optional on
42
+ * {@link AddonSettingsView} because both of these can answer it, so the view
43
+ * the kernel hands an addon never has to decide whether the method exists.
44
+ * The optionality on the public interface stays for the implementation that
45
+ * genuinely cannot batch: a forked runner's UDS-backed view.
46
+ */
47
+ export interface SettingsViewBackend extends AddonSettingsView {
48
+ readDeviceStoreBatch(deviceIds: readonly number[]): Promise<ReadonlyMap<number, Record<string, unknown>>>;
49
+ readAddonStoreKeyResult(key: string): Promise<StoredKeyRead>;
50
+ }
51
+ export interface ISettingsStore {
52
+ getSystem(key: string): unknown;
53
+ setSystem(key: string, value: unknown): void;
54
+ getAllSystem(): Record<string, unknown>;
55
+ /** Addon-level global settings (apply to every device unless overridden). */
56
+ getAllAddon(addonId: string): Record<string, unknown>;
57
+ setAllAddon(addonId: string, config: Record<string, unknown>): void;
58
+ /** Legacy per-provider settings (used by some older wiring paths). */
59
+ getAllProvider(providerId: string): Record<string, unknown>;
60
+ setProvider(providerId: string, key: string, value: unknown): void;
61
+ /** Legacy flat per-device settings. Kept for backward compatibility
62
+ * with pre-addon wiring; new code should use the addon-device API. */
63
+ getAllDevice(deviceId: string): Record<string, unknown>;
64
+ setDevice(deviceId: string, key: string, value: unknown): void;
65
+ /**
66
+ * Multi-level addon settings: per-device overrides for a specific addon.
67
+ *
68
+ * Resolution chain when the frontend asks for the effective value of a
69
+ * field on a given device:
70
+ * 1. schema default (from `ConfigUISchema.field.default`)
71
+ * 2. `getAllAddon(addonId)` — addon global value
72
+ * 3. `getAddonDevice(addonId, devId)` — per-device override (wins)
73
+ *
74
+ * Only fields declared with `scope: 'device'` in the addon's
75
+ * `ConfigUISchema` should ever be persisted here.
76
+ */
77
+ getAddonDevice(addonId: string, deviceId: string): Record<string, unknown>;
78
+ setAddonDevice(addonId: string, deviceId: string, values: Record<string, unknown>): void;
79
+ clearAddonDevice(addonId: string, deviceId: string): void;
80
+ /**
81
+ * Per-device runtime state — the whole cap-slice map as ONE row in the
82
+ * `device-runtime-state` collection.
83
+ *
84
+ * These are separate from `getAddonDevice`/`setAddonDevice` because runtime
85
+ * state is not an addon's config: it belongs to the device, it is replaced
86
+ * wholesale on every flush, and it is written orders of magnitude more often.
87
+ * It USED to share that keyspace under a synthetic `__device-state` addonId,
88
+ * which cost one row (and one INSERT) per cap slice per flush and left the
89
+ * collection it is named for empty on every installation. The backend keeps
90
+ * reading the legacy prefix; see `SqliteSettingsBackend.getDeviceRuntimeState`.
91
+ */
92
+ getDeviceRuntimeState(deviceId: string): Record<string, unknown>;
93
+ setDeviceRuntimeState(deviceId: string, blob: Record<string, unknown>): void;
94
+ clearDeviceRuntimeState(deviceId: string): void;
95
+ }
96
+ /**
97
+ * The settings view `ConfigManager.createSettingsView` returns. Its three-way
98
+ * reads are REQUIRED here (optional on the public {@link AddonSettingsView}
99
+ * only for a forked runner's UDS view), so a caller holding a `ConfigManager`
100
+ * never has to decide whether they exist.
101
+ */
102
+ export interface KernelAddonSettingsView extends AddonSettingsView {
103
+ readAddonStoreResult(): Promise<AddonStoreRead>;
104
+ readAddonStoreKeyResult(key: string): Promise<StoredKeyRead>;
105
+ }
@@ -0,0 +1,17 @@
1
+ import { SettingsViewBackend } from './settings-view-types.js';
2
+ /** The `ConfigManager` surface this backend reads and writes through. */
3
+ export interface StoreViewHost {
4
+ hasSyncStore(): boolean;
5
+ getBootstrap(configPath: string): unknown;
6
+ getAddonConfig(addonId: string): Record<string, unknown>;
7
+ setAddonConfig(addonId: string, config: Record<string, unknown>): void;
8
+ getAddonDevice(addonId: string, deviceId: string): Record<string, unknown>;
9
+ setAddonDevice(addonId: string, deviceId: string, values: Record<string, unknown>): void;
10
+ clearAddonDevice(addonId: string, deviceId: string): void;
11
+ getDeviceRuntimeState(deviceId: string): Record<string, unknown>;
12
+ setDeviceRuntimeState(deviceId: string, blob: Record<string, unknown>): void;
13
+ clearDeviceRuntimeState(deviceId: string): void;
14
+ getSection(section: string): Record<string, unknown>;
15
+ setSection(section: string, data: Record<string, unknown>): void;
16
+ }
17
+ export declare function createStoreSettingsView(addonId: string, cm: StoreViewHost): SettingsViewBackend;
@@ -0,0 +1,34 @@
1
+ import { ISettingsStoreProvider } from '@camstack/types';
2
+ /** The outcome of the one boot read, for the caller that logs and replays it. */
3
+ export type SystemSettingsPreload = {
4
+ readonly kind: 'loaded';
5
+ readonly keyCount: number;
6
+ } | {
7
+ readonly kind: 'failed';
8
+ readonly error: string;
9
+ };
10
+ export declare class SystemSettingsMirror {
11
+ private state;
12
+ /** The door the current read belongs to; a replaced door owns the mirror. */
13
+ private door;
14
+ private preload;
15
+ /**
16
+ * Writes to preloaded keys made through THIS process. They win over the boot
17
+ * read (they are newer) and survive a failed one, so an operator's Save is
18
+ * reported back even when the boot read was lost.
19
+ */
20
+ private readonly writes;
21
+ /** Start the ONE read for `door`. Called by `ConfigManager.setSettingsDoor`. */
22
+ load(door: ISettingsStoreProvider): void;
23
+ /** The in-flight / settled boot read, or `null` before any door. */
24
+ whenLoaded(): Promise<SystemSettingsPreload> | null;
25
+ /**
26
+ * A preloaded key's value: `undefined` when the store holds none, or a THROW
27
+ * (`preload-pending` / `preload-failed`) when it is not known.
28
+ */
29
+ get(key: string): unknown;
30
+ /** Record a write to a preloaded key so the mirror reports it. */
31
+ noteWrite(key: string, value: unknown): void;
32
+ /** Every row under the preloaded prefixes plus each preloaded key. Never rejects. */
33
+ private read;
34
+ }