@camstack/system 1.2.357 → 1.2.359

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 (70) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/auth/user-manager.d.ts +43 -2
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  5. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  7. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  8. package/dist/builtins/alerts/alerts.addon.js +1 -1
  9. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  10. package/dist/builtins/autotrack/index.js +2 -2
  11. package/dist/builtins/autotrack/index.mjs +2 -2
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  13. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  14. package/dist/builtins/calendars/calendars.addon.js +1 -1
  15. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  16. package/dist/builtins/camera-grid/index.js +1 -1
  17. package/dist/builtins/camera-grid/index.mjs +1 -1
  18. package/dist/builtins/composer/composer.addon.js +8 -8
  19. package/dist/builtins/composer/composer.addon.mjs +8 -8
  20. package/dist/builtins/console-logging/index.js +1 -1
  21. package/dist/builtins/console-logging/index.mjs +1 -1
  22. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  23. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  24. package/dist/builtins/device-manager/device-manager.addon.js +9 -9
  25. package/dist/builtins/device-manager/device-manager.addon.mjs +9 -9
  26. package/dist/builtins/hub-forwarder/index.js +1 -1
  27. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  28. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  29. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  30. package/dist/builtins/local-auth/admin-account-boot.d.ts +27 -0
  31. package/dist/builtins/local-auth/auth-schema.d.ts +3 -2
  32. package/dist/builtins/local-auth/local-auth.addon.js +281 -8
  33. package/dist/builtins/local-auth/local-auth.addon.mjs +291 -22
  34. package/dist/builtins/local-network/address-book.d.ts +69 -0
  35. package/dist/builtins/local-network/local-network.addon.d.ts +44 -32
  36. package/dist/builtins/local-network/local-network.addon.js +368 -55
  37. package/dist/builtins/local-network/local-network.addon.mjs +369 -54
  38. package/dist/builtins/loki-logging/index.js +1 -1
  39. package/dist/builtins/loki-logging/index.mjs +1 -1
  40. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  41. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  42. package/dist/builtins/platform-probe/index.js +1 -1
  43. package/dist/builtins/platform-probe/index.mjs +1 -1
  44. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  45. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  46. package/dist/builtins/snapshot/index.js +1 -1
  47. package/dist/builtins/snapshot/index.mjs +1 -1
  48. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  49. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  50. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  51. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  52. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  53. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  54. package/dist/builtins/system-config/system-config.addon.js +1 -1
  55. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  56. package/dist/builtins/winston-logging/index.js +1 -1
  57. package/dist/builtins/winston-logging/index.mjs +1 -1
  58. package/dist/{child-cap-dispatch-BcRHQBKW.mjs → child-cap-dispatch-Dm1TRDCr.mjs} +2 -2
  59. package/dist/{child-cap-dispatch-wbIQJctP.js → child-cap-dispatch-t61ohDpb.js} +2 -2
  60. package/dist/{composition-sources-CeO0K2cl.js → composition-sources-CCU_KizO.js} +1 -1
  61. package/dist/{composition-sources-BBfqepyT.mjs → composition-sources-CHlfWHl6.mjs} +1 -1
  62. package/dist/{dist-CFVOy8VT.js → dist-W_X44h-s.js} +669 -121
  63. package/dist/{dist-B7WRgEZY.mjs → dist-pTfD_M-U.mjs} +628 -122
  64. package/dist/index.js +6 -141
  65. package/dist/index.mjs +4 -139
  66. package/dist/kernel/config-manager.d.ts +1 -1
  67. package/dist/kernel/config-schema.d.ts +6 -1
  68. package/dist/{retired-settings-keys-DKRjfz0U.mjs → retired-settings-keys-CK_ifret.mjs} +1 -1
  69. package/dist/{retired-settings-keys-CRlTzxaS.js → retired-settings-keys-jglEzgWR.js} +1 -1
  70. package/package.json +3 -3
@@ -1,8 +1,232 @@
1
- import { Cr as EventCategory, Qn as BaseAddon, Ut as localNetworkCapability, an as pickAppLinkHub, s as CAMSTACK_APP_LINK_ORIGIN } from "../../dist-B7WRgEZY.mjs";
1
+ import { Ar as EventCategory, Ft as isAppLinkBase, Kt as localNetworkCapability, Rn as validateCustomAddress, Rt as isDefaultViewerEndpoint, Sn as resolveViewerAddresses, ar as BaseAddon, cn as pickAppLinkHub, s as CAMSTACK_APP_LINK_ORIGIN, tt as checkRoleAddress, vn as resolveHubAddressRoles, xn as resolveNotificationsAddress } from "../../dist-pTfD_M-U.mjs";
2
2
  import { P as readLanHttpState, _ as writeExtraSans, h as readTlsMode, i as storeUploadedCertificate, j as applyLanHttp, k as DEFAULT_LAN_HTTP_PORT, m as readTlsAccessStatus, n as regenerateCertificate, p as readExtraSans, r as revertToGeneratedCertificate, t as applyCertificateToListener, u as servedHttpsLeafFingerprint, y as ensureTlsCert } from "../../tls-CJHogwTl.mjs";
3
+ import { z } from "zod";
3
4
  import * as os from "node:os";
4
5
  import { readFileSync } from "node:fs";
5
6
  import { isIP } from "node:net";
7
+ //#region src/builtins/local-network/address-book.ts
8
+ /**
9
+ * The Network page's address book, as pure functions: custom addresses plus
10
+ * the role flags carried by detected and custom addresses alike.
11
+ *
12
+ * ## Storage
13
+ *
14
+ * The role flags keep the keys they always had, so a rolled-back hub reads
15
+ * them with the meaning it knew: `viewerBaseUrls` (viewer, many),
16
+ * `notificationBaseUrl` (notifications, D709), `agentBaseUrl` and
17
+ * `signInBaseUrl` (agents / sign-in, D729 amendment). A single-valued role is
18
+ * one URL — at most one address carries it, by construction. `''` / `[]` is
19
+ * automatic. The custom addresses are `customAddresses` (url + label).
20
+ *
21
+ * ## Migration in the read path
22
+ *
23
+ * Before this, a single-valued flag could name an address that was in no row:
24
+ * the notification links' typed custom address (D709) and the two hub-address
25
+ * overrides (D729 amendment) were free URLs. {@link normalizeAddressBook}
26
+ * turns every such flag into a custom address carrying that role, on every
27
+ * read; the next write persists the result. It is the same rule forever, not
28
+ * a one-off: a flagged detected address that disappears (a DHCP move) shows up
29
+ * as a custom row instead of vanishing while it still wins.
30
+ */
31
+ var CustomAddressListSchema = z.array(z.object({
32
+ baseUrl: z.string(),
33
+ label: z.string()
34
+ }));
35
+ /** The label a flag promoted to a custom address is given. */
36
+ var PROMOTED_LABEL = {
37
+ notifications: "Notification links address",
38
+ agents: "Hub address for agents",
39
+ signIn: "Sign-in address"
40
+ };
41
+ var SINGLE_ROLES = [
42
+ "notifications",
43
+ "agents",
44
+ "signIn"
45
+ ];
46
+ function flagOf(value) {
47
+ return value === "" ? null : value;
48
+ }
49
+ /** Parse the stored custom list; a malformed store reads as empty, never throws. */
50
+ function parseCustomAddresses(raw) {
51
+ const parsed = CustomAddressListSchema.safeParse(raw ?? []);
52
+ return parsed.success ? parsed.data : [];
53
+ }
54
+ /**
55
+ * The address book as it stands: stored custom addresses, plus every
56
+ * single-valued flag that names neither a detected address, a custom one nor
57
+ * the app link, promoted to a custom address (see the module doc).
58
+ */
59
+ function normalizeAddressBook(stored, detectedUrls) {
60
+ const flags = {
61
+ viewer: stored.viewerBaseUrls,
62
+ notifications: flagOf(stored.notificationBaseUrl),
63
+ agents: flagOf(stored.agentBaseUrl),
64
+ signIn: flagOf(stored.signInBaseUrl)
65
+ };
66
+ const custom = [...parseCustomAddresses(stored.customAddresses)];
67
+ for (const role of SINGLE_ROLES) {
68
+ const url = flags[role];
69
+ if (url === null || isAppLinkBase(url) || detectedUrls.has(url)) continue;
70
+ if (custom.some((c) => c.baseUrl === url)) continue;
71
+ custom.push({
72
+ baseUrl: url,
73
+ label: PROMOTED_LABEL[role]
74
+ });
75
+ }
76
+ return {
77
+ custom,
78
+ flags
79
+ };
80
+ }
81
+ /** The stored-settings patch that persists `state`. */
82
+ function addressBookPatch(state) {
83
+ return {
84
+ customAddresses: state.custom,
85
+ viewerBaseUrls: state.flags.viewer,
86
+ notificationBaseUrl: state.flags.notifications ?? "",
87
+ agentBaseUrl: state.flags.agents ?? "",
88
+ signInBaseUrl: state.flags.signIn ?? ""
89
+ };
90
+ }
91
+ function withCustomAddress(state, entry) {
92
+ const custom = state.custom.some((c) => c.baseUrl === entry.baseUrl) ? state.custom.map((c) => c.baseUrl === entry.baseUrl ? entry : c) : [...state.custom, entry];
93
+ return {
94
+ ...state,
95
+ custom
96
+ };
97
+ }
98
+ /** Drop a custom address and every flag it carried (those roles turn automatic). */
99
+ function withoutCustomAddress(state, baseUrl) {
100
+ const clear = (flag) => flag === baseUrl ? null : flag;
101
+ return {
102
+ custom: state.custom.filter((c) => c.baseUrl !== baseUrl),
103
+ flags: {
104
+ viewer: state.flags.viewer.filter((u) => u !== baseUrl),
105
+ notifications: clear(state.flags.notifications),
106
+ agents: clear(state.flags.agents),
107
+ signIn: clear(state.flags.signIn)
108
+ }
109
+ };
110
+ }
111
+ /** Every role back to automatic for `role`. */
112
+ function withRoleAutomatic(state, role) {
113
+ return role === "viewer" ? {
114
+ ...state,
115
+ flags: {
116
+ ...state.flags,
117
+ viewer: []
118
+ }
119
+ } : {
120
+ ...state,
121
+ flags: {
122
+ ...state.flags,
123
+ [role]: null
124
+ }
125
+ };
126
+ }
127
+ /**
128
+ * Flag or unflag `baseUrl` for `role`. A single-valued role flagged here moves
129
+ * off every other address (it is one URL); unflagging the address that carries
130
+ * it returns the role to automatic. The app link flagged for notifications IS
131
+ * the automatic answer, stored as automatic — one representation. `viewer`
132
+ * toggles inside the set; toggling while automatic starts from the automatic
133
+ * set (`autoViewer`) so the other addresses keep racing.
134
+ */
135
+ function withRoleFlag(state, role, baseUrl, enabled, autoViewer) {
136
+ if (role === "viewer") {
137
+ const without = (state.flags.viewer.length === 0 ? autoViewer : state.flags.viewer).filter((u) => u !== baseUrl);
138
+ const viewer = enabled ? [...without, baseUrl] : without;
139
+ return {
140
+ ...state,
141
+ flags: {
142
+ ...state.flags,
143
+ viewer
144
+ }
145
+ };
146
+ }
147
+ if (role === "notifications" && isAppLinkBase(baseUrl)) return withRoleAutomatic(state, role);
148
+ if (enabled) return {
149
+ ...state,
150
+ flags: {
151
+ ...state.flags,
152
+ [role]: baseUrl
153
+ }
154
+ };
155
+ return state.flags[role] === baseUrl ? withRoleAutomatic(state, role) : state;
156
+ }
157
+ /** What a row may be flagged with. */
158
+ function eligibleRoles(endpoint) {
159
+ if (endpoint.kind === "loopback") return [];
160
+ if (endpoint.kind === "app-link") return ["notifications"];
161
+ return [
162
+ "viewer",
163
+ "notifications",
164
+ "agents",
165
+ "signIn"
166
+ ];
167
+ }
168
+ /** A custom address as a palette entry (`kind: 'custom'`). */
169
+ function customEndpoint(entry) {
170
+ return {
171
+ label: entry.label === "" ? "Custom address" : entry.label,
172
+ baseUrl: entry.baseUrl,
173
+ kind: "custom",
174
+ interfaceKind: "other",
175
+ plausible: true,
176
+ plausibleReason: "",
177
+ priority: 200
178
+ };
179
+ }
180
+ /**
181
+ * The palette a client races: the detected endpoints plus the custom
182
+ * addresses flagged `viewer` (a detected URL is never listed twice).
183
+ */
184
+ function withViewerCustomEndpoints(detected, state) {
185
+ const known = new Set(detected.map((e) => e.baseUrl));
186
+ const wanted = new Set(state.flags.viewer);
187
+ const extra = state.custom.filter((c) => wanted.has(c.baseUrl) && !known.has(c.baseUrl)).map(customEndpoint);
188
+ return [...detected, ...extra];
189
+ }
190
+ /** Every role's answer — the resolvers from `@camstack/types`, nothing else. */
191
+ function resolveRoles(state, viewerPalette, httpsPalette) {
192
+ return resolveHubAddressRoles(withViewerCustomEndpoints(viewerPalette, state), httpsPalette, state.flags);
193
+ }
194
+ function flagsOn(state, baseUrl) {
195
+ const out = [];
196
+ if (state.flags.viewer.includes(baseUrl)) out.push("viewer");
197
+ for (const role of SINGLE_ROLES) if (state.flags[role] === baseUrl) out.push(role);
198
+ return out;
199
+ }
200
+ function inUseOn(roles, baseUrl) {
201
+ const out = [];
202
+ if (roles.viewer.resolved.includes(baseUrl)) out.push("viewer");
203
+ for (const role of SINGLE_ROLES) if (roles[role].resolved === baseUrl) out.push(role);
204
+ return out;
205
+ }
206
+ /** The Addresses table: app link, detected rows, then custom-only rows. */
207
+ function buildAddressBook(state, inputs) {
208
+ const roles = resolveRoles(state, inputs.rowsPalette, inputs.httpsPalette);
209
+ const customUrls = new Set(state.custom.map((c) => c.baseUrl));
210
+ const detectedUrls = new Set(inputs.rowsPalette.map((e) => e.baseUrl));
211
+ const row = (endpoint, detected) => ({
212
+ ...endpoint,
213
+ detected,
214
+ custom: customUrls.has(endpoint.baseUrl),
215
+ flags: endpoint.kind === "app-link" ? [] : flagsOn(state, endpoint.baseUrl),
216
+ inUse: endpoint.kind === "app-link" ? roles.notifications.resolved === "https://camstack.zentik.app" ? ["notifications"] : [] : inUseOn(roles, endpoint.baseUrl),
217
+ eligible: eligibleRoles(endpoint)
218
+ });
219
+ return {
220
+ rows: [
221
+ row(inputs.appLink, false),
222
+ ...inputs.rowsPalette.map((e) => row(e, true)),
223
+ ...state.custom.filter((c) => !detectedUrls.has(c.baseUrl)).map((c) => row(customEndpoint(c), false))
224
+ ],
225
+ roles,
226
+ appLinkHub: inputs.appLinkHub
227
+ };
228
+ }
229
+ //#endregion
6
230
  //#region src/builtins/local-network/ipv6-lan.ts
7
231
  /**
8
232
  * IPv6 helpers for LAN advertisement: prefix matching (privacy addresses
@@ -196,55 +420,135 @@ var LocalNetworkAddon = class extends BaseAddon {
196
420
  bootSeeded: false,
197
421
  notificationBaseUrl: "",
198
422
  viewerBaseUrls: [],
423
+ customAddresses: [],
199
424
  httpEnabled: true,
200
425
  httpPort: DEFAULT_LAN_HTTP_PORT,
201
- localHostname: ""
426
+ localHostname: "",
427
+ agentBaseUrl: "",
428
+ signInBaseUrl: ""
202
429
  });
203
430
  }
204
431
  /**
205
- * The effective flagged notification address: the operator's explicit
206
- * choice, else AUTO — the CamStack app link (D709, operator decision
207
- * 2026-10-06; before it AUTO was the tunnel, then the mesh). The app link is
208
- * not a hub address: the NC's signed media and buttons keep their own hub
209
- * ladder when it is flagged; only viewer taps take the app form.
432
+ * The detected palettes, with the mesh reconciled once: `https` (what agents
433
+ * and sign-in resolve over, and the app link's `hub=`) and `http` (what the
434
+ * viewer race resolves over, as before).
210
435
  */
211
- resolveNotificationEndpoint() {
212
- const explicit = this.config.notificationBaseUrl;
213
- return explicit !== "" ? explicit : CAMSTACK_APP_LINK_ORIGIN;
436
+ async palettes() {
437
+ const port = (await this.resolveAdvertisedListenPort(null)).port;
438
+ await this.reconcileMeshEndpoint();
439
+ return {
440
+ http: this.endpointsNow(false, false, "http", port),
441
+ https: this.endpointsNow(false, false, "https", port)
442
+ };
443
+ }
444
+ /** The stored address book, normalised against what is detected now. */
445
+ addressState(detected) {
446
+ return normalizeAddressBook(this.config, new Set(detected.map((e) => e.baseUrl)));
447
+ }
448
+ /** The role flags alone — they do not depend on what is detected. */
449
+ storedFlags() {
450
+ return normalizeAddressBook(this.config, /* @__PURE__ */ new Set()).flags;
451
+ }
452
+ /** `getAddresses`: the Addresses table and every role's answer. */
453
+ async addressBook(scheme) {
454
+ const { http, https } = await this.palettes();
455
+ return buildAddressBook(this.addressState([...http, ...https]), {
456
+ rowsPalette: scheme === "http" ? http : https,
457
+ httpsPalette: https,
458
+ appLink: appLinkEndpoint(),
459
+ appLinkHub: pickAppLinkHub(https)
460
+ });
214
461
  }
215
462
  /**
216
- * The `hub=` an app link carries: the best public HTTPS address in this
217
- * cap's own palette (tunnel, then mesh) — so it can never disagree with
218
- * what the Network page lists. See `pickAppLinkHub`.
463
+ * Apply one change to the address book and persist it, normalised (a flag
464
+ * naming an address in no row becomes a custom address in the same write).
219
465
  */
220
- async resolveAppLinkHub() {
221
- const port = (await this.resolveAdvertisedListenPort(null)).port;
222
- return pickAppLinkHub(await this.buildAdvertisedEndpoints(false, false, "https", port));
466
+ async changeAddressBook(change) {
467
+ const { http, https } = await this.palettes();
468
+ const detected = [...http, ...https];
469
+ const detectedUrls = new Set(detected.map((e) => e.baseUrl));
470
+ const changed = change(this.addressState(detected), {
471
+ detectedUrls,
472
+ autoViewer: [...new Set(detected.filter(isDefaultViewerEndpoint).map((e) => e.baseUrl))]
473
+ });
474
+ const after = normalizeAddressBook({
475
+ ...addressBookPatch(changed),
476
+ customAddresses: changed.custom
477
+ }, detectedUrls);
478
+ await this.updateGlobalSettings(addressBookPatch(after));
479
+ }
480
+ /** The deprecated `getHubAddresses` shape, from the role resolvers. */
481
+ async snapshotHubAddresses() {
482
+ const { roles } = await this.addressBook("https");
483
+ const legacy = (r) => ({
484
+ override: r.flagged,
485
+ resolved: r.resolved,
486
+ source: r.source === "flagged" ? "override" : r.source
487
+ });
488
+ return {
489
+ cluster: legacy(roles.agents),
490
+ browser: legacy(roles.signIn)
491
+ };
492
+ }
493
+ /**
494
+ * Flag / unflag `baseUrl` for `role`, or (`baseUrl: null`) return the role
495
+ * to automatic. Validated: the address must be a row (detected, custom, or
496
+ * the app link for notifications), never loopback, and `signIn` on plain
497
+ * http only when accepted. A refusal stores nothing.
498
+ */
499
+ async setRole(role, baseUrl, enabled, acceptInsecureHttp) {
500
+ if (baseUrl === null || baseUrl.trim() === "" && role !== "viewer") {
501
+ await this.changeAddressBook((state) => withRoleAutomatic(state, role));
502
+ this.ctx.logger.info("local-network: address role set to automatic", { meta: { role } });
503
+ return;
504
+ }
505
+ let target = baseUrl.trim();
506
+ if (enabled) {
507
+ const check = checkRoleAddress(role, target, { acceptInsecureHttp });
508
+ if (!check.ok) throw new Error(check.reason);
509
+ target = check.baseUrl;
510
+ }
511
+ await this.changeAddressBook((state, ctx) => {
512
+ const known = isAppLinkBase(target) || ctx.detectedUrls.has(target) || state.custom.some((c) => c.baseUrl === target);
513
+ if (enabled && !known && role !== "notifications") throw new Error(`${target} is not in the address list — add it as a custom address first.`);
514
+ return withRoleFlag(state, role, target, enabled, ctx.autoViewer);
515
+ });
516
+ this.ctx.logger.info("local-network: address role set", { meta: {
517
+ role,
518
+ baseUrl: target,
519
+ enabled
520
+ } });
223
521
  }
224
522
  async notificationEndpointSnapshot() {
225
- const explicit = this.config.notificationBaseUrl;
523
+ const resolution = resolveNotificationsAddress(this.storedFlags());
524
+ const port = (await this.resolveAdvertisedListenPort(null)).port;
226
525
  return {
227
- baseUrl: explicit === "" ? null : explicit,
228
- resolved: this.resolveNotificationEndpoint(),
229
- appLinkHub: await this.resolveAppLinkHub()
526
+ baseUrl: resolution.flagged,
527
+ resolved: resolution.resolved,
528
+ appLinkHub: pickAppLinkHub(await this.buildAdvertisedEndpoints(false, false, "https", port))
230
529
  };
231
530
  }
232
531
  async snapshotViewerEndpoints() {
233
- const selected = selectViewerEndpoints(await this.buildAdvertisedEndpoints(false, false, "http", (await this.resolveAdvertisedListenPort(null)).port), this.config.viewerBaseUrls);
532
+ const resolution = resolveViewerAddresses(await this.buildAdvertisedEndpoints(false, false, "http", (await this.resolveAdvertisedListenPort(null)).port), this.storedFlags());
234
533
  return {
235
- baseUrls: this.config.viewerBaseUrls,
236
- resolved: selected.map((e) => e.baseUrl)
534
+ baseUrls: resolution.flagged,
535
+ resolved: resolution.resolved
237
536
  };
238
537
  }
239
538
  /**
240
- * Palette shown in the Network tab: LAN IPv4, stable LAN IPv6, public,
241
- * mesh. The viewer HTTP/WS race is a subset (`selectViewerEndpoints`) —
242
- * IPv6 stays in this list because ICE/WebRTC gathers it regardless.
539
+ * Palette shown to clients: LAN IPv4, stable LAN IPv6, public, mesh, and
540
+ * the custom addresses flagged `viewer` (`kind: 'custom'`). The viewer
541
+ * HTTP/WS race is a subset (`selectViewerEndpoints`) — IPv6 stays in this
542
+ * list because ICE/WebRTC gathers it regardless.
243
543
  */
244
544
  async buildAdvertisedEndpoints(includeLoopback, ipv4Only, scheme, httpsPort) {
245
- const allow = this.config.allowedAddresses;
246
- const interfaces = applyAllowlist(this.enumerate(), allow);
247
545
  await this.reconcileMeshEndpoint();
546
+ const detected = this.endpointsNow(includeLoopback, ipv4Only, scheme, httpsPort);
547
+ return withViewerCustomEndpoints(detected, this.addressState(detected));
548
+ }
549
+ /** The DETECTED palette from the cached interface/tunnel/mesh state. */
550
+ endpointsNow(includeLoopback, ipv4Only, scheme, httpsPort) {
551
+ const interfaces = applyAllowlist(this.enumerate(), this.config.allowedAddresses);
248
552
  const httpState = readLanHttpState();
249
553
  return buildEndpoints(interfaces, httpsPort, includeLoopback, ipv4Only, this.publicHostname, scheme, this.meshEndpoint, {
250
554
  enabled: this.config.httpEnabled !== false,
@@ -287,20 +591,50 @@ var LocalNetworkAddon = class extends BaseAddon {
287
591
  },
288
592
  getNotificationEndpoint: async () => this.notificationEndpointSnapshot(),
289
593
  setNotificationEndpoint: async ({ baseUrl }) => {
290
- await this.updateGlobalSettings({ notificationBaseUrl: baseUrl ?? "" });
291
- this.ctx.logger.info("local-network: notification endpoint set", { meta: { baseUrl: baseUrl ?? "(auto)" } });
594
+ await this.setRole("notifications", baseUrl, true, false);
292
595
  return this.notificationEndpointSnapshot();
293
596
  },
294
597
  getViewerEndpoints: async () => this.snapshotViewerEndpoints(),
295
598
  setViewerEndpoints: async ({ baseUrls }) => {
296
- const cleaned = [...new Set(baseUrls.filter((u) => u.length > 0))];
297
- await this.updateGlobalSettings({ viewerBaseUrls: cleaned });
599
+ const viewer = [...new Set(baseUrls.filter((u) => u.length > 0))];
600
+ await this.changeAddressBook((state) => ({
601
+ ...state,
602
+ flags: {
603
+ ...state.flags,
604
+ viewer
605
+ }
606
+ }));
298
607
  this.ctx.logger.info("local-network: viewer endpoints set", { meta: {
299
- count: cleaned.length,
300
- auto: cleaned.length === 0
608
+ count: viewer.length,
609
+ auto: viewer.length === 0
301
610
  } });
302
611
  return this.snapshotViewerEndpoints();
303
612
  },
613
+ getAddresses: async (input) => this.addressBook(input?.scheme ?? "https"),
614
+ addCustomAddress: async ({ baseUrl, label }) => {
615
+ const check = validateCustomAddress(baseUrl);
616
+ if (!check.ok) throw new Error(check.reason);
617
+ const entry = {
618
+ baseUrl: check.baseUrl,
619
+ label: label.trim()
620
+ };
621
+ await this.changeAddressBook((state) => withCustomAddress(state, entry));
622
+ this.ctx.logger.info("local-network: custom address added", { meta: {
623
+ baseUrl: entry.baseUrl,
624
+ label: entry.label
625
+ } });
626
+ return this.addressBook("https");
627
+ },
628
+ removeCustomAddress: async ({ baseUrl }) => {
629
+ await this.changeAddressBook((state) => withoutCustomAddress(state, baseUrl));
630
+ this.ctx.logger.info("local-network: custom address removed", { meta: { baseUrl } });
631
+ return this.addressBook("https");
632
+ },
633
+ setAddressRole: async ({ role, baseUrl, enabled, acceptInsecureHttp }) => {
634
+ await this.setRole(role, baseUrl, enabled, acceptInsecureHttp === true);
635
+ return this.addressBook("https");
636
+ },
637
+ getHubAddresses: async () => this.snapshotHubAddresses(),
304
638
  getAllowedAddresses: async () => ({ addresses: this.config.allowedAddresses }),
305
639
  resetAllowlistToBestMatch: async () => {
306
640
  const seed = autoSeedAllowlist(this.enumerate());
@@ -842,25 +1176,6 @@ function buildEndpoints(interfaces, port, includeLoopback, ipv4Only, publicHostn
842
1176
  });
843
1177
  return out.toSorted((a, b) => a.priority - b.priority);
844
1178
  }
845
- /** AUTO viewer race: LAN IPv4 + the public tunnel. Never IPv6, mesh, or loopback. */
846
- function isDefaultViewerEndpoint(endpoint) {
847
- if (endpoint.kind === "lan-ipv4") return true;
848
- return endpoint.kind === "public" && endpoint.interfaceKind === "public";
849
- }
850
- /**
851
- * The URLs the SDK should race for API / viewer access.
852
- *
853
- * Empty `viewerBaseUrls` = AUTO (IPv4 + public tunnel). A non-empty list is
854
- * the operator's pin; if none of those URLs are in the current palette,
855
- * fall back to AUTO rather than racing an empty set.
856
- */
857
- function selectViewerEndpoints(endpoints, viewerBaseUrls) {
858
- const auto = endpoints.filter(isDefaultViewerEndpoint);
859
- if (viewerBaseUrls.length === 0) return auto;
860
- const wanted = new Set(viewerBaseUrls);
861
- const explicit = endpoints.filter((e) => wanted.has(e.baseUrl));
862
- return explicit.length > 0 ? explicit : auto;
863
- }
864
1179
  /**
865
1180
  * First-boot heuristic: which addresses should the allowlist start
866
1181
  * with? Includes LAN + Wi-Fi IPv4 addresses + plausible IPv6:
@@ -989,4 +1304,4 @@ function readLinuxIpv6Flags() {
989
1304
  }
990
1305
  }
991
1306
  //#endregion
992
- export { ALLOWLIST_RECONCILE_INTERVAL_MS, LocalNetworkAddon, appLinkEndpoint, applyAllowlist, autoSeedAllowlist, buildEndpoints, classifyKind, enumerateOsInterfaces, explainNonPlausible, ipv6Prefix64Key, isDefaultViewerEndpoint, isPlausibleAutoSeed, parseProcIfInet6, pickPreferred, prefixLen, reconcileAllowlist, resolveAdvertisedPort, selectViewerEndpoints, shouldAdvertiseIpv6 };
1307
+ export { ALLOWLIST_RECONCILE_INTERVAL_MS, LocalNetworkAddon, appLinkEndpoint, applyAllowlist, autoSeedAllowlist, buildEndpoints, classifyKind, enumerateOsInterfaces, explainNonPlausible, ipv6Prefix64Key, isPlausibleAutoSeed, parseProcIfInet6, pickPreferred, prefixLen, reconcileAllowlist, resolveAdvertisedPort, 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-CFVOy8VT.js");
6
+ const require_dist = require("../../dist-W_X44h-s.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 { Gt as logDestinationCapability, Qn as BaseAddon } from "../../dist-B7WRgEZY.mjs";
1
+ import { Jt as logDestinationCapability, ar as BaseAddon } from "../../dist-pTfD_M-U.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-CFVOy8VT.js");
6
+ const require_dist = require("../../dist-W_X44h-s.js");
7
7
  const require_proc_stat = require("../../proc-stat-DL-WUOhl.js");
8
8
  let node_fs_promises = require("node:fs/promises");
9
9
  let node_os = require("node:os");
@@ -1,4 +1,4 @@
1
- import { Cr as EventCategory, Qn as BaseAddon, Zn as errMsg, Zt as metricsProviderCapability, cr as createEvent, fn as resolveBucketMs, jt as foldSnapshotByFunction, un as reducePoints } from "../../dist-B7WRgEZY.mjs";
1
+ import { Ar as EventCategory, Mt as foldSnapshotByFunction, ar as BaseAddon, en as metricsProviderCapability, hn as resolveBucketMs, hr as createEvent, ir as errMsg, pn as reducePoints } from "../../dist-pTfD_M-U.mjs";
2
2
  import { a as parseProcStat, i as nodeProcReader, n as elapsedSecSince, o as parseProcStatusRss, r as lifetimeCpuPercent, t as commandFromCmdline } from "../../proc-stat-CbC55Kqi.mjs";
3
3
  import { access, readFile, readdir } from "node:fs/promises";
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-CFVOy8VT.js");
6
+ const require_dist = require("../../dist-W_X44h-s.js");
7
7
  let node_os = require("node:os");
8
8
  node_os = require_chunk.__toESM(node_os);
9
9
  let node_fs = require("node:fs");
@@ -1,5 +1,5 @@
1
1
  import { o as __toESM } from "../../chunk-CNf5ZN-e.mjs";
2
- import { Cr as EventCategory, Qn as BaseAddon, St as enumerateInferenceDevices, Zn as errMsg, bn as scoreRuntimes, sn as platformProbeCapability, ur as emitReadiness } from "../../dist-B7WRgEZY.mjs";
2
+ import { Ar as EventCategory, Ct as enumerateInferenceDevices, En as scoreRuntimes, _r as emitReadiness, ar as BaseAddon, ir as errMsg, un as platformProbeCapability } from "../../dist-pTfD_M-U.mjs";
3
3
  import * as os from "node:os";
4
4
  import * as fs from "node:fs";
5
5
  import { execFile } from "node:child_process";
@@ -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-CFVOy8VT.js");
6
+ const require_dist = require("../../dist-W_X44h-s.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 { Cr as EventCategory, Qn as BaseAddon } from "../../dist-B7WRgEZY.mjs";
1
+ import { Ar as EventCategory, ar as BaseAddon } from "../../dist-pTfD_M-U.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-CFVOy8VT.js");
6
+ const require_dist = require("../../dist-W_X44h-s.js");
7
7
  const require_snapshot_courtesy = require("../../snapshot-courtesy-D_C5-nC0.js");
8
8
  let zod = require("zod");
9
9
  let node_crypto = require("node:crypto");
@@ -1,4 +1,4 @@
1
- import { Cn as snapshotCapability, Cr as EventCategory, Jn as DeviceFeature, Qn as BaseAddon, X as batteryCapability, Xn as DeviceType, Xt as memoizePinnedFfmpeg, Y as bareAddonId, Zn as errMsg, a as BatteryStatusSchema, gt as deriveBatteryPresence, hr as nodePin, kn as streamQualityLabel } from "../../dist-B7WRgEZY.mjs";
1
+ import { $t as memoizePinnedFfmpeg, Ar as EventCategory, Fn as streamQualityLabel, Sr as nodePin, X as batteryCapability, Y as bareAddonId, _t as deriveBatteryPresence, a as BatteryStatusSchema, ar as BaseAddon, ir as errMsg, kn as snapshotCapability, rr as DeviceType, tr as DeviceFeature } from "../../dist-pTfD_M-U.mjs";
2
2
  import { i as renderCourtesyOverlayJpeg, n as courtesyCacheKey, r as renderCourtesyJpeg } from "../../snapshot-courtesy-CyFs1M3I.mjs";
3
3
  import { z } from "zod";
4
4
  import { randomUUID } from "node:crypto";
@@ -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-CFVOy8VT.js");
6
+ const require_dist = require("../../dist-W_X44h-s.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 { At as filesystemBrowseCapability, Dn as storageProviderCapability, Qn as BaseAddon } from "../../dist-B7WRgEZY.mjs";
1
+ import { Nn as storageProviderCapability, ar as BaseAddon, jt as filesystemBrowseCapability } from "../../dist-pTfD_M-U.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,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-CFVOy8VT.js");
6
+ const require_dist = require("../../dist-W_X44h-s.js");
7
7
  const require_hub_cap_forward = require("../../hub-cap-forward-DmHNjbB0.js");
8
8
  let zod = require("zod");
9
9
  let node_crypto = require("node:crypto");
@@ -1,4 +1,4 @@
1
- import { B as StorageLocationTypeSchema, Cr as EventCategory, E as LegacyStorageLocationDefaultSchema, Et as evictionPolicyOfLocation, Fn as withLocationMode, It as isLocationEnabled, Jt as mayReadLocation, M as REDACTED_SECRET, Nt as gbToBytes, Qn as BaseAddon, Qt as modeMayWrite, R as StorageCleanupJobSchema, Tn as storageMigrationCapability, V as StorageMigrationJobSchema, Vt as legacyModeOf, Yt as mayWriteToLocation, gr as parseJsonObject, hn as resolveLocationMode, tt as collectSecretConfigKeys, wn as storageCapability, xn as settingsStoreCapability, z as StorageLocationModeSchema } from "../../dist-B7WRgEZY.mjs";
1
+ import { An as storageCapability, Ar as EventCategory, B as StorageLocationTypeSchema, Cr as parseJsonObject, Dn as settingsStoreCapability, Dt as evictionPolicyOfLocation, E as LegacyStorageLocationDefaultSchema, Hn as withLocationMode, M as REDACTED_SECRET, Pt as gbToBytes, Qt as mayWriteToLocation, R as StorageCleanupJobSchema, V as StorageMigrationJobSchema, Wt as legacyModeOf, Zt as mayReadLocation, ar as BaseAddon, jn as storageMigrationCapability, nt as collectSecretConfigKeys, tn as modeMayWrite, yn as resolveLocationMode, z as StorageLocationModeSchema, zt as isLocationEnabled } from "../../dist-pTfD_M-U.mjs";
2
2
  import { t as HUB_CAP_FWD_ACTION } from "../../hub-cap-forward-BdT320dq.mjs";
3
3
  import { z } from "zod";
4
4
  import { randomUUID } from "node:crypto";
@@ -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-CFVOy8VT.js");
6
+ const require_dist = require("../../dist-W_X44h-s.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 { Qn as BaseAddon, Zn as errMsg, fr as hydrateSchema } from "../../dist-B7WRgEZY.mjs";
1
+ import { ar as BaseAddon, ir as errMsg, yr as hydrateSchema } from "../../dist-pTfD_M-U.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-CFVOy8VT.js");
6
+ const require_dist = require("../../dist-W_X44h-s.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 { Gt as logDestinationCapability, Qn as BaseAddon } from "../../dist-B7WRgEZY.mjs";
1
+ import { Jt as logDestinationCapability, ar as BaseAddon } from "../../dist-pTfD_M-U.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";