@camstack/system 1.2.116 → 1.2.118

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 (63) hide show
  1. package/dist/addon-utils.d.ts +1 -1
  2. package/dist/addon-utils.js +1 -1
  3. package/dist/addon-utils.mjs +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  5. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  7. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  8. package/dist/builtins/alerts/alerts.addon.js +1 -1
  9. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  11. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  12. package/dist/builtins/console-logging/index.js +1 -1
  13. package/dist/builtins/console-logging/index.mjs +1 -1
  14. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  15. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  16. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  17. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  18. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  19. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  20. package/dist/builtins/hub-forwarder/index.js +1 -1
  21. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  22. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  23. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  24. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  25. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  26. package/dist/builtins/local-network/ipv6-lan.d.ts +26 -0
  27. package/dist/builtins/local-network/local-network.addon.d.ts +50 -14
  28. package/dist/builtins/local-network/local-network.addon.js +254 -38
  29. package/dist/builtins/local-network/local-network.addon.mjs +248 -39
  30. package/dist/builtins/loki-logging/index.js +1 -1
  31. package/dist/builtins/loki-logging/index.mjs +1 -1
  32. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  33. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  34. package/dist/builtins/platform-probe/index.js +1 -1
  35. package/dist/builtins/platform-probe/index.mjs +1 -1
  36. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  37. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  38. package/dist/builtins/snapshot/index.js +1 -1
  39. package/dist/builtins/snapshot/index.mjs +1 -1
  40. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  41. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  42. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -2
  43. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +2 -2
  44. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  45. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  46. package/dist/builtins/system-config/system-config.addon.js +1 -1
  47. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  48. package/dist/builtins/winston-logging/index.js +1 -1
  49. package/dist/builtins/winston-logging/index.mjs +1 -1
  50. package/dist/{dist-Cri60Jd0.js → dist-C2_1HCpW.js} +149 -23
  51. package/dist/{dist-BpzbLPel.mjs → dist-DCTpBXm4.mjs} +149 -23
  52. package/dist/download/model-downloader.d.ts +18 -1
  53. package/dist/{file-data-plane-CuE_hBli.mjs → file-data-plane-BhKdxJgf.mjs} +49 -13
  54. package/dist/{file-data-plane-DUHPHa-Y.js → file-data-plane-DO8KbxCe.js} +49 -13
  55. package/dist/http/lan-http-bind.d.ts +12 -1
  56. package/dist/index.d.ts +1 -1
  57. package/dist/index.js +4 -3
  58. package/dist/index.mjs +4 -4
  59. package/dist/{lan-http-bind-CZ4EBeD_.js → lan-http-bind-DmgpFP6_.js} +53 -11
  60. package/dist/{lan-http-bind-BDJm4d-K.mjs → lan-http-bind-jKrj6OjQ.mjs} +48 -12
  61. package/dist/{retired-settings-keys-SrxMH3OT.mjs → retired-settings-keys-Czd76yLZ.mjs} +1 -1
  62. package/dist/{retired-settings-keys-BVAxT5XL.js → retired-settings-keys-DHRXLMPn.js} +1 -1
  63. package/package.json +1 -1
@@ -1,8 +1,9 @@
1
- import { Gt as EventCategory, Y as localNetworkCapability, yt as BaseAddon } from "../../dist-BpzbLPel.mjs";
2
- import { a as readLanHttpState, c as readTlsAccessStatus, d as writeTlsMode, f as validateUploadedTls, h as reissueTlsLeaf, l as readTlsMode, n as applyLanHttp, p as ensureTlsCert, s as readExtraSans, t as DEFAULT_LAN_HTTP_PORT, u as writeExtraSans } from "../../lan-http-bind-BDJm4d-K.mjs";
1
+ import { Gt as EventCategory, Y as localNetworkCapability, yt as BaseAddon } from "../../dist-DCTpBXm4.mjs";
2
+ import { c as readExtraSans, d as writeExtraSans, f as writeTlsMode, g as reissueTlsLeaf, l as readTlsAccessStatus, m as ensureTlsCert, o as readLanHttpState, p as validateUploadedTls, r as applyLanHttp, t as DEFAULT_LAN_HTTP_PORT, u as readTlsMode } from "../../lan-http-bind-jKrj6OjQ.mjs";
3
3
  import { mkdir, writeFile } from "node:fs/promises";
4
4
  import { join } from "node:path";
5
5
  import * as os from "node:os";
6
+ import { readFileSync } from "node:fs";
6
7
  import { isIP } from "node:net";
7
8
  //#region src/builtins/local-network/local-hostname.ts
8
9
  var DNS_NAME = /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])$/;
@@ -38,6 +39,111 @@ function parseLocalHostname(raw) {
38
39
  };
39
40
  }
40
41
  //#endregion
42
+ //#region src/builtins/local-network/ipv6-lan.ts
43
+ /**
44
+ * IPv6 helpers for LAN advertisement: prefix matching (privacy addresses
45
+ * rotate inside a /64), Linux `/proc/net/if_inet6` flags, and the gate
46
+ * that keeps temporary GUA off the connection candidate list.
47
+ */
48
+ var IFA_F_TEMPORARY = 1;
49
+ var IFA_F_DEPRECATED = 32;
50
+ var IFA_F_TENTATIVE = 64;
51
+ /** First 64 bits, canonical hex groups, or null if the address is not IPv6. */
52
+ function ipv6Prefix64Key(address) {
53
+ const groups = parseIpv6Groups(address);
54
+ if (groups === null) return null;
55
+ return groups.slice(0, 4).map((g) => g.toString(16).padStart(4, "0")).join(":");
56
+ }
57
+ /** 32-char lowercase hex (same encoding as `/proc/net/if_inet6`). */
58
+ function ipv6ToHex32(address) {
59
+ const groups = parseIpv6Groups(address);
60
+ if (groups === null) return null;
61
+ return groups.map((g) => g.toString(16).padStart(4, "0")).join("");
62
+ }
63
+ /**
64
+ * Parse Linux `if_inet6` into hex32 → flags. Unknown / malformed lines
65
+ * are skipped. Keys match {@link ipv6ToHex32}.
66
+ */
67
+ function parseProcIfInet6(text) {
68
+ const out = /* @__PURE__ */ new Map();
69
+ for (const line of text.split("\n")) {
70
+ const trimmed = line.trim();
71
+ if (trimmed === "") continue;
72
+ const parts = trimmed.split(/\s+/);
73
+ const hex = parts[0]?.toLowerCase();
74
+ const flagsRaw = parts[4];
75
+ if (!hex || hex.length !== 32 || flagsRaw === void 0) continue;
76
+ const flags = Number.parseInt(flagsRaw, 16);
77
+ if (!Number.isFinite(flags)) continue;
78
+ out.set(hex, flags);
79
+ }
80
+ return out;
81
+ }
82
+ /**
83
+ * Whether this IPv6 should be auto-seeded / advertised.
84
+ * `flags === undefined` means the platform did not tell us (macOS, tests
85
+ * without a proc snapshot) — keep every non-link-local plausible address.
86
+ *
87
+ * On Linux, drop addresses marked TEMPORARY / DEPRECATED / TENTATIVE, and
88
+ * `flags === 0` which is how this kernel reports RFC 4941 privacy temps
89
+ * (the stable GUA is PERMANENT|NODAD). Do **not** require PERMANENT —
90
+ * stateful DHCPv6 GUA often has only NODAD (0x02).
91
+ */
92
+ function shouldAdvertiseIpv6(address, flags) {
93
+ const a = address.toLowerCase().split("%")[0] ?? address;
94
+ if (a === "::" || a === "::1") return false;
95
+ if (a.startsWith("fe80:")) return false;
96
+ if (a.startsWith("ff")) return false;
97
+ if (flags === void 0) return true;
98
+ if ((flags & IFA_F_TEMPORARY) !== 0) return false;
99
+ if ((flags & IFA_F_DEPRECATED) !== 0) return false;
100
+ if ((flags & IFA_F_TENTATIVE) !== 0) return false;
101
+ if (flags === 0) return false;
102
+ return true;
103
+ }
104
+ function ipv6AllowlistMatches(address, allowed) {
105
+ if (allowed.includes(address)) return true;
106
+ const key = ipv6Prefix64Key(address);
107
+ if (key === null) return false;
108
+ for (const entry of allowed) {
109
+ if (!entry.includes(":")) continue;
110
+ if (ipv6Prefix64Key(entry) === key) return true;
111
+ }
112
+ return false;
113
+ }
114
+ function parseIpv6Groups(address) {
115
+ const raw = address.toLowerCase().split("%")[0];
116
+ if (!raw || raw.includes(".")) return null;
117
+ const sides = raw.split("::");
118
+ if (sides.length > 2) return null;
119
+ const parsePart = (part) => {
120
+ if (part === void 0 || part === "") return [];
121
+ const groups = part.split(":");
122
+ const out = [];
123
+ for (const g of groups) {
124
+ if (g === "") return null;
125
+ if (!/^[0-9a-f]{1,4}$/.test(g)) return null;
126
+ out.push(Number.parseInt(g, 16));
127
+ }
128
+ return out;
129
+ };
130
+ if (sides.length === 1) {
131
+ const groups = parsePart(sides[0]);
132
+ if (groups === null || groups.length !== 8) return null;
133
+ return groups;
134
+ }
135
+ const left = parsePart(sides[0]);
136
+ const right = parsePart(sides[1]);
137
+ if (left === null || right === null) return null;
138
+ const fill = 8 - left.length - right.length;
139
+ if (fill < 0) return null;
140
+ return [
141
+ ...left,
142
+ ...Array(fill).fill(0),
143
+ ...right
144
+ ];
145
+ }
146
+ //#endregion
41
147
  //#region src/builtins/local-network/local-network.addon.ts
42
148
  /**
43
149
  * local-network — hub-only system cap implementation.
@@ -63,6 +169,8 @@ function parseLocalHostname(raw) {
63
169
  */
64
170
  var MESH_PROVIDER_ADDON_IDS = ["tailscale-client"];
65
171
  var POLL_INTERVAL_MS = 3e4;
172
+ /** Drop rotated IPv6 / dead extras from the persisted allowlist. */
173
+ var ALLOWLIST_RECONCILE_INTERVAL_MS = 3600 * 1e3;
66
174
  /**
67
175
  * Compiled-in boot default — mirrors `CONFIG_DEFAULTS.server.port` in
68
176
  * `server/backend/src/boot/boot-config.ts`. Used only when neither the live
@@ -71,6 +179,8 @@ var POLL_INTERVAL_MS = 3e4;
71
179
  var DEFAULT_HUB_PORT = 4443;
72
180
  var LocalNetworkAddon = class extends BaseAddon {
73
181
  pollTimer = null;
182
+ allowlistTimer = null;
183
+ allowlistReconcilePending = false;
74
184
  lastSnapshotKey = "";
75
185
  /** Optional public hostname tracked from `NetworkTunnelStarted`/
76
186
  * `Stopped` events on the bus. */
@@ -87,6 +197,7 @@ var LocalNetworkAddon = class extends BaseAddon {
87
197
  allowedAddresses: [],
88
198
  bootSeeded: false,
89
199
  notificationBaseUrl: "",
200
+ viewerBaseUrls: [],
90
201
  httpEnabled: true,
91
202
  httpPort: DEFAULT_LAN_HTTP_PORT,
92
203
  localHostname: ""
@@ -107,6 +218,31 @@ var LocalNetworkAddon = class extends BaseAddon {
107
218
  if (mesh !== null && mesh.host !== "") return `https://${mesh.host}:${mesh.port}`;
108
219
  return null;
109
220
  }
221
+ async snapshotViewerEndpoints() {
222
+ const selected = selectViewerEndpoints(await this.buildAdvertisedEndpoints(false, false, "http", (await this.resolveAdvertisedListenPort(null)).port), this.config.viewerBaseUrls);
223
+ return {
224
+ baseUrls: this.config.viewerBaseUrls,
225
+ resolved: selected.map((e) => e.baseUrl)
226
+ };
227
+ }
228
+ /**
229
+ * Palette shown in the Network tab: LAN IPv4, stable LAN IPv6, public,
230
+ * mesh. The viewer HTTP/WS race is a subset (`selectViewerEndpoints`) —
231
+ * IPv6 stays in this list because ICE/WebRTC gathers it regardless.
232
+ */
233
+ async buildAdvertisedEndpoints(includeLoopback, ipv4Only, scheme, httpsPort) {
234
+ const allow = this.config.allowedAddresses;
235
+ const interfaces = applyAllowlist(this.enumerate(), allow);
236
+ await this.reconcileMeshEndpoint();
237
+ const httpState = readLanHttpState();
238
+ return buildEndpoints(interfaces, httpsPort, includeLoopback, ipv4Only, this.publicHostname, scheme, this.meshEndpoint, {
239
+ enabled: this.config.httpEnabled !== false,
240
+ listening: httpState.listening,
241
+ httpPort: this.resolvedHttpPort(),
242
+ httpsPort,
243
+ localHostname: this.config.localHostname || null
244
+ });
245
+ }
110
246
  async onInitialize() {
111
247
  if (!this.config.bootSeeded) {
112
248
  const seed = autoSeedAllowlist(this.enumerate());
@@ -116,6 +252,7 @@ var LocalNetworkAddon = class extends BaseAddon {
116
252
  });
117
253
  this.ctx.logger.info("local-network: first-boot auto-seed", { meta: { addresses: seed } });
118
254
  }
255
+ await this.persistReconciledAllowlist();
119
256
  const provider = {
120
257
  list: async () => ({
121
258
  interfaces: this.enumerate(),
@@ -123,27 +260,16 @@ var LocalNetworkAddon = class extends BaseAddon {
123
260
  }),
124
261
  getPreferred: async () => pickPreferred(applyAllowlist(this.enumerate(), this.config.allowedAddresses)),
125
262
  getConnectionEndpoints: async (input) => {
126
- const includeLoopback = input.includeLoopback ?? true;
127
- const ipv4Only = input.ipv4Only ?? false;
263
+ const includeLoopback = input.includeLoopback ?? false;
128
264
  const scheme = input.scheme ?? "http";
129
- const allow = this.config.allowedAddresses;
130
- const interfaces = applyAllowlist(this.enumerate(), allow);
131
265
  const localPort = await this.resolveAdvertisedListenPort(input.port ?? null);
132
266
  if (input.port !== void 0 && input.port !== localPort.port) this.ctx.logger.info("local-network: caller port hint overridden", { meta: {
133
267
  hint: input.port,
134
268
  advertised: localPort.port,
135
269
  source: localPort.source
136
270
  } });
137
- await this.reconcileMeshEndpoint();
138
- const httpState = readLanHttpState();
139
271
  return {
140
- endpoints: buildEndpoints(interfaces, localPort.port, includeLoopback, ipv4Only, this.publicHostname, scheme, this.meshEndpoint, {
141
- enabled: this.config.httpEnabled !== false,
142
- listening: httpState.listening,
143
- httpPort: this.resolvedHttpPort(),
144
- httpsPort: localPort.port,
145
- localHostname: this.config.localHostname || null
146
- }),
272
+ endpoints: await this.buildAdvertisedEndpoints(includeLoopback, input.ipv4Only ?? false, scheme, localPort.port),
147
273
  localPort
148
274
  };
149
275
  },
@@ -159,6 +285,16 @@ var LocalNetworkAddon = class extends BaseAddon {
159
285
  resolved: this.resolveNotificationEndpoint()
160
286
  };
161
287
  },
288
+ getViewerEndpoints: async () => this.snapshotViewerEndpoints(),
289
+ setViewerEndpoints: async ({ baseUrls }) => {
290
+ const cleaned = [...new Set(baseUrls.filter((u) => u.length > 0))];
291
+ await this.updateGlobalSettings({ viewerBaseUrls: cleaned });
292
+ this.ctx.logger.info("local-network: viewer endpoints set", { meta: {
293
+ count: cleaned.length,
294
+ auto: cleaned.length === 0
295
+ } });
296
+ return this.snapshotViewerEndpoints();
297
+ },
162
298
  getAllowedAddresses: async () => ({ addresses: this.config.allowedAddresses }),
163
299
  resetAllowlistToBestMatch: async () => {
164
300
  const seed = autoSeedAllowlist(this.enumerate());
@@ -210,6 +346,10 @@ var LocalNetworkAddon = class extends BaseAddon {
210
346
  this.lastSnapshotKey = snapshotKey(this.enumerate());
211
347
  this.pollTimer = setInterval(() => this.detectChanges(), POLL_INTERVAL_MS);
212
348
  this.pollTimer.unref?.();
349
+ this.allowlistTimer = setInterval(() => {
350
+ this.persistReconciledAllowlist();
351
+ }, ALLOWLIST_RECONCILE_INTERVAL_MS);
352
+ this.allowlistTimer.unref?.();
213
353
  this.ctx.eventBus?.subscribe({ category: EventCategory.NetworkTunnelStarted }, (event) => {
214
354
  const data = event.data ?? {};
215
355
  if (typeof data.url === "string") try {
@@ -285,6 +425,10 @@ var LocalNetworkAddon = class extends BaseAddon {
285
425
  clearInterval(this.pollTimer);
286
426
  this.pollTimer = null;
287
427
  }
428
+ if (this.allowlistTimer) {
429
+ clearInterval(this.allowlistTimer);
430
+ this.allowlistTimer = null;
431
+ }
288
432
  }
289
433
  /**
290
434
  * Other hub addons (e.g. cloudflare-tunnel) signal the active public
@@ -418,7 +562,26 @@ var LocalNetworkAddon = class extends BaseAddon {
418
562
  }
419
563
  }
420
564
  enumerate() {
421
- return enumerateOsInterfaces(os.networkInterfaces());
565
+ return enumerateOsInterfaces(os.networkInterfaces(), readLinuxIpv6Flags());
566
+ }
567
+ /**
568
+ * Replace rotated IPv6 and drop addresses that no longer exist, without
569
+ * clearing operator extras that are still live (docker/vpn opt-in).
570
+ */
571
+ async persistReconciledAllowlist(interfaces = this.enumerate()) {
572
+ if (this.allowlistReconcilePending) return;
573
+ this.allowlistReconcilePending = true;
574
+ try {
575
+ const next = reconcileAllowlist(this.config.allowedAddresses, interfaces);
576
+ if (sameAddressSet(this.config.allowedAddresses, next)) return;
577
+ await this.updateGlobalSettings({
578
+ allowedAddresses: next,
579
+ bootSeeded: true
580
+ });
581
+ this.ctx.logger.info("local-network: allowlist reconciled", { meta: { addresses: next } });
582
+ } finally {
583
+ this.allowlistReconcilePending = false;
584
+ }
422
585
  }
423
586
  detectChanges() {
424
587
  const interfaces = this.enumerate();
@@ -429,6 +592,7 @@ var LocalNetworkAddon = class extends BaseAddon {
429
592
  key
430
593
  } });
431
594
  this.lastSnapshotKey = key;
595
+ this.persistReconciledAllowlist(interfaces);
432
596
  this.ctx.eventBus?.emit({
433
597
  id: `local-network-changed-${Date.now()}`,
434
598
  timestamp: /* @__PURE__ */ new Date(),
@@ -477,7 +641,7 @@ function validPort(value) {
477
641
  if (value === null) return null;
478
642
  return Number.isInteger(value) && value >= 1 && value <= 65535 ? value : null;
479
643
  }
480
- function enumerateOsInterfaces(ifaces) {
644
+ function enumerateOsInterfaces(ifaces, ipv6Flags) {
481
645
  const raw = [];
482
646
  for (const [name, addrs] of Object.entries(ifaces)) {
483
647
  if (!addrs) continue;
@@ -497,8 +661,12 @@ function enumerateOsInterfaces(ifaces) {
497
661
  const classified = raw.map((r) => {
498
662
  const kind = classifyKind(r.name, r.address, r.internal);
499
663
  const reachableKind = kind === "lan" || kind === "wifi";
500
- const plausible = !r.internal && reachableKind && isPlausibleAutoSeed(r.address, r.family);
501
- const plausibleReason = plausible ? "" : explainNonPlausible({
664
+ const hex = r.family === "IPv6" ? ipv6ToHex32(r.address) : null;
665
+ const flags = hex !== null ? ipv6Flags?.get(hex) : void 0;
666
+ const rangeOk = isPlausibleAutoSeed(r.address, r.family);
667
+ const stableV6 = r.family !== "IPv6" || shouldAdvertiseIpv6(r.address, flags);
668
+ const plausible = !r.internal && reachableKind && rangeOk && stableV6;
669
+ const plausibleReason = plausible ? "" : r.family === "IPv6" && rangeOk && !stableV6 ? "IPv6 temporary / privacy address — rotates and is not advertised." : explainNonPlausible({
502
670
  kind,
503
671
  family: r.family,
504
672
  address: r.address,
@@ -527,13 +695,14 @@ function enumerateOsInterfaces(ifaces) {
527
695
  /**
528
696
  * Filter the interface list by the operator's allowlist. Empty
529
697
  * allowlist = no-op (every interface passes); otherwise only entries
530
- * whose `address` matches an allowlist entry remain. Loopback always
531
- * survives so the SDK keeps `127.0.0.1` as the last-resort fallback.
698
+ * whose `address` matches an allowlist entry remain. IPv6 matches the
699
+ * allowlisted /64 so a rotated privacy address does not hide the live
700
+ * GUA. Loopback is not an advertised client route and is not forced
701
+ * through.
532
702
  */
533
703
  function applyAllowlist(interfaces, allowed) {
534
704
  if (allowed.length === 0) return interfaces;
535
- const set = new Set(allowed);
536
- return interfaces.filter((i) => i.kind === "loopback" || set.has(i.address));
705
+ return interfaces.filter((i) => ipv6AllowlistMatches(i.address, allowed) || allowed.includes(i.address));
537
706
  }
538
707
  /**
539
708
  * Rank interfaces and pick the auto-selected outbound one. See the
@@ -592,6 +761,13 @@ function buildEndpoints(interfaces, port, includeLoopback, ipv4Only, publicHostn
592
761
  if (iface.address.startsWith("169.254.")) continue;
593
762
  emit(iface, "lan-ipv4", `${formatKind(iface.kind)} — ${iface.name}`, `${lanScheme}://${iface.address}:${lanPort}`, 10 + priority++);
594
763
  }
764
+ if (!ipv4Only) for (const iface of interfaces) {
765
+ if (iface.internal || iface.family !== "IPv6") continue;
766
+ if (iface.kind === "loopback") continue;
767
+ if (iface.address.toLowerCase().startsWith("fe80:")) continue;
768
+ if (!iface.plausible) continue;
769
+ emit(iface, "lan-ipv6", `${formatKind(iface.kind)} — ${iface.name} (IPv6)`, `${lanScheme}://[${iface.address}]:${lanPort}`, 10 + priority++);
770
+ }
595
771
  if (publicHostname) out.push({
596
772
  label: "Public tunnel",
597
773
  baseUrl: `https://${publicHostname}`,
@@ -610,15 +786,6 @@ function buildEndpoints(interfaces, port, includeLoopback, ipv4Only, publicHostn
610
786
  plausibleReason: "",
611
787
  priority: 150
612
788
  });
613
- if (!ipv4Only) {
614
- let v6prio = 200;
615
- for (const iface of interfaces) {
616
- if (iface.internal || iface.family !== "IPv6") continue;
617
- if (iface.kind === "loopback") continue;
618
- if (iface.address.startsWith("fe80:")) continue;
619
- emit(iface, "lan-ipv6", `${formatKind(iface.kind)} — ${iface.name} (IPv6)`, `${lanScheme}://[${iface.address}]:${lanPort}`, v6prio++);
620
- }
621
- }
622
789
  if (includeLoopback) out.push({
623
790
  label: "Loopback",
624
791
  baseUrl: `${scheme}://127.0.0.1:${port}`,
@@ -630,22 +797,39 @@ function buildEndpoints(interfaces, port, includeLoopback, ipv4Only, publicHostn
630
797
  });
631
798
  return out.toSorted((a, b) => a.priority - b.priority);
632
799
  }
800
+ /** AUTO viewer race: LAN IPv4 + the public tunnel. Never IPv6, mesh, or loopback. */
801
+ function isDefaultViewerEndpoint(endpoint) {
802
+ if (endpoint.kind === "lan-ipv4") return true;
803
+ return endpoint.kind === "public" && endpoint.interfaceKind === "public";
804
+ }
805
+ /**
806
+ * The URLs the SDK should race for API / viewer access.
807
+ *
808
+ * Empty `viewerBaseUrls` = AUTO (IPv4 + public tunnel). A non-empty list is
809
+ * the operator's pin; if none of those URLs are in the current palette,
810
+ * fall back to AUTO rather than racing an empty set.
811
+ */
812
+ function selectViewerEndpoints(endpoints, viewerBaseUrls) {
813
+ const auto = endpoints.filter(isDefaultViewerEndpoint);
814
+ if (viewerBaseUrls.length === 0) return auto;
815
+ const wanted = new Set(viewerBaseUrls);
816
+ const explicit = endpoints.filter((e) => wanted.has(e.baseUrl));
817
+ return explicit.length > 0 ? explicit : auto;
818
+ }
633
819
  /**
634
820
  * First-boot heuristic: which addresses should the allowlist start
635
821
  * with? Includes LAN + Wi-Fi IPv4 addresses + plausible IPv6:
636
822
  *
637
823
  * - **IPv4**: skip link-local (`169.254.*`), keep the rest.
638
824
  * - **IPv6**: skip link-local (`fe80::*`), unspecified, and
639
- * multicast. Keep ULAs (`fc00::/7` `fc??:` / `fd??:`) and Global
640
- * Unicast addresses (`2000::/3` `2???`/`3???`). Privacy-extension
641
- * temporary addresses get included by default; the operator can
642
- * prune them from the Network Addresses tab if the rotating IPs
643
- * become a nuisance.
825
+ * multicast. Keep ULAs (`fc00::/7`) and Global Unicast (`2000::/3`).
826
+ * Privacy-extension temporary addresses are skipped when Linux flags
827
+ * mark them non-permanent (`plausible === false`).
644
828
  *
645
829
  * Skips docker/vpn/loopback/other entirely — those stay opt-in.
646
830
  */
647
831
  function autoSeedAllowlist(interfaces) {
648
- return [...new Set(interfaces.filter((i) => !i.internal && (i.kind === "lan" || i.kind === "wifi") && isPlausibleAutoSeed(i.address, i.family)).map((i) => i.address))];
832
+ return [...new Set(interfaces.filter((i) => !i.internal && (i.kind === "lan" || i.kind === "wifi") && isPlausibleAutoSeed(i.address, i.family) && (i.family !== "IPv6" || i.plausible)).map((i) => i.address))];
649
833
  }
650
834
  /**
651
835
  * Per-interface tooltip text surfaced on the "Unlikely usable" badge.
@@ -734,5 +918,30 @@ function formatKind(kind) {
734
918
  case "other": return "Other";
735
919
  }
736
920
  }
921
+ /**
922
+ * Rewrite a persisted allowlist against the live interface set:
923
+ * current auto-seed (LAN/Wi-Fi, stable IPv6) plus still-live operator
924
+ * extras (docker/vpn). Empty stays empty — that is "unrestricted".
925
+ */
926
+ function reconcileAllowlist(current, interfaces) {
927
+ if (current.length === 0) return [];
928
+ const live = new Set(interfaces.map((i) => i.address));
929
+ const seed = autoSeedAllowlist(interfaces);
930
+ const seedSet = new Set(seed);
931
+ const extras = current.filter((a) => live.has(a) && !seedSet.has(a) && !a.includes(":"));
932
+ return [...new Set([...seed, ...extras])];
933
+ }
934
+ function sameAddressSet(a, b) {
935
+ if (a.length !== b.length) return false;
936
+ const set = new Set(a);
937
+ return b.every((addr) => set.has(addr));
938
+ }
939
+ function readLinuxIpv6Flags() {
940
+ try {
941
+ return parseProcIfInet6(readFileSync("/proc/net/if_inet6", "utf8"));
942
+ } catch {
943
+ return;
944
+ }
945
+ }
737
946
  //#endregion
738
- export { LocalNetworkAddon, applyAllowlist, autoSeedAllowlist, buildEndpoints, classifyKind, enumerateOsInterfaces, explainNonPlausible, isPlausibleAutoSeed, pickPreferred, prefixLen, resolveAdvertisedPort };
947
+ export { ALLOWLIST_RECONCILE_INTERVAL_MS, LocalNetworkAddon, applyAllowlist, autoSeedAllowlist, buildEndpoints, classifyKind, enumerateOsInterfaces, explainNonPlausible, ipv6Prefix64Key, isDefaultViewerEndpoint, isPlausibleAutoSeed, parseProcIfInet6, pickPreferred, prefixLen, reconcileAllowlist, resolveAdvertisedPort, selectViewerEndpoints, shouldAdvertiseIpv6 };
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-Cri60Jd0.js");
6
+ const require_dist = require("../../dist-C2_1HCpW.js");
7
7
  //#region src/builtins/loki-logging/loki-payload.ts
8
8
  /**
9
9
  * Loki rejects a label name that is not a valid Prometheus label
@@ -1,4 +1,4 @@
1
- import { X as logDestinationCapability, yt as BaseAddon } from "../../dist-BpzbLPel.mjs";
1
+ import { X as logDestinationCapability, yt as BaseAddon } from "../../dist-DCTpBXm4.mjs";
2
2
  //#region src/builtins/loki-logging/loki-payload.ts
3
3
  /**
4
4
  * Loki rejects a label name that is not a valid Prometheus label
@@ -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-Cri60Jd0.js");
6
+ const require_dist = require("../../dist-C2_1HCpW.js");
7
7
  let node_child_process = require("node:child_process");
8
8
  let node_util = require("node:util");
9
9
  let node_os = require("node:os");
@@ -1,4 +1,4 @@
1
- import { $ as metricsProviderCapability, Gt as EventCategory, Mt as createEvent, yt as BaseAddon } from "../../dist-BpzbLPel.mjs";
1
+ import { $ as metricsProviderCapability, Gt as EventCategory, Mt as createEvent, yt as BaseAddon } from "../../dist-DCTpBXm4.mjs";
2
2
  import { execFile, execFileSync } from "node:child_process";
3
3
  import { promisify } from "node:util";
4
4
  import * as os from "node:os";
@@ -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-Cri60Jd0.js");
6
+ const require_dist = require("../../dist-C2_1HCpW.js");
7
7
  let node_child_process = require("node:child_process");
8
8
  let node_util = require("node:util");
9
9
  let node_os = require("node:os");
@@ -1,5 +1,5 @@
1
1
  import { o as __toESM } from "../../chunk-CNf5ZN-e.mjs";
2
- import { Gt as EventCategory, L as enumerateInferenceDevices, Pt as emitReadiness, ct as scoreRuntimes, rt as platformProbeCapability, vt as errMsg, yt as BaseAddon } from "../../dist-BpzbLPel.mjs";
2
+ import { Gt as EventCategory, L as enumerateInferenceDevices, Pt as emitReadiness, ct as scoreRuntimes, rt as platformProbeCapability, vt as errMsg, yt as BaseAddon } from "../../dist-DCTpBXm4.mjs";
3
3
  import { execFile } from "node:child_process";
4
4
  import { promisify } from "node:util";
5
5
  import * as os from "node:os";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-Cri60Jd0.js");
6
+ const require_dist = require("../../dist-C2_1HCpW.js");
7
7
  //#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
8
8
  /**
9
9
  * Reconcile the durable `enabledProviders` set against authoritative
@@ -1,4 +1,4 @@
1
- import { Gt as EventCategory, yt as BaseAddon } from "../../dist-BpzbLPel.mjs";
1
+ import { Gt as EventCategory, yt as BaseAddon } from "../../dist-DCTpBXm4.mjs";
2
2
  //#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
3
3
  /**
4
4
  * Reconcile the durable `enabledProviders` set against authoritative
@@ -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-Cri60Jd0.js");
6
+ const require_dist = require("../../dist-C2_1HCpW.js");
7
7
  let zod = require("zod");
8
8
  let node_crypto = require("node:crypto");
9
9
  let _camstack_types_node = require("@camstack/types/node");
@@ -1,4 +1,4 @@
1
- import { Ct as DeviceFeature, Gt as EventCategory, M as deriveBatteryPresence, Rt as nodePin, T as batteryCapability, Tt as DeviceType, i as BatteryStatusSchema, mt as streamQualityLabel, ut as snapshotCapability, vt as errMsg, w as bareAddonId, yt as BaseAddon } from "../../dist-BpzbLPel.mjs";
1
+ import { Ct as DeviceFeature, Gt as EventCategory, M as deriveBatteryPresence, Rt as nodePin, T as batteryCapability, Tt as DeviceType, i as BatteryStatusSchema, mt as streamQualityLabel, ut as snapshotCapability, vt as errMsg, w as bareAddonId, yt as BaseAddon } from "../../dist-DCTpBXm4.mjs";
2
2
  import { z } from "zod";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { signExpiringUrl, verifyExpiringUrl } from "@camstack/types/node";
@@ -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-Cri60Jd0.js");
6
+ const require_dist = require("../../dist-C2_1HCpW.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let node_fs_promises = require("node:fs/promises");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { V as filesystemBrowseCapability, pt as storageProviderCapability, yt as BaseAddon } from "../../dist-BpzbLPel.mjs";
1
+ import { V as filesystemBrowseCapability, pt as storageProviderCapability, yt as BaseAddon } from "../../dist-DCTpBXm4.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { mkdir, readdir, realpath, statfs } from "node:fs/promises";
4
4
  import * as path$1 from "node:path";
@@ -3,8 +3,8 @@ 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-Cri60Jd0.js");
7
- const require_retired_settings_keys = require("../../retired-settings-keys-BVAxT5XL.js");
6
+ const require_dist = require("../../dist-C2_1HCpW.js");
7
+ const require_retired_settings_keys = require("../../retired-settings-keys-DHRXLMPn.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let node_fs = require("node:fs");
10
10
  let node_module = require("node:module");
@@ -1,5 +1,5 @@
1
- import { A as dataStoreProviderCapability, Bt as parseJsonUnknown, _t as vectorStoreCapability, f as RUNTIME_DEFAULTS, gt as vectorDimFromBase64, j as decodeVectorBase64, kt as asJsonObject, vt as errMsg, w as bareAddonId, yt as BaseAddon } from "../../dist-BpzbLPel.mjs";
2
- import { r as retiredKeyStoreOf, t as purgeRetiredSettingsKeys } from "../../retired-settings-keys-SrxMH3OT.mjs";
1
+ import { A as dataStoreProviderCapability, Bt as parseJsonUnknown, _t as vectorStoreCapability, f as RUNTIME_DEFAULTS, gt as vectorDimFromBase64, j as decodeVectorBase64, kt as asJsonObject, vt as errMsg, w as bareAddonId, yt as BaseAddon } from "../../dist-DCTpBXm4.mjs";
2
+ import { r as retiredKeyStoreOf, t as purgeRetiredSettingsKeys } from "../../retired-settings-keys-Czd76yLZ.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { statSync } from "node:fs";
@@ -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-Cri60Jd0.js");
6
+ const require_dist = require("../../dist-C2_1HCpW.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let node_fs_promises = require("node:fs/promises");
9
9
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -1,4 +1,4 @@
1
- import { dt as storageCapability, ft as storageMigrationCapability, g as StorageMigrationJobSchema, h as StorageLocationTypeSchema, lt as settingsStoreCapability, yt as BaseAddon, zt as parseJsonObject } from "../../dist-BpzbLPel.mjs";
1
+ import { dt as storageCapability, ft as storageMigrationCapability, g as StorageMigrationJobSchema, h as StorageLocationTypeSchema, lt as settingsStoreCapability, yt as BaseAddon, zt as parseJsonObject } from "../../dist-DCTpBXm4.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as path$1 from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-Cri60Jd0.js");
6
+ const require_dist = require("../../dist-C2_1HCpW.js");
7
7
  //#region src/builtins/system-config/system-config.addon.ts
8
8
  /**
9
9
  * Built-in `system-config` addon — Phase 4 of the settings redesign.
@@ -1,4 +1,4 @@
1
- import { It as hydrateSchema, vt as errMsg, yt as BaseAddon } from "../../dist-BpzbLPel.mjs";
1
+ import { It as hydrateSchema, vt as errMsg, yt as BaseAddon } from "../../dist-DCTpBXm4.mjs";
2
2
  //#region src/builtins/system-config/system-config.addon.ts
3
3
  /**
4
4
  * Built-in `system-config` addon — Phase 4 of the settings redesign.
@@ -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-Cri60Jd0.js");
6
+ const require_dist = require("../../dist-C2_1HCpW.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 { X as logDestinationCapability, yt as BaseAddon } from "../../dist-BpzbLPel.mjs";
1
+ import { X as logDestinationCapability, yt as BaseAddon } from "../../dist-DCTpBXm4.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";