@camstack/system 1.2.380 → 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 (117) 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-aggregation.d.ts +12 -0
  37. package/dist/builtins/device-manager/device-form-secrets.d.ts +25 -0
  38. package/dist/builtins/device-manager/device-manager.addon.d.ts +2 -0
  39. package/dist/builtins/device-manager/device-manager.addon.js +165 -26
  40. package/dist/builtins/device-manager/device-manager.addon.mjs +165 -26
  41. package/dist/builtins/hub-forwarder/index.js +1 -1
  42. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  43. package/dist/builtins/keychain/index.js +1108 -733
  44. package/dist/builtins/keychain/index.mjs +1108 -733
  45. package/dist/builtins/keychain/keychain-reset-trigger.d.ts +25 -0
  46. package/dist/builtins/keychain/keychain-service.d.ts +36 -3
  47. package/dist/builtins/keychain/keychain-store.d.ts +24 -1
  48. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  49. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  50. package/dist/builtins/local-auth/local-auth.addon.d.ts +16 -0
  51. package/dist/builtins/local-auth/local-auth.addon.js +1021 -118
  52. package/dist/builtins/local-auth/local-auth.addon.mjs +1034 -141
  53. package/dist/builtins/local-network/local-network.addon.js +3 -3
  54. package/dist/builtins/local-network/local-network.addon.mjs +3 -3
  55. package/dist/builtins/loki-logging/index.js +97 -29
  56. package/dist/builtins/loki-logging/index.mjs +97 -29
  57. package/dist/builtins/loki-logging/loki-logging.addon.d.ts +7 -23
  58. package/dist/builtins/loki-logging/retire-enabled-flag.d.ts +23 -0
  59. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  60. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  61. package/dist/builtins/platform-probe/index.js +1 -1
  62. package/dist/builtins/platform-probe/index.mjs +1 -1
  63. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  64. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  65. package/dist/builtins/snapshot/index.js +1 -1
  66. package/dist/builtins/snapshot/index.mjs +1 -1
  67. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  68. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  69. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  70. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  71. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +2 -2
  72. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +2 -2
  73. package/dist/builtins/system-backup/backup-encryption.d.ts +57 -0
  74. package/dist/builtins/system-backup/system-backup.service.d.ts +16 -1
  75. package/dist/builtins/system-config/cluster-secret-keeper.d.ts +7 -0
  76. package/dist/builtins/system-config/system-config.addon.d.ts +13 -3
  77. package/dist/builtins/system-config/system-config.addon.js +324 -38
  78. package/dist/builtins/system-config/system-config.addon.mjs +312 -39
  79. package/dist/builtins/winston-logging/index.js +1 -1
  80. package/dist/builtins/winston-logging/index.mjs +1 -1
  81. package/dist/{child-cap-dispatch-CZeRUgYF.js → child-cap-dispatch-BrMdKHfp.js} +6 -3
  82. package/dist/{child-cap-dispatch-CpjohToR.mjs → child-cap-dispatch-WsYPNvnK.mjs} +10 -7
  83. package/dist/{cluster-secret-Dg0OOOSr.js → cluster-secret-BEPLh3LL.js} +1 -1
  84. package/dist/{cluster-secret-BDue-uJj.mjs → cluster-secret-DhPdZ0VA.mjs} +1 -1
  85. package/dist/{composition-sources-CHvLMrFI.js → composition-sources-C6ZynCZ2.js} +1 -1
  86. package/dist/{composition-sources-DT_uJwI3.mjs → composition-sources-XqRX51db.mjs} +1 -1
  87. package/dist/{dist-B9oFw2jh.mjs → dist-CP5TTjwd.mjs} +1713 -157
  88. package/dist/{dist-Cc6KOdY-.js → dist-tnAO12ak.js} +1754 -156
  89. package/dist/{event-loop-stall-monitor-mk6WGxir.js → event-loop-stall-monitor-9kR0sAPS.js} +15 -15
  90. package/dist/{event-loop-stall-monitor-anUH2IE1.mjs → event-loop-stall-monitor-WkssIXjq.mjs} +15 -15
  91. package/dist/{hub-cap-forward-CH6_VWVO.mjs → hub-cap-forward-CDiN1p98.mjs} +18 -2
  92. package/dist/{hub-cap-forward-COCmYaup.js → hub-cap-forward-W8oEm4ME.js} +23 -1
  93. package/dist/index.d.ts +16 -1
  94. package/dist/index.js +3341 -79
  95. package/dist/index.mjs +3567 -354
  96. package/dist/{jwt-signing-key-Qhde8r82.mjs → jwt-signing-key-CAbozwvF.mjs} +1 -1
  97. package/dist/{jwt-signing-key-C2rE3wH6.js → jwt-signing-key-Dgf8sIXY.js} +1 -1
  98. package/dist/kernel/index.d.ts +12 -0
  99. package/dist/kernel/moleculer/broker-factory.d.ts +8 -1
  100. package/dist/kernel/moleculer/child-addon-call-dispatch.d.ts +1 -0
  101. package/dist/kernel/moleculer/hub-service.d.ts +9 -0
  102. package/dist/kernel/moleculer/mesh-access-middleware.d.ts +28 -0
  103. package/dist/kernel/moleculer/mesh-auth-mode.d.ts +64 -0
  104. package/dist/kernel/moleculer/mesh-authenticator.d.ts +71 -0
  105. package/dist/kernel/moleculer/mesh-auto-enforce.d.ts +26 -0
  106. package/dist/kernel/moleculer/mesh-join-client.d.ts +51 -0
  107. package/dist/kernel/moleculer/mesh-join-proof.d.ts +39 -0
  108. package/dist/kernel/system-settings-policy.d.ts +4 -3
  109. package/dist/kernel/transport/hub-cap-forward.d.ts +14 -3
  110. package/dist/kernel/transport/index.d.ts +1 -1
  111. package/dist/restore-archive-BV39nY46.mjs +4045 -0
  112. package/dist/restore-archive-CbvrfPuB.js +4139 -0
  113. package/dist/{retired-settings-keys-YOOKLA3v.js → retired-settings-keys-4bHpL-0m.js} +1 -1
  114. package/dist/{retired-settings-keys-DlZ3VV6x.mjs → retired-settings-keys-CzpIkwS-.mjs} +1 -1
  115. package/dist/{tls-CJHogwTl.mjs → tls-4cRH24Id.mjs} +3 -3
  116. package/package.json +1 -1
  117. package/dist/{tls-CCMX7MjX.js → tls-CljF057D.js} +1 -1
@@ -1,18 +1,151 @@
1
- import { Kr as transformSecretWrite, Or as hydrateSchema, Ur as secretTreeFromSchema, cr as errMsg, jr as isSecretMarker, lr as BaseAddon } from "../../dist-B9oFw2jh.mjs";
2
- import { d as setProcessClusterSecretGate, l as hashClusterSecret, n as CLUSTER_SECRET_REF } from "../../cluster-secret-BDue-uJj.mjs";
1
+ import { $r as transformSecretWrite, Ar as hydrateSchema, Fr as isSecretMarker, Lr as makeSecretMarker, Pr as isSecretClear, Yr as secretTreeFromSchema, lr as errMsg, ur as BaseAddon } from "../../dist-CP5TTjwd.mjs";
2
+ import { d as setProcessClusterSecretGate, l as hashClusterSecret, n as CLUSTER_SECRET_REF } from "../../cluster-secret-DhPdZ0VA.mjs";
3
+ import { randomBytes } from "node:crypto";
4
+ import { BlockList, isIP } from "node:net";
5
+ //#region src/auth/cidr.ts
6
+ /**
7
+ * CIDR lists for the auth settings that name networks: the trusted reverse
8
+ * proxies whose forwarding headers the hub believes, and the trusted local
9
+ * networks a login may skip its second factor from.
10
+ *
11
+ * Built on `net.BlockList`, so IPv4 and IPv6 are matched by the platform, not
12
+ * by string prefixes. A bare address is read as a single host. Anything that
13
+ * does not parse is REPORTED back (`invalid`) so the settings form can refuse
14
+ * it — a list that silently dropped a typo would quietly trust less than the
15
+ * operator wrote, and one that guessed would trust more.
16
+ */
17
+ /** Strip an IPv4-mapped IPv6 prefix and a zone id, so ranges see a clean address. */
18
+ function normalizeIp(ip) {
19
+ let out = ip.trim();
20
+ if (out.toLowerCase().startsWith("::ffff:") && isIP(out.slice(7)) === 4) out = out.slice(7);
21
+ const pct = out.indexOf("%");
22
+ if (pct !== -1) out = out.slice(0, pct);
23
+ return out;
24
+ }
25
+ function parseCidr(raw) {
26
+ const text = raw.trim();
27
+ const slash = text.indexOf("/");
28
+ const address = normalizeIp(slash === -1 ? text : text.slice(0, slash));
29
+ const version = isIP(address);
30
+ if (version === 0) return null;
31
+ const max = version === 4 ? 32 : 128;
32
+ let prefix = max;
33
+ if (slash !== -1) {
34
+ const bits = text.slice(slash + 1);
35
+ if (!/^\d{1,3}$/.test(bits)) return null;
36
+ prefix = Number.parseInt(bits, 10);
37
+ if (prefix > max) return null;
38
+ }
39
+ return {
40
+ network: address,
41
+ prefix,
42
+ family: version === 4 ? "ipv4" : "ipv6"
43
+ };
44
+ }
45
+ /** Parse a list of CIDR strings; blanks are skipped, unparseable entries returned. */
46
+ function parseCidrList(entries) {
47
+ const valid = [];
48
+ const invalid = [];
49
+ for (const entry of entries) {
50
+ if (entry.trim().length === 0) continue;
51
+ const parsed = parseCidr(entry);
52
+ if (parsed === null) invalid.push(entry);
53
+ else valid.push(parsed);
54
+ }
55
+ return {
56
+ valid,
57
+ invalid
58
+ };
59
+ }
60
+ /** A stored setting as a string list — accepts a list, or one comma/space separated string. */
61
+ function readCidrSetting(value) {
62
+ if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
63
+ if (typeof value === "string") return value.split(/[\s,]+/).filter((v) => v.length > 0);
64
+ return [];
65
+ }
66
+ /** A matcher over the VALID entries of `entries`; invalid ones match nothing. */
67
+ function createCidrMatcher(entries) {
68
+ const { valid } = parseCidrList(entries);
69
+ const list = new BlockList();
70
+ for (const cidr of valid) list.addSubnet(cidr.network, cidr.prefix, cidr.family);
71
+ return {
72
+ size: valid.length,
73
+ contains(ip) {
74
+ if (valid.length === 0) return false;
75
+ const address = normalizeIp(ip);
76
+ const version = isIP(address);
77
+ if (version === 0) return false;
78
+ return list.check(address, version === 4 ? "ipv4" : "ipv6");
79
+ }
80
+ };
81
+ }
82
+ //#endregion
83
+ //#region src/kernel/moleculer/mesh-auth-mode.ts
84
+ /** The field key inside the `cluster` section. */
85
+ var MESH_AUTH_MODE_FIELD = "meshAuth";
86
+ function isMeshAuthMode(value) {
87
+ return value === "transition" || value === "enforced";
88
+ }
89
+ /** A stored value that is not a mode reads as `transition`: never refuse on a typo. */
90
+ function parseMeshAuthMode(value) {
91
+ return isMeshAuthMode(value) ? value : "transition";
92
+ }
93
+ var MODE_SLOT = Symbol.for("camstack.cluster.mesh-auth-mode");
94
+ var ENFORCER_SLOT = Symbol.for("camstack.cluster.mesh-auth-enforcer");
95
+ var STATUS_SLOT = Symbol.for("camstack.cluster.mesh-auth-status");
96
+ /** Published by `system-config` whenever the stored mode is (re)read. */
97
+ function setProcessMeshAuthMode(mode) {
98
+ Reflect.set(globalThis, MODE_SLOT, mode);
99
+ }
100
+ /**
101
+ * The hub's mode. Until `system-config` has read it: `transition` — the safe
102
+ * side for an upgrading cluster (nothing that worked before is refused), and
103
+ * exactly what an unread setting means.
104
+ */
105
+ function getProcessMeshAuthMode() {
106
+ return parseMeshAuthMode(Reflect.get(globalThis, MODE_SLOT));
107
+ }
108
+ function setProcessMeshEnforcer(enforcer) {
109
+ if (enforcer === null) Reflect.deleteProperty(globalThis, ENFORCER_SLOT);
110
+ else Reflect.set(globalThis, ENFORCER_SLOT, enforcer);
111
+ }
112
+ /** Ask the owner of the setting to switch to `enforced`. `false` when nobody can. */
113
+ async function requestProcessMeshEnforcement(reason) {
114
+ const enforcer = Reflect.get(globalThis, ENFORCER_SLOT);
115
+ if (typeof enforcer !== "function") return false;
116
+ await enforcer(reason);
117
+ return true;
118
+ }
119
+ /** Published by the hub's broker; read by System → Cluster. */
120
+ function setProcessMeshAuthStatusSource(source) {
121
+ if (source === null) Reflect.deleteProperty(globalThis, STATUS_SLOT);
122
+ else Reflect.set(globalThis, STATUS_SLOT, source);
123
+ }
124
+ function getProcessMeshAuthStatus() {
125
+ const source = Reflect.get(globalThis, STATUS_SLOT);
126
+ return typeof source === "function" ? source() : null;
127
+ }
128
+ //#endregion
3
129
  //#region src/builtins/system-config/cluster-secret-keeper.ts
4
130
  /**
5
- * Keeps the hub's cluster-secret gate in step with System → Cluster (D797).
131
+ * Keeps the hub's cluster-secret gate in step with System → Cluster (D797,
132
+ * D802).
6
133
  *
7
134
  * The field is a `password` of `system-config`: its `system-settings` row
8
135
  * (`cluster.clusterSecret`) holds a keychain marker, the value lives in the
9
- * keychain (owner `system`). The gate the `$hub.registerNode` handler reads:
136
+ * keychain (owner `system`). The gate the mesh join reads:
10
137
  *
11
- * - no marker → `off` — every node may register, exactly as a hub
12
- * that never had a secret;
13
- * - marker + value → `on` — nodes present the matching hash;
138
+ * - no marker → the secret is MANDATORY (D802): the keeper asks
139
+ * its owner to generate one (`generateSecret`), and
140
+ * until that succeeds the gate is `off` — no node can
141
+ * authenticate, and only `transition` lets them in;
142
+ * - marker + value → `on` — nodes prove the secret;
14
143
  * - marker, no answer → `unknown` — refused, retryably, and retried here.
15
- * A locked keychain or a value lost to a reset NEVER opens the gate.
144
+ * A locked keychain or a value lost to a reset NEVER opens the gate, and
145
+ * never triggers a generation (the operator decides what replaces a value
146
+ * the keychain lost).
147
+ *
148
+ * It also publishes the mesh mode (`cluster.meshAuth`, `mesh-auth-mode.ts`).
16
149
  */
17
150
  /** The field key inside the `cluster` section. */
18
151
  var CLUSTER_SECRET_FIELD = "clusterSecret";
@@ -41,22 +174,35 @@ var ClusterSecretKeeper = class {
41
174
  /** Re-read the row and the keychain; publish the gate. */
42
175
  async reload() {
43
176
  if (this.stopped) return;
44
- const gate = await this.compute();
177
+ let gate = await this.compute();
178
+ if (gate.kind === "off" && this.deps.generateSecret !== void 0 && !this.stopped) gate = await this.generate();
45
179
  if (this.stopped) return;
46
180
  setProcessClusterSecretGate(gate);
47
181
  this.report(gate);
48
- if (gate.kind === "unknown") this.scheduleRetry();
182
+ if (gate.kind === "unknown" || gate.kind === "off" && this.deps.generateSecret !== void 0) this.scheduleRetry();
183
+ }
184
+ async generate() {
185
+ try {
186
+ await this.deps.generateSecret?.();
187
+ } catch (err) {
188
+ 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) } });
189
+ return { kind: "off" };
190
+ }
191
+ this.deps.logger.info("Generated the cluster secret (none was stored). Reveal it on System → Cluster and enter it in each agent.");
192
+ return this.compute();
49
193
  }
50
194
  async compute() {
51
- let stored;
195
+ let section;
52
196
  try {
53
- stored = (await this.deps.readSection())[CLUSTER_SECRET_FIELD];
197
+ section = await this.deps.readSection();
54
198
  } catch (err) {
55
199
  return {
56
200
  kind: "unknown",
57
201
  reason: `settings unreadable: ${errText(err)}`
58
202
  };
59
203
  }
204
+ setProcessMeshAuthMode(parseMeshAuthMode(section[MESH_AUTH_MODE_FIELD]));
205
+ const stored = section[CLUSTER_SECRET_FIELD];
60
206
  if (stored === void 0 || stored === null || stored === "") return { kind: "off" };
61
207
  if (!isSecretMarker(stored)) return {
62
208
  kind: "unknown",
@@ -106,7 +252,7 @@ var ClusterSecretKeeper = class {
106
252
  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 } });
107
253
  return;
108
254
  }
109
- 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");
255
+ 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");
110
256
  }
111
257
  scheduleRetry() {
112
258
  if (this.stopped || this.retryTimer !== null) return;
@@ -133,10 +279,12 @@ function errText(err) {
133
279
  *
134
280
  * Sections exposed — this list is the whole of it:
135
281
  * - server (port, host, dataPath — READ-ONLY, loaded from config.yaml at bootstrap)
136
- * - auth (tokenExpiry)
282
+ * - auth (tokenExpiry = session lifetime, accessTokenExpiry, trustedProxies,
283
+ * trustedNetworks)
137
284
  * - cluster (clusterSecret — a `password` field: the value goes to the
138
285
  * keychain, owner `system`; the row keeps a marker. Also drawn
139
- * on System → Cluster. D797)
286
+ * on System → Cluster. D797. MANDATORY and generated at first
287
+ * boot; `meshAuth` — transition / enforced. D802)
140
288
  *
141
289
  * Sections NOT exposed (each owned by the addon that implements them):
142
290
  * - logging → `winston-logging` addon (level + retention)
@@ -177,8 +325,32 @@ var KEY_TO_SECTION = {
177
325
  host: "server",
178
326
  dataPath: "server",
179
327
  tokenExpiry: "auth",
180
- [CLUSTER_SECRET_FIELD]: "cluster"
328
+ accessTokenExpiry: "auth",
329
+ trustedProxies: "auth",
330
+ trustedNetworks: "auth",
331
+ [CLUSTER_SECRET_FIELD]: "cluster",
332
+ [MESH_AUTH_MODE_FIELD]: "cluster"
333
+ };
334
+ /** Bytes of a generated cluster secret (D802) — 256 bits, base64url (43 chars). */
335
+ var GENERATED_SECRET_BYTES = 32;
336
+ var STATE_LABELS = {
337
+ authenticated: "authenticated",
338
+ legacy: "legacy hash only — update the agent",
339
+ unauthenticated: "not authenticated — enter the cluster secret in this agent"
181
340
  };
341
+ /** The Cluster page's live line per node (D802). */
342
+ function describeMeshStatus(status) {
343
+ if (status === null) return "The mesh status is available on the hub only.";
344
+ 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.";
345
+ if (status.nodes.length === 0) return `${head}\nNo other node is connected.`;
346
+ return `${head}\n${status.nodes.map((n) => `• ${n.nodeId}: ${STATE_LABELS[n.state] ?? n.state}`).join("\n")}`;
347
+ }
348
+ /**
349
+ * Auth keys holding a list of networks (CIDR or single address). A save with
350
+ * an entry that does not parse is REFUSED whole: dropping it would trust less
351
+ * than the operator wrote, guessing would trust more.
352
+ */
353
+ var CIDR_LIST_KEYS = new Set(["trustedProxies", "trustedNetworks"]);
182
354
  /** The section id of the cluster form — System → Cluster renders only this one. */
183
355
  var SYSTEM_CONFIG_CLUSTER_SECTION_ID = "system-config-cluster";
184
356
  var SystemConfigAddon = class extends BaseAddon {
@@ -191,12 +363,18 @@ var SystemConfigAddon = class extends BaseAddon {
191
363
  this.clusterSecretKeeper = new ClusterSecretKeeper({
192
364
  secrets: this.systemSecrets(),
193
365
  readSection: async () => await this.ctx.settings?.getSection("cluster") ?? {},
194
- logger: this.ctx.logger
366
+ logger: this.ctx.logger,
367
+ generateSecret: () => this.generateClusterSecret()
368
+ });
369
+ setProcessMeshEnforcer(async (reason) => {
370
+ await this.writeMeshAuthMode("enforced");
371
+ this.ctx.logger.info("Mesh authentication is now ENFORCED", { meta: { reason } });
195
372
  });
196
373
  await this.clusterSecretKeeper.start();
197
374
  this.ctx.logger.info("Initialized — exposes yml-backed sections via getGlobalSettings");
198
375
  }
199
376
  async onShutdown() {
377
+ setProcessMeshEnforcer(null);
200
378
  this.clusterSecretKeeper?.stop();
201
379
  this.clusterSecretKeeper = null;
202
380
  }
@@ -249,26 +427,80 @@ var SystemConfigAddon = class extends BaseAddon {
249
427
  title: SECTION_TITLES["auth"],
250
428
  description: "Token and session settings.",
251
429
  columns: 1,
252
- fields: [{
253
- type: "text",
254
- key: "tokenExpiry",
255
- label: "Token Expiry",
256
- 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.",
257
- placeholder: "30d",
258
- default: "30d"
259
- }]
430
+ fields: [
431
+ {
432
+ type: "text",
433
+ key: "tokenExpiry",
434
+ label: "Session lifetime",
435
+ 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.",
436
+ placeholder: "30d",
437
+ default: "30d"
438
+ },
439
+ {
440
+ type: "text",
441
+ key: "accessTokenExpiry",
442
+ label: "Access token lifetime",
443
+ 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.",
444
+ placeholder: "2h",
445
+ default: "2h"
446
+ },
447
+ {
448
+ type: "tags",
449
+ key: "trustedProxies",
450
+ label: "Trusted reverse proxies",
451
+ 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.",
452
+ creatable: true
453
+ },
454
+ {
455
+ type: "info",
456
+ key: "trustedNetworksWarning",
457
+ label: "Skipping two-factor authentication",
458
+ variant: "danger",
459
+ 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."
460
+ },
461
+ {
462
+ type: "tags",
463
+ key: "trustedNetworks",
464
+ label: "Trusted networks (skip the second factor)",
465
+ 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.",
466
+ creatable: true
467
+ }
468
+ ]
260
469
  },
261
470
  {
262
471
  id: SYSTEM_CONFIG_CLUSTER_SECTION_ID,
263
472
  title: SECTION_TITLES["cluster"],
264
- description: "Agents joining this hub. Optional: with no cluster secret, any node that reaches the hub may register.",
473
+ description: "Agents joining this hub. Every agent proves the cluster secret when it connects (challenge–response; the secret never crosses the network).",
265
474
  columns: 1,
266
- fields: [{
267
- type: "password",
268
- key: CLUSTER_SECRET_FIELD,
269
- label: "Cluster secret",
270
- 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."
271
- }]
475
+ fields: [
476
+ {
477
+ type: "password",
478
+ key: CLUSTER_SECRET_FIELD,
479
+ label: "Cluster secret",
480
+ 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."
481
+ },
482
+ {
483
+ type: "select",
484
+ key: MESH_AUTH_MODE_FIELD,
485
+ label: "Agents without the secret",
486
+ 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.",
487
+ default: "transition",
488
+ options: [{
489
+ value: "transition",
490
+ label: "Transition — allow, warn"
491
+ }, {
492
+ value: "enforced",
493
+ label: "Enforced — refuse"
494
+ }]
495
+ },
496
+ {
497
+ type: "info",
498
+ key: "meshAuthStatus",
499
+ label: "Mesh authentication",
500
+ content: describeMeshStatus(getProcessMeshAuthStatus()),
501
+ variant: "info"
502
+ }
503
+ ]
272
504
  }
273
505
  ] };
274
506
  }
@@ -302,6 +534,10 @@ var SystemConfigAddon = class extends BaseAddon {
302
534
  this.ctx.logger.warn("update ignored read-only server-section key", { meta: { key } });
303
535
  continue;
304
536
  }
537
+ if (CIDR_LIST_KEYS.has(key)) {
538
+ const { invalid } = parseCidrList(readCidrSetting(value));
539
+ if (invalid.length > 0) throw new Error(`Not a network address or CIDR range: ${invalid.join(", ")}`);
540
+ }
305
541
  let bucket = perSection.get(section);
306
542
  if (!bucket) {
307
543
  bucket = {};
@@ -323,13 +559,52 @@ var SystemConfigAddon = class extends BaseAddon {
323
559
  }
324
560
  /**
325
561
  * The cluster secret's save path: the value goes to the keychain (owner
326
- * `system`), the row gets the marker; `''` / a view keeps, `$secretClear`
327
- * clears. A value the keychain cannot store is REFUSED — never written
328
- * inline (D787, P5).
562
+ * `system`), the row gets the marker; `''` / a view keeps. `$secretClear` is
563
+ * REFUSED: the secret is mandatory (D802). A value the keychain cannot store
564
+ * is REFUSED — never written inline (D787, P5). `meshAuth` is a plain value.
329
565
  */
330
566
  async writeClusterSection(patch) {
567
+ const { [MESH_AUTH_MODE_FIELD]: mode, ...secretPatch } = patch;
568
+ if (mode !== void 0) {
569
+ if (!isMeshAuthMode(mode)) throw new Error(`system-config: meshAuth must be 'transition' or 'enforced'`);
570
+ await this.writeMeshAuthMode(mode);
571
+ }
572
+ if (Object.keys(secretPatch).length === 0) return;
573
+ if (isSecretClear(secretPatch["clusterSecret"])) throw new Error("The cluster secret is mandatory and cannot be cleared — save a new value to rotate it");
574
+ await this.storeClusterSecret(secretPatch);
575
+ this.ctx.logger.info("Cluster secret updated");
576
+ await this.clusterSecretKeeper?.reload();
577
+ }
578
+ /** `cluster.meshAuth` (D802), then republish it. */
579
+ async writeMeshAuthMode(mode) {
580
+ await this.ctx.settings.setSection("cluster", { [MESH_AUTH_MODE_FIELD]: mode });
581
+ await this.clusterSecretKeeper?.reload();
582
+ }
583
+ /**
584
+ * First boot, or a keychain reset that cleared the marker (D802): a fresh
585
+ * random secret, stored like an operator save. A NEW secret means every
586
+ * agent must be given it, so the mesh goes (back) to `transition`; on a hub
587
+ * with no agent yet, the hub's auto-enforce ends it at once.
588
+ */
589
+ async generateClusterSecret() {
331
590
  const settings = this.ctx.settings;
332
591
  const secrets = this.systemSecrets();
592
+ if (!settings || secrets === null) throw new Error("system-config: no keychain door — the cluster secret cannot be generated");
593
+ const existing = (await secrets.resolve([CLUSTER_SECRET_REF]))[CLUSTER_SECRET_REF];
594
+ if (existing?.kind === "value") {
595
+ await settings.setSection("cluster", { [CLUSTER_SECRET_FIELD]: makeSecretMarker(CLUSTER_SECRET_REF) });
596
+ this.ctx.logger.warn("The cluster secret row was empty but the keychain holds a value — re-adopted it");
597
+ return;
598
+ }
599
+ if (existing?.kind !== "missing") throw new Error(`the keychain cannot answer for the cluster secret (${existing?.kind ?? "no answer"})`);
600
+ const value = randomBytes(GENERATED_SECRET_BYTES).toString("base64url");
601
+ await this.storeClusterSecret({ [CLUSTER_SECRET_FIELD]: value });
602
+ await settings.setSection("cluster", { [MESH_AUTH_MODE_FIELD]: "transition" });
603
+ }
604
+ async storeClusterSecret(patch) {
605
+ const settings = this.ctx.settings;
606
+ if (!settings) throw new Error("system-config: ctx.settings not available");
607
+ const secrets = this.systemSecrets();
333
608
  if (secrets === null) throw new Error("system-config: no keychain door — the cluster secret cannot be stored");
334
609
  const stored = await settings.getSection("cluster");
335
610
  const result = await transformSecretWrite({
@@ -341,10 +616,8 @@ var SystemConfigAddon = class extends BaseAddon {
341
616
  });
342
617
  if (Object.keys(result.patch).length > 0) await settings.setSection("cluster", result.patch);
343
618
  const { orphaned } = await result.afterWrite();
344
- if (orphaned.length > 0) this.ctx.logger.warn("cluster secret cleared, but its old keychain value could not be deleted", { meta: { refs: orphaned } });
345
- this.ctx.logger.info("Cluster secret updated");
346
- await this.clusterSecretKeeper?.reload();
619
+ if (orphaned.length > 0) this.ctx.logger.warn("an old cluster secret value could not be deleted from the keychain", { meta: { refs: orphaned } });
347
620
  }
348
621
  };
349
622
  //#endregion
350
- export { SYSTEM_CONFIG_CLUSTER_SECTION_ID, SystemConfigAddon, SystemConfigAddon as default };
623
+ export { SYSTEM_CONFIG_CLUSTER_SECTION_ID, SystemConfigAddon, SystemConfigAddon as default, parseMeshAuthMode as a, setProcessMeshAuthStatusSource as c, normalizeIp as d, parseCidrList as f, isMeshAuthMode as i, setProcessMeshEnforcer as l, getProcessMeshAuthMode as n, requestProcessMeshEnforcement as o, readCidrSetting as p, getProcessMeshAuthStatus as r, setProcessMeshAuthMode as s, MESH_AUTH_MODE_FIELD as t, createCidrMatcher as u };
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-Cc6KOdY-.js");
6
+ const require_dist = require("../../dist-tnAO12ak.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  let node_path = require("node:path");
9
9
  node_path = require_chunk.__toESM(node_path);
@@ -1,4 +1,4 @@
1
- import { Zt as logDestinationCapability, lr as BaseAddon } from "../../dist-B9oFw2jh.mjs";
1
+ import { Qt as logDestinationCapability, ur as BaseAddon } from "../../dist-CP5TTjwd.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  import * as path$1 from "node:path";
4
4
  import path from "node:path";
@@ -1,10 +1,11 @@
1
1
  const require_chunk = require("./chunk-Cek0wNdY.js");
2
- const require_dist = require("./dist-Cc6KOdY-.js");
2
+ const require_dist = require("./dist-tnAO12ak.js");
3
3
  const require_proc_stat = require("./proc-stat-DL-WUOhl.js");
4
- require("./hub-cap-forward-COCmYaup.js");
4
+ require("./hub-cap-forward-W8oEm4ME.js");
5
5
  let node_crypto = require("node:crypto");
6
6
  node_crypto = require_chunk.__toESM(node_crypto);
7
7
  let _camstack_types_node = require("@camstack/types/node");
8
+ let node_net = require("node:net");
8
9
  let node_fs_promises = require("node:fs/promises");
9
10
  let node_path = require("node:path");
10
11
  node_path = require_chunk.__toESM(node_path);
@@ -23,7 +24,6 @@ let node_vm = require("node:vm");
23
24
  node_vm = require_chunk.__toESM(node_vm);
24
25
  let _camstack_types_addon = require("@camstack/types/addon");
25
26
  let node_events = require("node:events");
26
- let node_net = require("node:net");
27
27
  let _trpc_client = require("@trpc/client");
28
28
  //#region src/kernel/event-plane-report.ts
29
29
  /**
@@ -3223,11 +3223,14 @@ function isCredentialKeyName(key) {
3223
3223
  return SECRET_KEY_SUBSTRINGS.some((needle) => lower.includes(needle));
3224
3224
  }
3225
3225
  var URL_IN_TEXT = /\b[a-z][a-z0-9+.-]*:\/\/[^\s"'<>`]+/gi;
3226
+ /** A root-relative path WITH a query, as request logs print it. */
3227
+ var RELATIVE_URL_IN_TEXT = /(^|[\s"'(=])(\/[^\s"'<>`?]*\?[^\s"'<>`]+)/g;
3226
3228
  var CREDENTIAL_FLAG = /(--(?:token|auth-?key|password|passwd|pass|secret|api-?key|access-?token|refresh-?token)(?:=|\s+))(\S+)/gi;
3227
3229
  /** Redact one free-form string. */
3228
3230
  function redactLogText(text) {
3229
3231
  let out = require_dist.scrubKnownSecrets(text);
3230
3232
  if (out.includes("://")) out = out.replace(URL_IN_TEXT, (url) => require_dist.maskUrlCredentials(url));
3233
+ if (out.includes("/") && out.includes("?") && out.includes("=")) out = out.replace(RELATIVE_URL_IN_TEXT, (_m, lead, path) => `${lead}${require_dist.maskUrlCredentials(path)}`);
3231
3234
  if (out.includes("--")) out = out.replace(CREDENTIAL_FLAG, (_m, flag, value) => value.startsWith("‹secret:") ? `${flag}${value}` : `${flag}${REDACTED}`);
3232
3235
  return out;
3233
3236
  }
@@ -1,9 +1,10 @@
1
- import { Cr as createEvent, Vr as scrubKnownSecrets, en as maskUrlCredentials, qr as EventCategory$1 } from "./dist-B9oFw2jh.mjs";
1
+ import { Tr as createEvent, ei as EventCategory$1, qr as scrubKnownSecrets, tn as maskUrlCredentials } from "./dist-CP5TTjwd.mjs";
2
2
  import { a as parseProcStat, i as nodeProcReader } from "./proc-stat-CbC55Kqi.mjs";
3
- import "./hub-cap-forward-CH6_VWVO.mjs";
3
+ import "./hub-cap-forward-CDiN1p98.mjs";
4
4
  import * as crypto$1 from "node:crypto";
5
5
  import { randomBytes, randomUUID } from "node:crypto";
6
6
  import { ensureBinary, ensureFfmpeg, ensurePython, installPythonPackages, installPythonRequirements } from "@camstack/types/node";
7
+ import { connect, createConnection, createServer } from "node:net";
7
8
  import { mkdir, rm, stat, unlink } from "node:fs/promises";
8
9
  import * as path$1 from "node:path";
9
10
  import { dirname, isAbsolute, join } from "node:path";
@@ -13,14 +14,13 @@ import * as fs from "node:fs";
13
14
  import { createReadStream, createWriteStream, existsSync, readFileSync, readdirSync } from "node:fs";
14
15
  import { execFile } from "node:child_process";
15
16
  import { promisify } from "node:util";
16
- import { createServer, request } from "node:http";
17
+ import { createServer as createServer$1, request } from "node:http";
17
18
  import * as v8 from "node:v8";
18
19
  import { Session } from "node:inspector/promises";
19
20
  import { monitorEventLoopDelay } from "node:perf_hooks";
20
21
  import * as vm from "node:vm";
21
22
  import { BOOT_RECOVERY_BACKOFF_MS, DATAPLANE_SECRET_HEADER, DEVICE_CHILDREN_BATCH_MAX, DeviceType, DisposerChain, EventCategory, PEER_BYTES_CHUNK_BYTES, PEER_BYTES_MAX_OUTSTANDING, PEER_BYTES_STALL_MS, PEER_BYTES_TICKET_TTL_MS, ReadinessRegistry, asJsonObject, asString, collectZodSecretPaths, createDeviceProxy, createEventBusSliceSource, createSecretsAccess, deviceOpsCapability, emitReadiness, errMsg, expandCapMethods, readNodePin, scopeKey, sleep, withCapInputDefaults } from "@camstack/types/addon";
22
23
  import { once } from "node:events";
23
- import { connect, createConnection, createServer as createServer$1 } from "node:net";
24
24
  import { TRPCClientError, createTRPCClient } from "@trpc/client";
25
25
  //#region src/kernel/event-plane-report.ts
26
26
  /**
@@ -2545,7 +2545,7 @@ function createAddonDataPlaneFacility(args) {
2545
2545
  };
2546
2546
  const ensureServer = async () => {
2547
2547
  if (server) return;
2548
- const s = createServer((req, res) => route(req, res));
2548
+ const s = createServer$1((req, res) => route(req, res));
2549
2549
  await new Promise((resolve, reject) => {
2550
2550
  s.once("error", reject);
2551
2551
  s.listen(0, bindHost, () => resolve());
@@ -2764,7 +2764,7 @@ function createAddonPeerBytesFacility(args) {
2764
2764
  const listen = () => {
2765
2765
  if (listening !== null) return listening;
2766
2766
  listening = new Promise((resolve, reject) => {
2767
- const created = createServer((req, res) => {
2767
+ const created = createServer$1((req, res) => {
2768
2768
  route(req, res);
2769
2769
  });
2770
2770
  created.on("error", (err) => {
@@ -3220,11 +3220,14 @@ function isCredentialKeyName(key) {
3220
3220
  return SECRET_KEY_SUBSTRINGS.some((needle) => lower.includes(needle));
3221
3221
  }
3222
3222
  var URL_IN_TEXT = /\b[a-z][a-z0-9+.-]*:\/\/[^\s"'<>`]+/gi;
3223
+ /** A root-relative path WITH a query, as request logs print it. */
3224
+ var RELATIVE_URL_IN_TEXT = /(^|[\s"'(=])(\/[^\s"'<>`?]*\?[^\s"'<>`]+)/g;
3223
3225
  var CREDENTIAL_FLAG = /(--(?:token|auth-?key|password|passwd|pass|secret|api-?key|access-?token|refresh-?token)(?:=|\s+))(\S+)/gi;
3224
3226
  /** Redact one free-form string. */
3225
3227
  function redactLogText(text) {
3226
3228
  let out = scrubKnownSecrets(text);
3227
3229
  if (out.includes("://")) out = out.replace(URL_IN_TEXT, (url) => maskUrlCredentials(url));
3230
+ if (out.includes("/") && out.includes("?") && out.includes("=")) out = out.replace(RELATIVE_URL_IN_TEXT, (_m, lead, path) => `${lead}${maskUrlCredentials(path)}`);
3228
3231
  if (out.includes("--")) out = out.replace(CREDENTIAL_FLAG, (_m, flag, value) => value.startsWith("‹secret:") ? `${flag}${value}` : `${flag}${REDACTED}`);
3229
3232
  return out;
3230
3233
  }
@@ -7459,7 +7462,7 @@ var UdsLocalTransportServer = class {
7459
7462
  await unlink(this.path);
7460
7463
  } catch {}
7461
7464
  await new Promise((resolve, reject) => {
7462
- const srv = createServer$1((socket) => {
7465
+ const srv = createServer((socket) => {
7463
7466
  const ch = new SocketChannel(socket);
7464
7467
  this.channels.add(ch);
7465
7468
  ch.onClose(() => this.channels.delete(ch));
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-Cc6KOdY-.js");
1
+ const require_dist = require("./dist-tnAO12ak.js");
2
2
  let node_crypto = require("node:crypto");
3
3
  //#region src/kernel/moleculer/cluster-secret.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { br as buildSecretRef } from "./dist-B9oFw2jh.mjs";
1
+ import { Sr as buildSecretRef } from "./dist-CP5TTjwd.mjs";
2
2
  import { createHash, timingSafeEqual } from "node:crypto";
3
3
  //#region src/kernel/moleculer/cluster-secret.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-Cc6KOdY-.js");
1
+ const require_dist = require("./dist-tnAO12ak.js");
2
2
  //#region src/builtins/core-blocks/composition-sources.ts
3
3
  /**
4
4
  * Describe what a composition reads and writes for SAVE-time validation
@@ -1,4 +1,4 @@
1
- import { cr as errMsg, tr as compositionSourceKey } from "./dist-B9oFw2jh.mjs";
1
+ import { lr as errMsg, nr as compositionSourceKey } from "./dist-CP5TTjwd.mjs";
2
2
  //#region src/builtins/core-blocks/composition-sources.ts
3
3
  /**
4
4
  * Describe what a composition reads and writes for SAVE-time validation