@camstack/system 1.2.378 → 1.2.380

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 (119) hide show
  1. package/assets/addon-pages-aggregator/icon.svg +5 -0
  2. package/assets/addon-widgets-aggregator/icon.svg +6 -0
  3. package/assets/alert-center/icon.svg +5 -0
  4. package/assets/autotrack/icon.svg +5 -0
  5. package/assets/backup-orchestrator/icon.svg +5 -0
  6. package/assets/calendars/icon.svg +5 -0
  7. package/assets/camera-grid/icon.svg +10 -0
  8. package/assets/composer/icon.svg +7 -0
  9. package/assets/console-logging/icon.svg +4 -0
  10. package/assets/core-blocks/icon.svg +6 -0
  11. package/assets/device-manager/icon.svg +6 -0
  12. package/assets/filesystem-storage/icon.svg +6 -0
  13. package/assets/hub-forwarder/icon.svg +7 -0
  14. package/assets/keychain/icon.svg +6 -0
  15. package/assets/liveness-monitor/icon.svg +4 -0
  16. package/assets/local-auth/icon.svg +5 -0
  17. package/assets/local-network/icon.svg +7 -0
  18. package/assets/loki-logging/icon.svg +7 -0
  19. package/assets/metrics-native/icon.svg +3 -0
  20. package/assets/platform-probe/icon.svg +5 -0
  21. package/assets/remote-access-orchestrator/icon.svg +5 -0
  22. package/assets/snapshot/icon.svg +4 -0
  23. package/assets/sqlite-settings/icon.svg +5 -0
  24. package/assets/storage-orchestrator/icon.svg +8 -0
  25. package/assets/system-config/icon.svg +4 -0
  26. package/assets/winston-logging/icon.svg +6 -0
  27. package/dist/addon-runner.js +1 -1
  28. package/dist/addon-runner.mjs +1 -1
  29. package/dist/addon-utils.js +1 -1
  30. package/dist/addon-utils.mjs +1 -1
  31. package/dist/auth/auth-manager.d.ts +10 -2
  32. package/dist/auth/jwt-signing-key.d.ts +42 -0
  33. package/dist/auth/user-manager.d.ts +19 -19
  34. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  35. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  36. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  37. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  38. package/dist/builtins/alerts/alerts.addon.js +1 -1
  39. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  40. package/dist/builtins/autotrack/index.js +1 -1
  41. package/dist/builtins/autotrack/index.mjs +1 -1
  42. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  43. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  44. package/dist/builtins/calendars/calendars.addon.js +1 -1
  45. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  46. package/dist/builtins/camera-grid/index.js +1 -1
  47. package/dist/builtins/camera-grid/index.mjs +1 -1
  48. package/dist/builtins/composer/composer.addon.js +2 -2
  49. package/dist/builtins/composer/composer.addon.mjs +2 -2
  50. package/dist/builtins/console-logging/index.js +1 -1
  51. package/dist/builtins/console-logging/index.mjs +1 -1
  52. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  53. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  54. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  55. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  56. package/dist/builtins/hub-forwarder/index.js +1 -1
  57. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  58. package/dist/builtins/keychain/index.js +34 -4
  59. package/dist/builtins/keychain/index.mjs +34 -4
  60. package/dist/builtins/keychain/keychain-service.d.ts +14 -2
  61. package/dist/builtins/keychain/system-held-secrets.d.ts +11 -0
  62. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  63. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  64. package/dist/builtins/local-auth/admin-account-boot.d.ts +1 -1
  65. package/dist/builtins/local-auth/auth-config-reader.d.ts +9 -7
  66. package/dist/builtins/local-auth/jwt-signing-key-keeper.d.ts +30 -0
  67. package/dist/builtins/local-auth/local-auth.addon.d.ts +2 -6
  68. package/dist/builtins/local-auth/local-auth.addon.js +127 -221
  69. package/dist/builtins/local-auth/local-auth.addon.mjs +126 -220
  70. package/dist/builtins/local-network/local-network.addon.js +1 -1
  71. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  72. package/dist/builtins/loki-logging/index.js +1 -1
  73. package/dist/builtins/loki-logging/index.mjs +1 -1
  74. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  75. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  76. package/dist/builtins/platform-probe/index.js +1 -1
  77. package/dist/builtins/platform-probe/index.mjs +1 -1
  78. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  79. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  80. package/dist/builtins/snapshot/index.js +1 -1
  81. package/dist/builtins/snapshot/index.mjs +1 -1
  82. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  83. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  84. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  85. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  86. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  87. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  88. package/dist/builtins/system-config/cluster-secret-keeper.d.ts +36 -0
  89. package/dist/builtins/system-config/system-config.addon.d.ts +13 -0
  90. package/dist/builtins/system-config/system-config.addon.js +244 -53
  91. package/dist/builtins/system-config/system-config.addon.mjs +244 -54
  92. package/dist/builtins/winston-logging/index.js +1 -1
  93. package/dist/builtins/winston-logging/index.mjs +1 -1
  94. package/dist/{child-cap-dispatch-C9lmrWFT.js → child-cap-dispatch-CZeRUgYF.js} +1 -1
  95. package/dist/{child-cap-dispatch-DpHovtQd.mjs → child-cap-dispatch-CpjohToR.mjs} +1 -1
  96. package/dist/cluster-secret-BDue-uJj.mjs +83 -0
  97. package/dist/cluster-secret-Dg0OOOSr.js +148 -0
  98. package/dist/{composition-sources-CxhGdtdh.js → composition-sources-CHvLMrFI.js} +1 -1
  99. package/dist/{composition-sources-BLBd338N.mjs → composition-sources-DT_uJwI3.mjs} +1 -1
  100. package/dist/{dist-Bvl5JgNt.mjs → dist-B9oFw2jh.mjs} +58 -2
  101. package/dist/{dist-KxsOMQik.js → dist-Cc6KOdY-.js} +69 -1
  102. package/dist/{event-loop-stall-monitor-D8rqSWfW.mjs → event-loop-stall-monitor-anUH2IE1.mjs} +14 -14
  103. package/dist/{event-loop-stall-monitor-CbSmCWkp.js → event-loop-stall-monitor-mk6WGxir.js} +14 -14
  104. package/dist/index.d.ts +4 -2
  105. package/dist/index.js +192 -62
  106. package/dist/index.mjs +171 -55
  107. package/dist/jwt-signing-key-C2rE3wH6.js +174 -0
  108. package/dist/jwt-signing-key-Qhde8r82.mjs +127 -0
  109. package/dist/kernel/config-manager.d.ts +1 -1
  110. package/dist/kernel/config-schema.d.ts +4 -11
  111. package/dist/kernel/index.d.ts +2 -1
  112. package/dist/kernel/moleculer/broker-factory.d.ts +0 -2
  113. package/dist/kernel/moleculer/cluster-secret.d.ts +31 -0
  114. package/dist/kernel/moleculer/hub-service.d.ts +7 -4
  115. package/dist/kernel/moleculer/node-registry.d.ts +1 -1
  116. package/dist/kernel/system-settings-policy.d.ts +7 -9
  117. package/dist/{retired-settings-keys-BAcfvFtm.mjs → retired-settings-keys-DlZ3VV6x.mjs} +1 -1
  118. package/dist/{retired-settings-keys-gTsZfpmP.js → retired-settings-keys-YOOKLA3v.js} +1 -1
  119. package/package.json +55 -3
@@ -1,4 +1,4 @@
1
- import { Ar as isSecretMarker, B as StorageLocationModeSchema, Bt as isLocationEnabled, D as LegacyStorageLocationDefaultSchema, Dt as evictionPolicyOfLocation, Fn as storageMigrationCapability, Gr as EventCategory, H as StorageMigrationJobSchema, Kn as withLocationMode, Nr as parseJsonObject, Pn as storageCapability, Pt as gbToBytes, Rr as resolveSecretMarkers, V as StorageLocationTypeSchema, Vr as secretTreeFromPaths, Wr as transformSecretWrite, an as modeMayWrite, br as collectSecretConfigKeys, jn as settingsStoreCapability, lr as BaseAddon, nn as mayWriteToLocation, pr as REDACTED_SECRET, qt as legacyModeOf, tn as mayReadLocation, wn as resolveLocationMode, z as StorageCleanupJobSchema } from "../../dist-Bvl5JgNt.mjs";
1
+ import { B as StorageLocationModeSchema, Bt as isLocationEnabled, D as LegacyStorageLocationDefaultSchema, Dt as evictionPolicyOfLocation, Fn as storageMigrationCapability, H as StorageMigrationJobSchema, Hr as secretTreeFromPaths, Kn as withLocationMode, Kr as transformSecretWrite, Pn as storageCapability, Pr as parseJsonObject, Pt as gbToBytes, V as StorageLocationTypeSchema, an as modeMayWrite, jn as settingsStoreCapability, jr as isSecretMarker, lr as BaseAddon, nn as mayWriteToLocation, pr as REDACTED_SECRET, qr as EventCategory, qt as legacyModeOf, tn as mayReadLocation, wn as resolveLocationMode, xr as collectSecretConfigKeys, z as StorageCleanupJobSchema, zr as resolveSecretMarkers } from "../../dist-B9oFw2jh.mjs";
2
2
  import { t as HUB_CAP_FWD_ACTION } from "../../hub-cap-forward-CH6_VWVO.mjs";
3
3
  import { z } from "zod";
4
4
  import { randomUUID } from "node:crypto";
@@ -0,0 +1,36 @@
1
+ import { SecretsAccess } from '@camstack/types';
2
+ /** The field key inside the `cluster` section. */
3
+ export declare const CLUSTER_SECRET_FIELD = "clusterSecret";
4
+ export declare const CLUSTER_SECRET_RETRY_MS = 30000;
5
+ interface KeeperLogger {
6
+ info(message: string, meta?: {
7
+ meta?: Record<string, unknown>;
8
+ }): void;
9
+ error(message: string, meta?: {
10
+ meta?: Record<string, unknown>;
11
+ }): void;
12
+ }
13
+ export interface ClusterSecretKeeperDeps {
14
+ /** Keychain handle for owner `system`; `null` without a keychain door. */
15
+ readonly secrets: SecretsAccess | null;
16
+ /** The stored `cluster` section (markers, never values). Throws when unreadable. */
17
+ readonly readSection: () => Promise<Record<string, unknown>>;
18
+ readonly logger: KeeperLogger;
19
+ readonly retryMs?: number;
20
+ }
21
+ export declare class ClusterSecretKeeper {
22
+ private readonly deps;
23
+ private retryTimer;
24
+ private unsubscribe;
25
+ private stopped;
26
+ private last;
27
+ constructor(deps: ClusterSecretKeeperDeps);
28
+ start(): Promise<void>;
29
+ stop(): void;
30
+ /** Re-read the row and the keychain; publish the gate. */
31
+ reload(): Promise<void>;
32
+ private compute;
33
+ private report;
34
+ private scheduleRetry;
35
+ }
36
+ export {};
@@ -1,10 +1,23 @@
1
1
  import { ConfigUISchemaWithValues, BaseAddon } from '@camstack/types';
2
+ /** The section id of the cluster form — System → Cluster renders only this one. */
3
+ export declare const SYSTEM_CONFIG_CLUSTER_SECTION_ID = "system-config-cluster";
2
4
  export declare class SystemConfigAddon extends BaseAddon {
3
5
  readonly id = "system-config";
6
+ private clusterSecretKeeper;
4
7
  constructor();
5
8
  protected onInitialize(): Promise<void>;
9
+ protected onShutdown(): Promise<void>;
10
+ /** The keychain handle for owner `system`; `null` where there is no door. */
11
+ private systemSecrets;
6
12
  private buildGlobalSchema;
7
13
  getGlobalSettings(): Promise<ConfigUISchemaWithValues>;
8
14
  updateGlobalSettings(patch: Record<string, unknown>): Promise<void>;
15
+ /**
16
+ * The cluster secret's save path: the value goes to the keychain (owner
17
+ * `system`), the row gets the marker; `''` / a view keeps, `$secretClear`
18
+ * clears. A value the keychain cannot store is REFUSED — never written
19
+ * inline (D787, P5).
20
+ */
21
+ private writeClusterSection;
9
22
  }
10
23
  export default SystemConfigAddon;
@@ -3,7 +3,129 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-KxsOMQik.js");
6
+ const require_dist = require("../../dist-Cc6KOdY-.js");
7
+ const require_cluster_secret = require("../../cluster-secret-Dg0OOOSr.js");
8
+ //#region src/builtins/system-config/cluster-secret-keeper.ts
9
+ /**
10
+ * Keeps the hub's cluster-secret gate in step with System → Cluster (D797).
11
+ *
12
+ * The field is a `password` of `system-config`: its `system-settings` row
13
+ * (`cluster.clusterSecret`) holds a keychain marker, the value lives in the
14
+ * keychain (owner `system`). The gate the `$hub.registerNode` handler reads:
15
+ *
16
+ * - no marker → `off` — every node may register, exactly as a hub
17
+ * that never had a secret;
18
+ * - marker + value → `on` — nodes present the matching hash;
19
+ * - marker, no answer → `unknown` — refused, retryably, and retried here.
20
+ * A locked keychain or a value lost to a reset NEVER opens the gate.
21
+ */
22
+ /** The field key inside the `cluster` section. */
23
+ var CLUSTER_SECRET_FIELD = "clusterSecret";
24
+ var ClusterSecretKeeper = class {
25
+ deps;
26
+ retryTimer = null;
27
+ unsubscribe = null;
28
+ stopped = false;
29
+ last = null;
30
+ constructor(deps) {
31
+ this.deps = deps;
32
+ }
33
+ async start() {
34
+ this.unsubscribe = this.deps.secrets?.onChange((ref) => {
35
+ if (ref === require_cluster_secret.CLUSTER_SECRET_REF) this.reload();
36
+ }) ?? null;
37
+ await this.reload();
38
+ }
39
+ stop() {
40
+ this.stopped = true;
41
+ if (this.retryTimer !== null) clearTimeout(this.retryTimer);
42
+ this.retryTimer = null;
43
+ this.unsubscribe?.();
44
+ this.unsubscribe = null;
45
+ }
46
+ /** Re-read the row and the keychain; publish the gate. */
47
+ async reload() {
48
+ if (this.stopped) return;
49
+ const gate = await this.compute();
50
+ if (this.stopped) return;
51
+ require_cluster_secret.setProcessClusterSecretGate(gate);
52
+ this.report(gate);
53
+ if (gate.kind === "unknown") this.scheduleRetry();
54
+ }
55
+ async compute() {
56
+ let stored;
57
+ try {
58
+ stored = (await this.deps.readSection())[CLUSTER_SECRET_FIELD];
59
+ } catch (err) {
60
+ return {
61
+ kind: "unknown",
62
+ reason: `settings unreadable: ${errText(err)}`
63
+ };
64
+ }
65
+ if (stored === void 0 || stored === null || stored === "") return { kind: "off" };
66
+ if (!require_dist.isSecretMarker(stored)) return {
67
+ kind: "unknown",
68
+ reason: "stored inline, not in the keychain — set it again in System → Cluster"
69
+ };
70
+ const secrets = this.deps.secrets;
71
+ if (secrets === null) return {
72
+ kind: "unknown",
73
+ reason: "no keychain door in this process"
74
+ };
75
+ let entry;
76
+ try {
77
+ entry = (await secrets.resolve([require_cluster_secret.CLUSTER_SECRET_REF]))[require_cluster_secret.CLUSTER_SECRET_REF];
78
+ } catch (err) {
79
+ return {
80
+ kind: "unknown",
81
+ reason: errText(err)
82
+ };
83
+ }
84
+ if (entry === void 0) return {
85
+ kind: "unknown",
86
+ reason: "no-answer"
87
+ };
88
+ switch (entry.kind) {
89
+ case "value": return {
90
+ kind: "on",
91
+ hash: require_cluster_secret.hashClusterSecret(entry.value)
92
+ };
93
+ case "unavailable": return {
94
+ kind: "unknown",
95
+ reason: entry.reason
96
+ };
97
+ case "missing": return {
98
+ kind: "unknown",
99
+ reason: "the stored value is gone — set it again in System → Cluster"
100
+ };
101
+ case "forbidden": return {
102
+ kind: "unknown",
103
+ reason: "forbidden"
104
+ };
105
+ }
106
+ }
107
+ report(gate) {
108
+ const changed = this.last !== gate.kind;
109
+ this.last = gate.kind;
110
+ if (gate.kind === "unknown") {
111
+ this.deps.logger.error("Agents cannot register: a cluster secret is set but cannot be read from the keychain. Registrations are refused until it can (no fallback).", { meta: { reason: gate.reason } });
112
+ return;
113
+ }
114
+ if (changed) this.deps.logger.info(gate.kind === "on" ? "Cluster secret set — agents must present it to register" : "No cluster secret set — any node may register");
115
+ }
116
+ scheduleRetry() {
117
+ if (this.stopped || this.retryTimer !== null) return;
118
+ this.retryTimer = setTimeout(() => {
119
+ this.retryTimer = null;
120
+ this.reload();
121
+ }, this.deps.retryMs ?? 3e4);
122
+ this.retryTimer.unref?.();
123
+ }
124
+ };
125
+ function errText(err) {
126
+ return err instanceof Error ? err.message : String(err);
127
+ }
128
+ //#endregion
7
129
  //#region src/builtins/system-config/system-config.addon.ts
8
130
  /**
9
131
  * Built-in `system-config` addon — Phase 4 of the settings redesign.
@@ -17,6 +139,9 @@ const require_dist = require("../../dist-KxsOMQik.js");
17
139
  * Sections exposed — this list is the whole of it:
18
140
  * - server (port, host, dataPath — READ-ONLY, loaded from config.yaml at bootstrap)
19
141
  * - auth (tokenExpiry)
142
+ * - cluster (clusterSecret — a `password` field: the value goes to the
143
+ * keychain, owner `system`; the row keeps a marker. Also drawn
144
+ * on System → Cluster. D797)
20
145
  *
21
146
  * Sections NOT exposed (each owned by the addon that implements them):
22
147
  * - logging → `winston-logging` addon (level + retention)
@@ -45,75 +170,112 @@ const require_dist = require("../../dist-KxsOMQik.js");
45
170
  /** Section key → user-facing section title. */
46
171
  var SECTION_TITLES = {
47
172
  server: "Server",
48
- auth: "Authentication"
173
+ auth: "Authentication",
174
+ cluster: "Cluster"
49
175
  };
176
+ /** The keychain owner of the hub's own secrets (D797). */
177
+ var SYSTEM_SECRET_OWNER = "system";
50
178
  /** Keys that map 1:1 to a given yml section — used by `updateGlobalSettings`
51
179
  * to figure out which section owns each key in the incoming patch. */
52
180
  var KEY_TO_SECTION = {
53
181
  port: "server",
54
182
  host: "server",
55
183
  dataPath: "server",
56
- tokenExpiry: "auth"
184
+ tokenExpiry: "auth",
185
+ [CLUSTER_SECRET_FIELD]: "cluster"
57
186
  };
187
+ /** The section id of the cluster form — System → Cluster renders only this one. */
188
+ var SYSTEM_CONFIG_CLUSTER_SECTION_ID = "system-config-cluster";
58
189
  var SystemConfigAddon = class extends require_dist.BaseAddon {
59
190
  id = "system-config";
191
+ clusterSecretKeeper = null;
60
192
  constructor() {
61
193
  super({});
62
194
  }
63
195
  async onInitialize() {
196
+ this.clusterSecretKeeper = new ClusterSecretKeeper({
197
+ secrets: this.systemSecrets(),
198
+ readSection: async () => await this.ctx.settings?.getSection("cluster") ?? {},
199
+ logger: this.ctx.logger
200
+ });
201
+ await this.clusterSecretKeeper.start();
64
202
  this.ctx.logger.info("Initialized — exposes yml-backed sections via getGlobalSettings");
65
203
  }
204
+ async onShutdown() {
205
+ this.clusterSecretKeeper?.stop();
206
+ this.clusterSecretKeeper = null;
207
+ }
208
+ /** The keychain handle for owner `system`; `null` where there is no door. */
209
+ systemSecrets() {
210
+ const forOwner = this.ctx.secrets?.forOwner ?? null;
211
+ return forOwner === null ? null : forOwner(SYSTEM_SECRET_OWNER);
212
+ }
66
213
  buildGlobalSchema() {
67
- return { sections: [{
68
- id: "system-config-server",
69
- title: SECTION_TITLES["server"],
70
- description: "Core server connection settings — read-only, loaded from config.yaml at bootstrap.",
71
- columns: 2,
72
- fields: [
73
- {
74
- type: "info",
75
- key: "server-restart-note",
76
- label: "Restart required",
77
- content: "Server settings are read-only. Change them in config.yaml and restart.",
78
- variant: "warning"
79
- },
80
- {
214
+ return { sections: [
215
+ {
216
+ id: "system-config-server",
217
+ title: SECTION_TITLES["server"],
218
+ description: "Core server connection settings — read-only, loaded from config.yaml at bootstrap.",
219
+ columns: 2,
220
+ fields: [
221
+ {
222
+ type: "info",
223
+ key: "server-restart-note",
224
+ label: "Restart required",
225
+ content: "Server settings are read-only. Change them in config.yaml and restart.",
226
+ variant: "warning"
227
+ },
228
+ {
229
+ type: "text",
230
+ key: "port",
231
+ label: "Port",
232
+ description: "Listening port",
233
+ disabled: true
234
+ },
235
+ {
236
+ type: "text",
237
+ key: "host",
238
+ label: "Host",
239
+ description: "Bind address",
240
+ disabled: true
241
+ },
242
+ {
243
+ type: "text",
244
+ key: "dataPath",
245
+ label: "Data Path",
246
+ description: "Root data directory",
247
+ disabled: true,
248
+ span: 2
249
+ }
250
+ ]
251
+ },
252
+ {
253
+ id: "system-config-auth",
254
+ title: SECTION_TITLES["auth"],
255
+ description: "Token and session settings.",
256
+ columns: 1,
257
+ fields: [{
81
258
  type: "text",
82
- key: "port",
83
- label: "Port",
84
- description: "Listening port",
85
- disabled: true
86
- },
87
- {
88
- type: "text",
89
- key: "host",
90
- label: "Host",
91
- description: "Bind address",
92
- disabled: true
93
- },
94
- {
95
- type: "text",
96
- key: "dataPath",
97
- label: "Data Path",
98
- description: "Root data directory",
99
- disabled: true,
100
- span: 2
101
- }
102
- ]
103
- }, {
104
- id: "system-config-auth",
105
- title: SECTION_TITLES["auth"],
106
- description: "Token and session settings.",
107
- columns: 1,
108
- fields: [{
109
- type: "text",
110
- key: "tokenExpiry",
111
- label: "Token Expiry",
112
- description: "Lifetime of a session token, as a duration string (e.g. 24h, 7d, 30d). A bare number is rejected — it would be read as milliseconds. Applies to tokens issued from the next login or silent refresh; tokens already handed out keep the lifetime they were signed with.",
113
- placeholder: "30d",
114
- default: "30d"
115
- }]
116
- }] };
259
+ key: "tokenExpiry",
260
+ label: "Token Expiry",
261
+ description: "Lifetime of a session token, as a duration string (e.g. 24h, 7d, 30d). A bare number is rejected — it would be read as milliseconds. Applies to tokens issued from the next login or silent refresh; tokens already handed out keep the lifetime they were signed with.",
262
+ placeholder: "30d",
263
+ default: "30d"
264
+ }]
265
+ },
266
+ {
267
+ id: SYSTEM_CONFIG_CLUSTER_SECTION_ID,
268
+ title: SECTION_TITLES["cluster"],
269
+ description: "Agents joining this hub. Optional: with no cluster secret, any node that reaches the hub may register.",
270
+ columns: 1,
271
+ fields: [{
272
+ type: "password",
273
+ key: CLUSTER_SECRET_FIELD,
274
+ label: "Cluster secret",
275
+ description: "When set, an agent must present it to register. Stored in the keychain. To pair an agent: Reveal and copy it here, then enter it in the agent at first run (its status page or the desktop setup window). Changing it disconnects agents that still hold the old one at their next registration."
276
+ }]
277
+ }
278
+ ] };
117
279
  }
118
280
  async getGlobalSettings() {
119
281
  const schema = this.buildGlobalSchema();
@@ -130,7 +292,7 @@ var SystemConfigAddon = class extends require_dist.BaseAddon {
130
292
  } });
131
293
  throw err;
132
294
  }
133
- return require_dist.hydrateSchema(schema, merged);
295
+ return require_dist.hydrateSchema(schema, merged, { inlineSecretsAsViews: true });
134
296
  }
135
297
  async updateGlobalSettings(patch) {
136
298
  if (!this.ctx.settings) throw new Error("system-config: ctx.settings not available — cannot write sections");
@@ -153,6 +315,10 @@ var SystemConfigAddon = class extends require_dist.BaseAddon {
153
315
  bucket[key] = value;
154
316
  }
155
317
  for (const [section, sectionPatch] of perSection) {
318
+ if (section === "cluster") {
319
+ await this.writeClusterSection(sectionPatch);
320
+ continue;
321
+ }
156
322
  await this.ctx.settings.setSection(section, sectionPatch);
157
323
  this.ctx.logger.info("Wrote keys to section", { meta: {
158
324
  section,
@@ -160,7 +326,32 @@ var SystemConfigAddon = class extends require_dist.BaseAddon {
160
326
  } });
161
327
  }
162
328
  }
329
+ /**
330
+ * The cluster secret's save path: the value goes to the keychain (owner
331
+ * `system`), the row gets the marker; `''` / a view keeps, `$secretClear`
332
+ * clears. A value the keychain cannot store is REFUSED — never written
333
+ * inline (D787, P5).
334
+ */
335
+ async writeClusterSection(patch) {
336
+ const settings = this.ctx.settings;
337
+ const secrets = this.systemSecrets();
338
+ if (secrets === null) throw new Error("system-config: no keychain door — the cluster secret cannot be stored");
339
+ const stored = await settings.getSection("cluster");
340
+ const result = await require_dist.transformSecretWrite({
341
+ tree: require_dist.secretTreeFromSchema(this.buildGlobalSchema()),
342
+ secrets,
343
+ patch,
344
+ stored,
345
+ operatorWrite: true
346
+ });
347
+ if (Object.keys(result.patch).length > 0) await settings.setSection("cluster", result.patch);
348
+ const { orphaned } = await result.afterWrite();
349
+ if (orphaned.length > 0) this.ctx.logger.warn("cluster secret cleared, but its old keychain value could not be deleted", { meta: { refs: orphaned } });
350
+ this.ctx.logger.info("Cluster secret updated");
351
+ await this.clusterSecretKeeper?.reload();
352
+ }
163
353
  };
164
354
  //#endregion
355
+ exports.SYSTEM_CONFIG_CLUSTER_SECTION_ID = SYSTEM_CONFIG_CLUSTER_SECTION_ID;
165
356
  exports.SystemConfigAddon = SystemConfigAddon;
166
357
  exports.default = SystemConfigAddon;