@camstack/system 1.2.381 → 1.2.382

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 (114) hide show
  1. package/dist/addon-runner.js +4 -2
  2. package/dist/addon-runner.mjs +4 -2
  3. package/dist/addon-utils.js +1 -1
  4. package/dist/addon-utils.mjs +1 -1
  5. package/dist/auth/admin-token-manager.d.ts +75 -0
  6. package/dist/auth/api-key-manager.d.ts +1 -14
  7. package/dist/auth/auth-audit-log.d.ts +59 -0
  8. package/dist/auth/auth-manager.d.ts +28 -16
  9. package/dist/auth/cidr.d.ts +22 -0
  10. package/dist/auth/login-session-manager.d.ts +158 -0
  11. package/dist/auth/user-management-internal.d.ts +69 -0
  12. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  13. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  14. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  15. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  16. package/dist/builtins/alerts/alerts.addon.js +1 -1
  17. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  18. package/dist/builtins/autotrack/index.js +1 -1
  19. package/dist/builtins/autotrack/index.mjs +1 -1
  20. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.d.ts +14 -0
  21. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +291 -3631
  22. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +284 -3617
  23. package/dist/builtins/backup-orchestrator/encryption-key-store.d.ts +36 -0
  24. package/dist/builtins/backup-orchestrator/manifest-store.d.ts +19 -1
  25. package/dist/builtins/backup-orchestrator/restore-archive.d.ts +24 -0
  26. package/dist/builtins/calendars/calendars.addon.js +1 -1
  27. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  28. package/dist/builtins/camera-grid/index.js +1 -1
  29. package/dist/builtins/camera-grid/index.mjs +1 -1
  30. package/dist/builtins/composer/composer.addon.js +2 -2
  31. package/dist/builtins/composer/composer.addon.mjs +2 -2
  32. package/dist/builtins/console-logging/index.js +1 -1
  33. package/dist/builtins/console-logging/index.mjs +1 -1
  34. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  35. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  36. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  37. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  38. package/dist/builtins/hub-forwarder/index.js +1 -1
  39. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  40. package/dist/builtins/keychain/index.js +1108 -733
  41. package/dist/builtins/keychain/index.mjs +1108 -733
  42. package/dist/builtins/keychain/keychain-reset-trigger.d.ts +25 -0
  43. package/dist/builtins/keychain/keychain-service.d.ts +36 -3
  44. package/dist/builtins/keychain/keychain-store.d.ts +24 -1
  45. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  46. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  47. package/dist/builtins/local-auth/local-auth.addon.d.ts +16 -0
  48. package/dist/builtins/local-auth/local-auth.addon.js +1021 -118
  49. package/dist/builtins/local-auth/local-auth.addon.mjs +1034 -141
  50. package/dist/builtins/local-network/local-network.addon.js +3 -3
  51. package/dist/builtins/local-network/local-network.addon.mjs +3 -3
  52. package/dist/builtins/loki-logging/index.js +97 -29
  53. package/dist/builtins/loki-logging/index.mjs +97 -29
  54. package/dist/builtins/loki-logging/loki-logging.addon.d.ts +7 -23
  55. package/dist/builtins/loki-logging/retire-enabled-flag.d.ts +23 -0
  56. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  57. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  58. package/dist/builtins/platform-probe/index.js +1 -1
  59. package/dist/builtins/platform-probe/index.mjs +1 -1
  60. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  61. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  62. package/dist/builtins/snapshot/index.js +1 -1
  63. package/dist/builtins/snapshot/index.mjs +1 -1
  64. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  65. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  66. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  67. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  68. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +2 -2
  69. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +2 -2
  70. package/dist/builtins/system-backup/backup-encryption.d.ts +57 -0
  71. package/dist/builtins/system-backup/system-backup.service.d.ts +16 -1
  72. package/dist/builtins/system-config/cluster-secret-keeper.d.ts +7 -0
  73. package/dist/builtins/system-config/system-config.addon.d.ts +13 -3
  74. package/dist/builtins/system-config/system-config.addon.js +324 -38
  75. package/dist/builtins/system-config/system-config.addon.mjs +312 -39
  76. package/dist/builtins/winston-logging/index.js +1 -1
  77. package/dist/builtins/winston-logging/index.mjs +1 -1
  78. package/dist/{child-cap-dispatch-hf-Gce_4.js → child-cap-dispatch-BrMdKHfp.js} +6 -3
  79. package/dist/{child-cap-dispatch-BTe4eKep.mjs → child-cap-dispatch-WsYPNvnK.mjs} +10 -7
  80. package/dist/{cluster-secret-BUiG8DCo.js → cluster-secret-BEPLh3LL.js} +1 -1
  81. package/dist/{cluster-secret-DgJTZ4qY.mjs → cluster-secret-DhPdZ0VA.mjs} +1 -1
  82. package/dist/{composition-sources-4SmsP0O4.js → composition-sources-C6ZynCZ2.js} +1 -1
  83. package/dist/{composition-sources-CgOQVIwa.mjs → composition-sources-XqRX51db.mjs} +1 -1
  84. package/dist/{dist-BC0GnGeg.mjs → dist-CP5TTjwd.mjs} +1710 -158
  85. package/dist/{dist-BugrtARh.js → dist-tnAO12ak.js} +1721 -157
  86. package/dist/{event-loop-stall-monitor-CnQ-K6Gn.js → event-loop-stall-monitor-9kR0sAPS.js} +15 -15
  87. package/dist/{event-loop-stall-monitor-GgpnfAC0.mjs → event-loop-stall-monitor-WkssIXjq.mjs} +15 -15
  88. package/dist/{hub-cap-forward-CH6_VWVO.mjs → hub-cap-forward-CDiN1p98.mjs} +18 -2
  89. package/dist/{hub-cap-forward-COCmYaup.js → hub-cap-forward-W8oEm4ME.js} +23 -1
  90. package/dist/index.d.ts +16 -1
  91. package/dist/index.js +3341 -79
  92. package/dist/index.mjs +3567 -354
  93. package/dist/{jwt-signing-key-CsuVKA1W.mjs → jwt-signing-key-CAbozwvF.mjs} +1 -1
  94. package/dist/{jwt-signing-key-CjoZcol8.js → jwt-signing-key-Dgf8sIXY.js} +1 -1
  95. package/dist/kernel/index.d.ts +12 -0
  96. package/dist/kernel/moleculer/broker-factory.d.ts +8 -1
  97. package/dist/kernel/moleculer/child-addon-call-dispatch.d.ts +1 -0
  98. package/dist/kernel/moleculer/hub-service.d.ts +9 -0
  99. package/dist/kernel/moleculer/mesh-access-middleware.d.ts +28 -0
  100. package/dist/kernel/moleculer/mesh-auth-mode.d.ts +64 -0
  101. package/dist/kernel/moleculer/mesh-authenticator.d.ts +71 -0
  102. package/dist/kernel/moleculer/mesh-auto-enforce.d.ts +26 -0
  103. package/dist/kernel/moleculer/mesh-join-client.d.ts +51 -0
  104. package/dist/kernel/moleculer/mesh-join-proof.d.ts +39 -0
  105. package/dist/kernel/system-settings-policy.d.ts +4 -3
  106. package/dist/kernel/transport/hub-cap-forward.d.ts +14 -3
  107. package/dist/kernel/transport/index.d.ts +1 -1
  108. package/dist/restore-archive-BV39nY46.mjs +4045 -0
  109. package/dist/restore-archive-CbvrfPuB.js +4139 -0
  110. package/dist/{retired-settings-keys-DCuetwOt.js → retired-settings-keys-4bHpL-0m.js} +1 -1
  111. package/dist/{retired-settings-keys-MCVhxPkJ.mjs → retired-settings-keys-CzpIkwS-.mjs} +1 -1
  112. package/dist/{tls-CJHogwTl.mjs → tls-4cRH24Id.mjs} +3 -3
  113. package/package.json +1 -1
  114. package/dist/{tls-CCMX7MjX.js → tls-CljF057D.js} +1 -1
@@ -1,4 +1,5 @@
1
1
  import { ArchiveSqliteSnapshot, DatabaseSnapshotResult, IScopedLogger } from '@camstack/types';
2
+ import { BACKUP_ENCRYPTION_SCHEME } from './backup-encryption.js';
2
3
  /**
3
4
  * Thrown when an archive build (or a backup run driving one) is
4
5
  * stopped via its `AbortSignal`. Callers use `instanceof` to tell a
@@ -94,11 +95,19 @@ export interface ArchiveManifest {
94
95
  */
95
96
  sqliteSnapshots?: ArchiveSqliteSnapshot[];
96
97
  }
98
+ /** How an archive was encrypted — absent on a plain `.tar.gz`. */
99
+ export interface ArchiveEncryption {
100
+ readonly scheme: typeof BACKUP_ENCRYPTION_SCHEME;
101
+ /** The backup key the archive needs to be restored. */
102
+ readonly keyId: string;
103
+ }
97
104
  export interface CreateArchiveResult {
98
105
  archivePath: string;
99
106
  includedLocations: readonly string[];
100
- /** Archive size in bytes (compressed, on disk). */
107
+ /** Archive size in bytes (compressed — and encrypted, when it is — on disk). */
101
108
  sizeBytes: number;
109
+ /** Present when the archive is encrypted to a backup key. */
110
+ encryption?: ArchiveEncryption;
102
111
  /** Embedded manifest — same shape as `getArchiveManifest()`. */
103
112
  manifest: ArchiveManifest;
104
113
  }
@@ -197,6 +206,12 @@ export declare class SystemBackupService {
197
206
  * because its upload prunes an older archive that HAS it.
198
207
  */
199
208
  owner?: DatabaseOwner;
209
+ /**
210
+ * A backup PUBLIC key: the gzip stream is encrypted to it as it is
211
+ * written, so no plaintext archive ever touches the staging disk. Parsed
212
+ * before any work — a bad key builds nothing.
213
+ */
214
+ encryptTo?: string;
200
215
  }): Promise<CreateArchiveResult>;
201
216
  /**
202
217
  * Tar `present` (filtered by `excluded`) plus the staged manifest into
@@ -17,6 +17,12 @@ export interface ClusterSecretKeeperDeps {
17
17
  readonly readSection: () => Promise<Record<string, unknown>>;
18
18
  readonly logger: KeeperLogger;
19
19
  readonly retryMs?: number;
20
+ /**
21
+ * Generate and store a new secret (D802). Called when no marker is stored.
22
+ * Throws when it cannot (no keychain, locked): the keeper retries on its
23
+ * timer. Absent → nothing is generated (tests of the D797 states).
24
+ */
25
+ readonly generateSecret?: () => Promise<void>;
20
26
  }
21
27
  export declare class ClusterSecretKeeper {
22
28
  private readonly deps;
@@ -29,6 +35,7 @@ export declare class ClusterSecretKeeper {
29
35
  stop(): void;
30
36
  /** Re-read the row and the keychain; publish the gate. */
31
37
  reload(): Promise<void>;
38
+ private generate;
32
39
  private compute;
33
40
  private report;
34
41
  private scheduleRetry;
@@ -14,10 +14,20 @@ export declare class SystemConfigAddon extends BaseAddon {
14
14
  updateGlobalSettings(patch: Record<string, unknown>): Promise<void>;
15
15
  /**
16
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).
17
+ * `system`), the row gets the marker; `''` / a view keeps. `$secretClear` is
18
+ * REFUSED: the secret is mandatory (D802). A value the keychain cannot store
19
+ * is REFUSED — never written inline (D787, P5). `meshAuth` is a plain value.
20
20
  */
21
21
  private writeClusterSection;
22
+ /** `cluster.meshAuth` (D802), then republish it. */
23
+ private writeMeshAuthMode;
24
+ /**
25
+ * First boot, or a keychain reset that cleared the marker (D802): a fresh
26
+ * random secret, stored like an operator save. A NEW secret means every
27
+ * agent must be given it, so the mesh goes (back) to `transition`; on a hub
28
+ * with no agent yet, the hub's auto-enforce ends it at once.
29
+ */
30
+ private generateClusterSecret;
31
+ private storeClusterSecret;
22
32
  }
23
33
  export default SystemConfigAddon;
@@ -3,21 +3,154 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BugrtARh.js");
7
- const require_cluster_secret = require("../../cluster-secret-BUiG8DCo.js");
6
+ const require_dist = require("../../dist-tnAO12ak.js");
7
+ const require_cluster_secret = require("../../cluster-secret-BEPLh3LL.js");
8
+ let node_crypto = require("node:crypto");
9
+ let node_net = require("node:net");
10
+ //#region src/auth/cidr.ts
11
+ /**
12
+ * CIDR lists for the auth settings that name networks: the trusted reverse
13
+ * proxies whose forwarding headers the hub believes, and the trusted local
14
+ * networks a login may skip its second factor from.
15
+ *
16
+ * Built on `net.BlockList`, so IPv4 and IPv6 are matched by the platform, not
17
+ * by string prefixes. A bare address is read as a single host. Anything that
18
+ * does not parse is REPORTED back (`invalid`) so the settings form can refuse
19
+ * it — a list that silently dropped a typo would quietly trust less than the
20
+ * operator wrote, and one that guessed would trust more.
21
+ */
22
+ /** Strip an IPv4-mapped IPv6 prefix and a zone id, so ranges see a clean address. */
23
+ function normalizeIp(ip) {
24
+ let out = ip.trim();
25
+ if (out.toLowerCase().startsWith("::ffff:") && (0, node_net.isIP)(out.slice(7)) === 4) out = out.slice(7);
26
+ const pct = out.indexOf("%");
27
+ if (pct !== -1) out = out.slice(0, pct);
28
+ return out;
29
+ }
30
+ function parseCidr(raw) {
31
+ const text = raw.trim();
32
+ const slash = text.indexOf("/");
33
+ const address = normalizeIp(slash === -1 ? text : text.slice(0, slash));
34
+ const version = (0, node_net.isIP)(address);
35
+ if (version === 0) return null;
36
+ const max = version === 4 ? 32 : 128;
37
+ let prefix = max;
38
+ if (slash !== -1) {
39
+ const bits = text.slice(slash + 1);
40
+ if (!/^\d{1,3}$/.test(bits)) return null;
41
+ prefix = Number.parseInt(bits, 10);
42
+ if (prefix > max) return null;
43
+ }
44
+ return {
45
+ network: address,
46
+ prefix,
47
+ family: version === 4 ? "ipv4" : "ipv6"
48
+ };
49
+ }
50
+ /** Parse a list of CIDR strings; blanks are skipped, unparseable entries returned. */
51
+ function parseCidrList(entries) {
52
+ const valid = [];
53
+ const invalid = [];
54
+ for (const entry of entries) {
55
+ if (entry.trim().length === 0) continue;
56
+ const parsed = parseCidr(entry);
57
+ if (parsed === null) invalid.push(entry);
58
+ else valid.push(parsed);
59
+ }
60
+ return {
61
+ valid,
62
+ invalid
63
+ };
64
+ }
65
+ /** A stored setting as a string list — accepts a list, or one comma/space separated string. */
66
+ function readCidrSetting(value) {
67
+ if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
68
+ if (typeof value === "string") return value.split(/[\s,]+/).filter((v) => v.length > 0);
69
+ return [];
70
+ }
71
+ /** A matcher over the VALID entries of `entries`; invalid ones match nothing. */
72
+ function createCidrMatcher(entries) {
73
+ const { valid } = parseCidrList(entries);
74
+ const list = new node_net.BlockList();
75
+ for (const cidr of valid) list.addSubnet(cidr.network, cidr.prefix, cidr.family);
76
+ return {
77
+ size: valid.length,
78
+ contains(ip) {
79
+ if (valid.length === 0) return false;
80
+ const address = normalizeIp(ip);
81
+ const version = (0, node_net.isIP)(address);
82
+ if (version === 0) return false;
83
+ return list.check(address, version === 4 ? "ipv4" : "ipv6");
84
+ }
85
+ };
86
+ }
87
+ //#endregion
88
+ //#region src/kernel/moleculer/mesh-auth-mode.ts
89
+ /** The field key inside the `cluster` section. */
90
+ var MESH_AUTH_MODE_FIELD = "meshAuth";
91
+ function isMeshAuthMode(value) {
92
+ return value === "transition" || value === "enforced";
93
+ }
94
+ /** A stored value that is not a mode reads as `transition`: never refuse on a typo. */
95
+ function parseMeshAuthMode(value) {
96
+ return isMeshAuthMode(value) ? value : "transition";
97
+ }
98
+ var MODE_SLOT = Symbol.for("camstack.cluster.mesh-auth-mode");
99
+ var ENFORCER_SLOT = Symbol.for("camstack.cluster.mesh-auth-enforcer");
100
+ var STATUS_SLOT = Symbol.for("camstack.cluster.mesh-auth-status");
101
+ /** Published by `system-config` whenever the stored mode is (re)read. */
102
+ function setProcessMeshAuthMode(mode) {
103
+ Reflect.set(globalThis, MODE_SLOT, mode);
104
+ }
105
+ /**
106
+ * The hub's mode. Until `system-config` has read it: `transition` — the safe
107
+ * side for an upgrading cluster (nothing that worked before is refused), and
108
+ * exactly what an unread setting means.
109
+ */
110
+ function getProcessMeshAuthMode() {
111
+ return parseMeshAuthMode(Reflect.get(globalThis, MODE_SLOT));
112
+ }
113
+ function setProcessMeshEnforcer(enforcer) {
114
+ if (enforcer === null) Reflect.deleteProperty(globalThis, ENFORCER_SLOT);
115
+ else Reflect.set(globalThis, ENFORCER_SLOT, enforcer);
116
+ }
117
+ /** Ask the owner of the setting to switch to `enforced`. `false` when nobody can. */
118
+ async function requestProcessMeshEnforcement(reason) {
119
+ const enforcer = Reflect.get(globalThis, ENFORCER_SLOT);
120
+ if (typeof enforcer !== "function") return false;
121
+ await enforcer(reason);
122
+ return true;
123
+ }
124
+ /** Published by the hub's broker; read by System → Cluster. */
125
+ function setProcessMeshAuthStatusSource(source) {
126
+ if (source === null) Reflect.deleteProperty(globalThis, STATUS_SLOT);
127
+ else Reflect.set(globalThis, STATUS_SLOT, source);
128
+ }
129
+ function getProcessMeshAuthStatus() {
130
+ const source = Reflect.get(globalThis, STATUS_SLOT);
131
+ return typeof source === "function" ? source() : null;
132
+ }
133
+ //#endregion
8
134
  //#region src/builtins/system-config/cluster-secret-keeper.ts
9
135
  /**
10
- * Keeps the hub's cluster-secret gate in step with System → Cluster (D797).
136
+ * Keeps the hub's cluster-secret gate in step with System → Cluster (D797,
137
+ * D802).
11
138
  *
12
139
  * The field is a `password` of `system-config`: its `system-settings` row
13
140
  * (`cluster.clusterSecret`) holds a keychain marker, the value lives in the
14
- * keychain (owner `system`). The gate the `$hub.registerNode` handler reads:
141
+ * keychain (owner `system`). The gate the mesh join reads:
15
142
  *
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;
143
+ * - no marker → the secret is MANDATORY (D802): the keeper asks
144
+ * its owner to generate one (`generateSecret`), and
145
+ * until that succeeds the gate is `off` — no node can
146
+ * authenticate, and only `transition` lets them in;
147
+ * - marker + value → `on` — nodes prove the secret;
19
148
  * - marker, no answer → `unknown` — refused, retryably, and retried here.
20
- * A locked keychain or a value lost to a reset NEVER opens the gate.
149
+ * A locked keychain or a value lost to a reset NEVER opens the gate, and
150
+ * never triggers a generation (the operator decides what replaces a value
151
+ * the keychain lost).
152
+ *
153
+ * It also publishes the mesh mode (`cluster.meshAuth`, `mesh-auth-mode.ts`).
21
154
  */
22
155
  /** The field key inside the `cluster` section. */
23
156
  var CLUSTER_SECRET_FIELD = "clusterSecret";
@@ -46,22 +179,35 @@ var ClusterSecretKeeper = class {
46
179
  /** Re-read the row and the keychain; publish the gate. */
47
180
  async reload() {
48
181
  if (this.stopped) return;
49
- const gate = await this.compute();
182
+ let gate = await this.compute();
183
+ if (gate.kind === "off" && this.deps.generateSecret !== void 0 && !this.stopped) gate = await this.generate();
50
184
  if (this.stopped) return;
51
185
  require_cluster_secret.setProcessClusterSecretGate(gate);
52
186
  this.report(gate);
53
- if (gate.kind === "unknown") this.scheduleRetry();
187
+ if (gate.kind === "unknown" || gate.kind === "off" && this.deps.generateSecret !== void 0) this.scheduleRetry();
188
+ }
189
+ async generate() {
190
+ try {
191
+ await this.deps.generateSecret?.();
192
+ } catch (err) {
193
+ this.deps.logger.error("The cluster secret is mandatory but could not be generated yet — retrying. Agents cannot authenticate until it exists.", { meta: { error: errText(err) } });
194
+ return { kind: "off" };
195
+ }
196
+ this.deps.logger.info("Generated the cluster secret (none was stored). Reveal it on System → Cluster and enter it in each agent.");
197
+ return this.compute();
54
198
  }
55
199
  async compute() {
56
- let stored;
200
+ let section;
57
201
  try {
58
- stored = (await this.deps.readSection())[CLUSTER_SECRET_FIELD];
202
+ section = await this.deps.readSection();
59
203
  } catch (err) {
60
204
  return {
61
205
  kind: "unknown",
62
206
  reason: `settings unreadable: ${errText(err)}`
63
207
  };
64
208
  }
209
+ setProcessMeshAuthMode(parseMeshAuthMode(section[MESH_AUTH_MODE_FIELD]));
210
+ const stored = section[CLUSTER_SECRET_FIELD];
65
211
  if (stored === void 0 || stored === null || stored === "") return { kind: "off" };
66
212
  if (!require_dist.isSecretMarker(stored)) return {
67
213
  kind: "unknown",
@@ -111,7 +257,7 @@ var ClusterSecretKeeper = class {
111
257
  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
258
  return;
113
259
  }
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");
260
+ if (changed) this.deps.logger.info(gate.kind === "on" ? "Cluster secret set — agents prove it to join the mesh" : "No cluster secret yet — no node can authenticate to the mesh");
115
261
  }
116
262
  scheduleRetry() {
117
263
  if (this.stopped || this.retryTimer !== null) return;
@@ -138,10 +284,12 @@ function errText(err) {
138
284
  *
139
285
  * Sections exposed — this list is the whole of it:
140
286
  * - server (port, host, dataPath — READ-ONLY, loaded from config.yaml at bootstrap)
141
- * - auth (tokenExpiry)
287
+ * - auth (tokenExpiry = session lifetime, accessTokenExpiry, trustedProxies,
288
+ * trustedNetworks)
142
289
  * - cluster (clusterSecret — a `password` field: the value goes to the
143
290
  * keychain, owner `system`; the row keeps a marker. Also drawn
144
- * on System → Cluster. D797)
291
+ * on System → Cluster. D797. MANDATORY and generated at first
292
+ * boot; `meshAuth` — transition / enforced. D802)
145
293
  *
146
294
  * Sections NOT exposed (each owned by the addon that implements them):
147
295
  * - logging → `winston-logging` addon (level + retention)
@@ -182,8 +330,32 @@ var KEY_TO_SECTION = {
182
330
  host: "server",
183
331
  dataPath: "server",
184
332
  tokenExpiry: "auth",
185
- [CLUSTER_SECRET_FIELD]: "cluster"
333
+ accessTokenExpiry: "auth",
334
+ trustedProxies: "auth",
335
+ trustedNetworks: "auth",
336
+ [CLUSTER_SECRET_FIELD]: "cluster",
337
+ [MESH_AUTH_MODE_FIELD]: "cluster"
338
+ };
339
+ /** Bytes of a generated cluster secret (D802) — 256 bits, base64url (43 chars). */
340
+ var GENERATED_SECRET_BYTES = 32;
341
+ var STATE_LABELS = {
342
+ authenticated: "authenticated",
343
+ legacy: "legacy hash only — update the agent",
344
+ unauthenticated: "not authenticated — enter the cluster secret in this agent"
186
345
  };
346
+ /** The Cluster page's live line per node (D802). */
347
+ function describeMeshStatus(status) {
348
+ if (status === null) return "The mesh status is available on the hub only.";
349
+ const head = status.mode === "enforced" ? "Enforced: a node that has not authenticated is refused." : "Transition: nodes without the secret are still let in (with a warning). The hub switches to Enforced by itself once every known agent has authenticated.";
350
+ if (status.nodes.length === 0) return `${head}\nNo other node is connected.`;
351
+ return `${head}\n${status.nodes.map((n) => `• ${n.nodeId}: ${STATE_LABELS[n.state] ?? n.state}`).join("\n")}`;
352
+ }
353
+ /**
354
+ * Auth keys holding a list of networks (CIDR or single address). A save with
355
+ * an entry that does not parse is REFUSED whole: dropping it would trust less
356
+ * than the operator wrote, guessing would trust more.
357
+ */
358
+ var CIDR_LIST_KEYS = new Set(["trustedProxies", "trustedNetworks"]);
187
359
  /** The section id of the cluster form — System → Cluster renders only this one. */
188
360
  var SYSTEM_CONFIG_CLUSTER_SECTION_ID = "system-config-cluster";
189
361
  var SystemConfigAddon = class extends require_dist.BaseAddon {
@@ -196,12 +368,18 @@ var SystemConfigAddon = class extends require_dist.BaseAddon {
196
368
  this.clusterSecretKeeper = new ClusterSecretKeeper({
197
369
  secrets: this.systemSecrets(),
198
370
  readSection: async () => await this.ctx.settings?.getSection("cluster") ?? {},
199
- logger: this.ctx.logger
371
+ logger: this.ctx.logger,
372
+ generateSecret: () => this.generateClusterSecret()
373
+ });
374
+ setProcessMeshEnforcer(async (reason) => {
375
+ await this.writeMeshAuthMode("enforced");
376
+ this.ctx.logger.info("Mesh authentication is now ENFORCED", { meta: { reason } });
200
377
  });
201
378
  await this.clusterSecretKeeper.start();
202
379
  this.ctx.logger.info("Initialized — exposes yml-backed sections via getGlobalSettings");
203
380
  }
204
381
  async onShutdown() {
382
+ setProcessMeshEnforcer(null);
205
383
  this.clusterSecretKeeper?.stop();
206
384
  this.clusterSecretKeeper = null;
207
385
  }
@@ -254,26 +432,80 @@ var SystemConfigAddon = class extends require_dist.BaseAddon {
254
432
  title: SECTION_TITLES["auth"],
255
433
  description: "Token and session settings.",
256
434
  columns: 1,
257
- fields: [{
258
- type: "text",
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
- }]
435
+ fields: [
436
+ {
437
+ type: "text",
438
+ key: "tokenExpiry",
439
+ label: "Session lifetime",
440
+ description: "How long a sign-in lasts without being used, as a duration string (e.g. 7d, 30d). Every refresh starts it again, so a device in use stays signed in; one left unopened this long must sign in again. A bare number is rejected — it would be read as milliseconds. Applies from the next sign-in or refresh.",
441
+ placeholder: "30d",
442
+ default: "30d"
443
+ },
444
+ {
445
+ type: "text",
446
+ key: "accessTokenExpiry",
447
+ label: "Access token lifetime",
448
+ description: "How long one access token lives before the app silently swaps it for a new one (e.g. 2h; between 5m and 24h, never longer than the session). Shorter limits how long a copied token is useful. Apps that predate refresh tokens get one token for the whole session instead.",
449
+ placeholder: "2h",
450
+ default: "2h"
451
+ },
452
+ {
453
+ type: "tags",
454
+ key: "trustedProxies",
455
+ label: "Trusted reverse proxies",
456
+ description: "Addresses or CIDR ranges (e.g. 192.168.1.2 or 10.0.0.0/24) of reverse proxies in front of the hub. Only a request whose direct sender is one of these — or this host itself, as with the Cloudflare tunnel — has its X-Forwarded-For / CF-Connecting-IP read as the real client address for rate limits. Leave empty unless a proxy on another host forwards to the hub.",
457
+ creatable: true
458
+ },
459
+ {
460
+ type: "info",
461
+ key: "trustedNetworksWarning",
462
+ label: "Skipping two-factor authentication",
463
+ variant: "danger",
464
+ content: "A sign-in made DIRECTLY from a network listed below skips the second factor (TOTP or passkey): anyone on that network who knows or guesses a password gets in with the password alone — including a compromised camera or a guest device. Requests that come through the Cloudflare tunnel, the public address or any reverse proxy never skip it, whatever address they claim. Every skip is recorded in the authentication audit log. Leave empty (the default) to always ask for the second factor."
465
+ },
466
+ {
467
+ type: "tags",
468
+ key: "trustedNetworks",
469
+ label: "Trusted networks (skip the second factor)",
470
+ description: "Addresses or CIDR ranges, e.g. 192.168.1.0/24. Keep it as narrow as you can — a single trusted VLAN, not the whole LAN.",
471
+ creatable: true
472
+ }
473
+ ]
265
474
  },
266
475
  {
267
476
  id: SYSTEM_CONFIG_CLUSTER_SECTION_ID,
268
477
  title: SECTION_TITLES["cluster"],
269
- description: "Agents joining this hub. Optional: with no cluster secret, any node that reaches the hub may register.",
478
+ description: "Agents joining this hub. Every agent proves the cluster secret when it connects (challenge–response; the secret never crosses the network).",
270
479
  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
- }]
480
+ fields: [
481
+ {
482
+ type: "password",
483
+ key: CLUSTER_SECRET_FIELD,
484
+ label: "Cluster secret",
485
+ description: "Mandatory — generated by the hub at first boot and stored in the keychain. To pair an agent: Reveal and copy it here, then enter it in the agent (its status page, or the desktop setup window). Saving a new value rotates it: every agent must then be given the new one."
486
+ },
487
+ {
488
+ type: "select",
489
+ key: MESH_AUTH_MODE_FIELD,
490
+ label: "Agents without the secret",
491
+ description: "Transition lets an agent that has not authenticated keep working (with a warning) while you give each agent the secret; it ends by itself once every known agent has authenticated. Enforced refuses it.",
492
+ default: "transition",
493
+ options: [{
494
+ value: "transition",
495
+ label: "Transition — allow, warn"
496
+ }, {
497
+ value: "enforced",
498
+ label: "Enforced — refuse"
499
+ }]
500
+ },
501
+ {
502
+ type: "info",
503
+ key: "meshAuthStatus",
504
+ label: "Mesh authentication",
505
+ content: describeMeshStatus(getProcessMeshAuthStatus()),
506
+ variant: "info"
507
+ }
508
+ ]
277
509
  }
278
510
  ] };
279
511
  }
@@ -307,6 +539,10 @@ var SystemConfigAddon = class extends require_dist.BaseAddon {
307
539
  this.ctx.logger.warn("update ignored read-only server-section key", { meta: { key } });
308
540
  continue;
309
541
  }
542
+ if (CIDR_LIST_KEYS.has(key)) {
543
+ const { invalid } = parseCidrList(readCidrSetting(value));
544
+ if (invalid.length > 0) throw new Error(`Not a network address or CIDR range: ${invalid.join(", ")}`);
545
+ }
310
546
  let bucket = perSection.get(section);
311
547
  if (!bucket) {
312
548
  bucket = {};
@@ -328,13 +564,52 @@ var SystemConfigAddon = class extends require_dist.BaseAddon {
328
564
  }
329
565
  /**
330
566
  * 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).
567
+ * `system`), the row gets the marker; `''` / a view keeps. `$secretClear` is
568
+ * REFUSED: the secret is mandatory (D802). A value the keychain cannot store
569
+ * is REFUSED — never written inline (D787, P5). `meshAuth` is a plain value.
334
570
  */
335
571
  async writeClusterSection(patch) {
572
+ const { [MESH_AUTH_MODE_FIELD]: mode, ...secretPatch } = patch;
573
+ if (mode !== void 0) {
574
+ if (!isMeshAuthMode(mode)) throw new Error(`system-config: meshAuth must be 'transition' or 'enforced'`);
575
+ await this.writeMeshAuthMode(mode);
576
+ }
577
+ if (Object.keys(secretPatch).length === 0) return;
578
+ if (require_dist.isSecretClear(secretPatch["clusterSecret"])) throw new Error("The cluster secret is mandatory and cannot be cleared — save a new value to rotate it");
579
+ await this.storeClusterSecret(secretPatch);
580
+ this.ctx.logger.info("Cluster secret updated");
581
+ await this.clusterSecretKeeper?.reload();
582
+ }
583
+ /** `cluster.meshAuth` (D802), then republish it. */
584
+ async writeMeshAuthMode(mode) {
585
+ await this.ctx.settings.setSection("cluster", { [MESH_AUTH_MODE_FIELD]: mode });
586
+ await this.clusterSecretKeeper?.reload();
587
+ }
588
+ /**
589
+ * First boot, or a keychain reset that cleared the marker (D802): a fresh
590
+ * random secret, stored like an operator save. A NEW secret means every
591
+ * agent must be given it, so the mesh goes (back) to `transition`; on a hub
592
+ * with no agent yet, the hub's auto-enforce ends it at once.
593
+ */
594
+ async generateClusterSecret() {
336
595
  const settings = this.ctx.settings;
337
596
  const secrets = this.systemSecrets();
597
+ if (!settings || secrets === null) throw new Error("system-config: no keychain door — the cluster secret cannot be generated");
598
+ const existing = (await secrets.resolve([require_cluster_secret.CLUSTER_SECRET_REF]))[require_cluster_secret.CLUSTER_SECRET_REF];
599
+ if (existing?.kind === "value") {
600
+ await settings.setSection("cluster", { [CLUSTER_SECRET_FIELD]: require_dist.makeSecretMarker(require_cluster_secret.CLUSTER_SECRET_REF) });
601
+ this.ctx.logger.warn("The cluster secret row was empty but the keychain holds a value — re-adopted it");
602
+ return;
603
+ }
604
+ if (existing?.kind !== "missing") throw new Error(`the keychain cannot answer for the cluster secret (${existing?.kind ?? "no answer"})`);
605
+ const value = (0, node_crypto.randomBytes)(GENERATED_SECRET_BYTES).toString("base64url");
606
+ await this.storeClusterSecret({ [CLUSTER_SECRET_FIELD]: value });
607
+ await settings.setSection("cluster", { [MESH_AUTH_MODE_FIELD]: "transition" });
608
+ }
609
+ async storeClusterSecret(patch) {
610
+ const settings = this.ctx.settings;
611
+ if (!settings) throw new Error("system-config: ctx.settings not available");
612
+ const secrets = this.systemSecrets();
338
613
  if (secrets === null) throw new Error("system-config: no keychain door — the cluster secret cannot be stored");
339
614
  const stored = await settings.getSection("cluster");
340
615
  const result = await require_dist.transformSecretWrite({
@@ -346,12 +621,23 @@ var SystemConfigAddon = class extends require_dist.BaseAddon {
346
621
  });
347
622
  if (Object.keys(result.patch).length > 0) await settings.setSection("cluster", result.patch);
348
623
  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();
624
+ if (orphaned.length > 0) this.ctx.logger.warn("an old cluster secret value could not be deleted from the keychain", { meta: { refs: orphaned } });
352
625
  }
353
626
  };
354
627
  //#endregion
628
+ exports.MESH_AUTH_MODE_FIELD = MESH_AUTH_MODE_FIELD;
355
629
  exports.SYSTEM_CONFIG_CLUSTER_SECTION_ID = SYSTEM_CONFIG_CLUSTER_SECTION_ID;
356
630
  exports.SystemConfigAddon = SystemConfigAddon;
357
631
  exports.default = SystemConfigAddon;
632
+ exports.createCidrMatcher = createCidrMatcher;
633
+ exports.getProcessMeshAuthMode = getProcessMeshAuthMode;
634
+ exports.getProcessMeshAuthStatus = getProcessMeshAuthStatus;
635
+ exports.isMeshAuthMode = isMeshAuthMode;
636
+ exports.normalizeIp = normalizeIp;
637
+ exports.parseCidrList = parseCidrList;
638
+ exports.parseMeshAuthMode = parseMeshAuthMode;
639
+ exports.readCidrSetting = readCidrSetting;
640
+ exports.requestProcessMeshEnforcement = requestProcessMeshEnforcement;
641
+ exports.setProcessMeshAuthMode = setProcessMeshAuthMode;
642
+ exports.setProcessMeshAuthStatusSource = setProcessMeshAuthStatusSource;
643
+ exports.setProcessMeshEnforcer = setProcessMeshEnforcer;