@camstack/system 1.2.358 → 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 (65) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/autotrack/index.js +1 -1
  10. package/dist/builtins/autotrack/index.mjs +1 -1
  11. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  13. package/dist/builtins/calendars/calendars.addon.js +1 -1
  14. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  15. package/dist/builtins/camera-grid/index.js +1 -1
  16. package/dist/builtins/camera-grid/index.mjs +1 -1
  17. package/dist/builtins/composer/composer.addon.js +2 -2
  18. package/dist/builtins/composer/composer.addon.mjs +2 -2
  19. package/dist/builtins/console-logging/index.js +1 -1
  20. package/dist/builtins/console-logging/index.mjs +1 -1
  21. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  22. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  23. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  24. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  25. package/dist/builtins/hub-forwarder/index.js +1 -1
  26. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  27. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  28. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  29. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  30. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  31. package/dist/builtins/local-network/address-book.d.ts +69 -0
  32. package/dist/builtins/local-network/local-network.addon.d.ts +38 -45
  33. package/dist/builtins/local-network/local-network.addon.js +359 -76
  34. package/dist/builtins/local-network/local-network.addon.mjs +360 -75
  35. package/dist/builtins/loki-logging/index.js +1 -1
  36. package/dist/builtins/loki-logging/index.mjs +1 -1
  37. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  38. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  39. package/dist/builtins/platform-probe/index.js +1 -1
  40. package/dist/builtins/platform-probe/index.mjs +1 -1
  41. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  42. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  43. package/dist/builtins/snapshot/index.js +1 -1
  44. package/dist/builtins/snapshot/index.mjs +1 -1
  45. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  46. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  47. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  48. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  49. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  50. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  51. package/dist/builtins/system-config/system-config.addon.js +1 -1
  52. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  53. package/dist/builtins/winston-logging/index.js +1 -1
  54. package/dist/builtins/winston-logging/index.mjs +1 -1
  55. package/dist/{child-cap-dispatch-C2dFiu87.mjs → child-cap-dispatch-Dm1TRDCr.mjs} +1 -1
  56. package/dist/{child-cap-dispatch-BWk99kmA.js → child-cap-dispatch-t61ohDpb.js} +1 -1
  57. package/dist/{composition-sources-BaHPH661.js → composition-sources-CCU_KizO.js} +1 -1
  58. package/dist/{composition-sources-D8Hapyav.mjs → composition-sources-CHlfWHl6.mjs} +1 -1
  59. package/dist/{dist-DzAHit0a.js → dist-W_X44h-s.js} +386 -106
  60. package/dist/{dist-D3bhbMY3.mjs → dist-pTfD_M-U.mjs} +353 -103
  61. package/dist/index.js +2 -2
  62. package/dist/index.mjs +2 -2
  63. package/dist/{retired-settings-keys-B6AFmtlO.mjs → retired-settings-keys-CK_ifret.mjs} +1 -1
  64. package/dist/{retired-settings-keys-CkXoPjKy.js → retired-settings-keys-jglEzgWR.js} +1 -1
  65. package/package.json +1 -1
@@ -1,8 +1,232 @@
1
- import { Nn as validateHubAddressOverride, Tr as EventCategory, Ut as localNetworkCapability, an as pickAppLinkHub, er as BaseAddon, hn as resolveHubAddress, s as CAMSTACK_APP_LINK_ORIGIN } from "../../dist-D3bhbMY3.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,6 +420,7 @@ 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
426
  localHostname: "",
@@ -204,62 +429,126 @@ var LocalNetworkAddon = class extends BaseAddon {
204
429
  });
205
430
  }
206
431
  /**
207
- * Both audiences' hub address: the override when set, else the automatic
208
- * pick over the HTTPS palette (`resolveHubAddress`). The one reading every
209
- * consumer goes through — `getHubAddresses`.
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
- async snapshotHubAddresses() {
436
+ async palettes() {
212
437
  const port = (await this.resolveAdvertisedListenPort(null)).port;
213
- const palette = await this.buildAdvertisedEndpoints(false, false, "https", port);
438
+ await this.reconcileMeshEndpoint();
214
439
  return {
215
- cluster: resolveHubAddress(palette, "cluster", this.config.agentBaseUrl || null),
216
- browser: resolveHubAddress(palette, "browser", this.config.signInBaseUrl || null)
440
+ http: this.endpointsNow(false, false, "http", port),
441
+ https: this.endpointsNow(false, false, "https", port)
217
442
  };
218
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
+ });
461
+ }
219
462
  /**
220
- * The effective flagged notification address: the operator's explicit
221
- * choice, else AUTO — the CamStack app link (D709, operator decision
222
- * 2026-10-06; before it AUTO was the tunnel, then the mesh). The app link is
223
- * not a hub address: the NC's signed media and buttons keep their own hub
224
- * ladder when it is flagged; only viewer taps take the app form.
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).
225
465
  */
226
- resolveNotificationEndpoint() {
227
- const explicit = this.config.notificationBaseUrl;
228
- return explicit !== "" ? explicit : CAMSTACK_APP_LINK_ORIGIN;
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
+ };
229
492
  }
230
493
  /**
231
- * The `hub=` an app link carries: the best public HTTPS address in this
232
- * cap's own palette (tunnel, then mesh) — so it can never disagree with
233
- * what the Network page lists. See `pickAppLinkHub`.
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.
234
498
  */
235
- async resolveAppLinkHub() {
236
- const port = (await this.resolveAdvertisedListenPort(null)).port;
237
- return pickAppLinkHub(await this.buildAdvertisedEndpoints(false, false, "https", port));
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
+ } });
238
521
  }
239
522
  async notificationEndpointSnapshot() {
240
- const explicit = this.config.notificationBaseUrl;
523
+ const resolution = resolveNotificationsAddress(this.storedFlags());
524
+ const port = (await this.resolveAdvertisedListenPort(null)).port;
241
525
  return {
242
- baseUrl: explicit === "" ? null : explicit,
243
- resolved: this.resolveNotificationEndpoint(),
244
- appLinkHub: await this.resolveAppLinkHub()
526
+ baseUrl: resolution.flagged,
527
+ resolved: resolution.resolved,
528
+ appLinkHub: pickAppLinkHub(await this.buildAdvertisedEndpoints(false, false, "https", port))
245
529
  };
246
530
  }
247
531
  async snapshotViewerEndpoints() {
248
- 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());
249
533
  return {
250
- baseUrls: this.config.viewerBaseUrls,
251
- resolved: selected.map((e) => e.baseUrl)
534
+ baseUrls: resolution.flagged,
535
+ resolved: resolution.resolved
252
536
  };
253
537
  }
254
538
  /**
255
- * Palette shown in the Network tab: LAN IPv4, stable LAN IPv6, public,
256
- * mesh. The viewer HTTP/WS race is a subset (`selectViewerEndpoints`) —
257
- * 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.
258
543
  */
259
544
  async buildAdvertisedEndpoints(includeLoopback, ipv4Only, scheme, httpsPort) {
260
- const allow = this.config.allowedAddresses;
261
- const interfaces = applyAllowlist(this.enumerate(), allow);
262
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);
263
552
  const httpState = readLanHttpState();
264
553
  return buildEndpoints(interfaces, httpsPort, includeLoopback, ipv4Only, this.publicHostname, scheme, this.meshEndpoint, {
265
554
  enabled: this.config.httpEnabled !== false,
@@ -302,35 +591,50 @@ var LocalNetworkAddon = class extends BaseAddon {
302
591
  },
303
592
  getNotificationEndpoint: async () => this.notificationEndpointSnapshot(),
304
593
  setNotificationEndpoint: async ({ baseUrl }) => {
305
- await this.updateGlobalSettings({ notificationBaseUrl: baseUrl ?? "" });
306
- this.ctx.logger.info("local-network: notification endpoint set", { meta: { baseUrl: baseUrl ?? "(auto)" } });
594
+ await this.setRole("notifications", baseUrl, true, false);
307
595
  return this.notificationEndpointSnapshot();
308
596
  },
309
- getHubAddresses: async () => this.snapshotHubAddresses(),
310
- setHubAddress: async ({ audience, baseUrl }) => {
311
- let stored = "";
312
- if (baseUrl !== null && baseUrl.trim() !== "") {
313
- const check = validateHubAddressOverride(baseUrl);
314
- if (!check.ok) throw new Error(check.reason);
315
- stored = check.baseUrl;
316
- }
317
- await this.updateGlobalSettings(audience === "cluster" ? { agentBaseUrl: stored } : { signInBaseUrl: stored });
318
- this.ctx.logger.info("local-network: hub address override set", { meta: {
319
- audience,
320
- baseUrl: stored === "" ? "(auto)" : stored
321
- } });
322
- return this.snapshotHubAddresses();
323
- },
324
597
  getViewerEndpoints: async () => this.snapshotViewerEndpoints(),
325
598
  setViewerEndpoints: async ({ baseUrls }) => {
326
- const cleaned = [...new Set(baseUrls.filter((u) => u.length > 0))];
327
- 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
+ }));
328
607
  this.ctx.logger.info("local-network: viewer endpoints set", { meta: {
329
- count: cleaned.length,
330
- auto: cleaned.length === 0
608
+ count: viewer.length,
609
+ auto: viewer.length === 0
331
610
  } });
332
611
  return this.snapshotViewerEndpoints();
333
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(),
334
638
  getAllowedAddresses: async () => ({ addresses: this.config.allowedAddresses }),
335
639
  resetAllowlistToBestMatch: async () => {
336
640
  const seed = autoSeedAllowlist(this.enumerate());
@@ -872,25 +1176,6 @@ function buildEndpoints(interfaces, port, includeLoopback, ipv4Only, publicHostn
872
1176
  });
873
1177
  return out.toSorted((a, b) => a.priority - b.priority);
874
1178
  }
875
- /** AUTO viewer race: LAN IPv4 + the public tunnel. Never IPv6, mesh, or loopback. */
876
- function isDefaultViewerEndpoint(endpoint) {
877
- if (endpoint.kind === "lan-ipv4") return true;
878
- return endpoint.kind === "public" && endpoint.interfaceKind === "public";
879
- }
880
- /**
881
- * The URLs the SDK should race for API / viewer access.
882
- *
883
- * Empty `viewerBaseUrls` = AUTO (IPv4 + public tunnel). A non-empty list is
884
- * the operator's pin; if none of those URLs are in the current palette,
885
- * fall back to AUTO rather than racing an empty set.
886
- */
887
- function selectViewerEndpoints(endpoints, viewerBaseUrls) {
888
- const auto = endpoints.filter(isDefaultViewerEndpoint);
889
- if (viewerBaseUrls.length === 0) return auto;
890
- const wanted = new Set(viewerBaseUrls);
891
- const explicit = endpoints.filter((e) => wanted.has(e.baseUrl));
892
- return explicit.length > 0 ? explicit : auto;
893
- }
894
1179
  /**
895
1180
  * First-boot heuristic: which addresses should the allowlist start
896
1181
  * with? Includes LAN + Wi-Fi IPv4 addresses + plausible IPv6:
@@ -1019,4 +1304,4 @@ function readLinuxIpv6Flags() {
1019
1304
  }
1020
1305
  }
1021
1306
  //#endregion
1022
- 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-DzAHit0a.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, er as BaseAddon } from "../../dist-D3bhbMY3.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-DzAHit0a.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 { $n as errMsg, Tr as EventCategory, Zt as metricsProviderCapability, er as BaseAddon, fn as resolveBucketMs, jt as foldSnapshotByFunction, un as reducePoints, ur as createEvent } from "../../dist-D3bhbMY3.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-DzAHit0a.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 { $n as errMsg, St as enumerateInferenceDevices, Tr as EventCategory, er as BaseAddon, fr as emitReadiness, sn as platformProbeCapability, xn as scoreRuntimes } from "../../dist-D3bhbMY3.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-DzAHit0a.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 { Tr as EventCategory, er as BaseAddon } from "../../dist-D3bhbMY3.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-DzAHit0a.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 { $n as errMsg, An as streamQualityLabel, Qn as DeviceType, Tr as EventCategory, X as batteryCapability, Xn as DeviceFeature, Xt as memoizePinnedFfmpeg, Y as bareAddonId, _r as nodePin, a as BatteryStatusSchema, er as BaseAddon, gt as deriveBatteryPresence, wn as snapshotCapability } from "../../dist-D3bhbMY3.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-DzAHit0a.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, On as storageProviderCapability, er as BaseAddon } from "../../dist-D3bhbMY3.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-DzAHit0a.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, E as LegacyStorageLocationDefaultSchema, En as storageMigrationCapability, Et as evictionPolicyOfLocation, It as isLocationEnabled, Jt as mayReadLocation, Ln as withLocationMode, M as REDACTED_SECRET, Nt as gbToBytes, Qt as modeMayWrite, R as StorageCleanupJobSchema, Sn as settingsStoreCapability, Tn as storageCapability, Tr as EventCategory, V as StorageMigrationJobSchema, Vt as legacyModeOf, Yt as mayWriteToLocation, er as BaseAddon, gn as resolveLocationMode, tt as collectSecretConfigKeys, vr as parseJsonObject, z as StorageLocationModeSchema } from "../../dist-D3bhbMY3.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-DzAHit0a.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.