@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,11 +1,235 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-CFVOy8VT.js");
3
+ const require_dist = require("../../dist-W_X44h-s.js");
4
4
  const require_tls = require("../../tls-CCMX7MjX.js");
5
+ let zod = require("zod");
5
6
  let node_os = require("node:os");
6
7
  node_os = require_chunk.__toESM(node_os);
7
8
  let node_fs = require("node:fs");
8
9
  let node_net = require("node:net");
10
+ //#region src/builtins/local-network/address-book.ts
11
+ /**
12
+ * The Network page's address book, as pure functions: custom addresses plus
13
+ * the role flags carried by detected and custom addresses alike.
14
+ *
15
+ * ## Storage
16
+ *
17
+ * The role flags keep the keys they always had, so a rolled-back hub reads
18
+ * them with the meaning it knew: `viewerBaseUrls` (viewer, many),
19
+ * `notificationBaseUrl` (notifications, D709), `agentBaseUrl` and
20
+ * `signInBaseUrl` (agents / sign-in, D729 amendment). A single-valued role is
21
+ * one URL — at most one address carries it, by construction. `''` / `[]` is
22
+ * automatic. The custom addresses are `customAddresses` (url + label).
23
+ *
24
+ * ## Migration in the read path
25
+ *
26
+ * Before this, a single-valued flag could name an address that was in no row:
27
+ * the notification links' typed custom address (D709) and the two hub-address
28
+ * overrides (D729 amendment) were free URLs. {@link normalizeAddressBook}
29
+ * turns every such flag into a custom address carrying that role, on every
30
+ * read; the next write persists the result. It is the same rule forever, not
31
+ * a one-off: a flagged detected address that disappears (a DHCP move) shows up
32
+ * as a custom row instead of vanishing while it still wins.
33
+ */
34
+ var CustomAddressListSchema = zod.z.array(zod.z.object({
35
+ baseUrl: zod.z.string(),
36
+ label: zod.z.string()
37
+ }));
38
+ /** The label a flag promoted to a custom address is given. */
39
+ var PROMOTED_LABEL = {
40
+ notifications: "Notification links address",
41
+ agents: "Hub address for agents",
42
+ signIn: "Sign-in address"
43
+ };
44
+ var SINGLE_ROLES = [
45
+ "notifications",
46
+ "agents",
47
+ "signIn"
48
+ ];
49
+ function flagOf(value) {
50
+ return value === "" ? null : value;
51
+ }
52
+ /** Parse the stored custom list; a malformed store reads as empty, never throws. */
53
+ function parseCustomAddresses(raw) {
54
+ const parsed = CustomAddressListSchema.safeParse(raw ?? []);
55
+ return parsed.success ? parsed.data : [];
56
+ }
57
+ /**
58
+ * The address book as it stands: stored custom addresses, plus every
59
+ * single-valued flag that names neither a detected address, a custom one nor
60
+ * the app link, promoted to a custom address (see the module doc).
61
+ */
62
+ function normalizeAddressBook(stored, detectedUrls) {
63
+ const flags = {
64
+ viewer: stored.viewerBaseUrls,
65
+ notifications: flagOf(stored.notificationBaseUrl),
66
+ agents: flagOf(stored.agentBaseUrl),
67
+ signIn: flagOf(stored.signInBaseUrl)
68
+ };
69
+ const custom = [...parseCustomAddresses(stored.customAddresses)];
70
+ for (const role of SINGLE_ROLES) {
71
+ const url = flags[role];
72
+ if (url === null || require_dist.isAppLinkBase(url) || detectedUrls.has(url)) continue;
73
+ if (custom.some((c) => c.baseUrl === url)) continue;
74
+ custom.push({
75
+ baseUrl: url,
76
+ label: PROMOTED_LABEL[role]
77
+ });
78
+ }
79
+ return {
80
+ custom,
81
+ flags
82
+ };
83
+ }
84
+ /** The stored-settings patch that persists `state`. */
85
+ function addressBookPatch(state) {
86
+ return {
87
+ customAddresses: state.custom,
88
+ viewerBaseUrls: state.flags.viewer,
89
+ notificationBaseUrl: state.flags.notifications ?? "",
90
+ agentBaseUrl: state.flags.agents ?? "",
91
+ signInBaseUrl: state.flags.signIn ?? ""
92
+ };
93
+ }
94
+ function withCustomAddress(state, entry) {
95
+ const custom = state.custom.some((c) => c.baseUrl === entry.baseUrl) ? state.custom.map((c) => c.baseUrl === entry.baseUrl ? entry : c) : [...state.custom, entry];
96
+ return {
97
+ ...state,
98
+ custom
99
+ };
100
+ }
101
+ /** Drop a custom address and every flag it carried (those roles turn automatic). */
102
+ function withoutCustomAddress(state, baseUrl) {
103
+ const clear = (flag) => flag === baseUrl ? null : flag;
104
+ return {
105
+ custom: state.custom.filter((c) => c.baseUrl !== baseUrl),
106
+ flags: {
107
+ viewer: state.flags.viewer.filter((u) => u !== baseUrl),
108
+ notifications: clear(state.flags.notifications),
109
+ agents: clear(state.flags.agents),
110
+ signIn: clear(state.flags.signIn)
111
+ }
112
+ };
113
+ }
114
+ /** Every role back to automatic for `role`. */
115
+ function withRoleAutomatic(state, role) {
116
+ return role === "viewer" ? {
117
+ ...state,
118
+ flags: {
119
+ ...state.flags,
120
+ viewer: []
121
+ }
122
+ } : {
123
+ ...state,
124
+ flags: {
125
+ ...state.flags,
126
+ [role]: null
127
+ }
128
+ };
129
+ }
130
+ /**
131
+ * Flag or unflag `baseUrl` for `role`. A single-valued role flagged here moves
132
+ * off every other address (it is one URL); unflagging the address that carries
133
+ * it returns the role to automatic. The app link flagged for notifications IS
134
+ * the automatic answer, stored as automatic — one representation. `viewer`
135
+ * toggles inside the set; toggling while automatic starts from the automatic
136
+ * set (`autoViewer`) so the other addresses keep racing.
137
+ */
138
+ function withRoleFlag(state, role, baseUrl, enabled, autoViewer) {
139
+ if (role === "viewer") {
140
+ const without = (state.flags.viewer.length === 0 ? autoViewer : state.flags.viewer).filter((u) => u !== baseUrl);
141
+ const viewer = enabled ? [...without, baseUrl] : without;
142
+ return {
143
+ ...state,
144
+ flags: {
145
+ ...state.flags,
146
+ viewer
147
+ }
148
+ };
149
+ }
150
+ if (role === "notifications" && require_dist.isAppLinkBase(baseUrl)) return withRoleAutomatic(state, role);
151
+ if (enabled) return {
152
+ ...state,
153
+ flags: {
154
+ ...state.flags,
155
+ [role]: baseUrl
156
+ }
157
+ };
158
+ return state.flags[role] === baseUrl ? withRoleAutomatic(state, role) : state;
159
+ }
160
+ /** What a row may be flagged with. */
161
+ function eligibleRoles(endpoint) {
162
+ if (endpoint.kind === "loopback") return [];
163
+ if (endpoint.kind === "app-link") return ["notifications"];
164
+ return [
165
+ "viewer",
166
+ "notifications",
167
+ "agents",
168
+ "signIn"
169
+ ];
170
+ }
171
+ /** A custom address as a palette entry (`kind: 'custom'`). */
172
+ function customEndpoint(entry) {
173
+ return {
174
+ label: entry.label === "" ? "Custom address" : entry.label,
175
+ baseUrl: entry.baseUrl,
176
+ kind: "custom",
177
+ interfaceKind: "other",
178
+ plausible: true,
179
+ plausibleReason: "",
180
+ priority: 200
181
+ };
182
+ }
183
+ /**
184
+ * The palette a client races: the detected endpoints plus the custom
185
+ * addresses flagged `viewer` (a detected URL is never listed twice).
186
+ */
187
+ function withViewerCustomEndpoints(detected, state) {
188
+ const known = new Set(detected.map((e) => e.baseUrl));
189
+ const wanted = new Set(state.flags.viewer);
190
+ const extra = state.custom.filter((c) => wanted.has(c.baseUrl) && !known.has(c.baseUrl)).map(customEndpoint);
191
+ return [...detected, ...extra];
192
+ }
193
+ /** Every role's answer — the resolvers from `@camstack/types`, nothing else. */
194
+ function resolveRoles(state, viewerPalette, httpsPalette) {
195
+ return require_dist.resolveHubAddressRoles(withViewerCustomEndpoints(viewerPalette, state), httpsPalette, state.flags);
196
+ }
197
+ function flagsOn(state, baseUrl) {
198
+ const out = [];
199
+ if (state.flags.viewer.includes(baseUrl)) out.push("viewer");
200
+ for (const role of SINGLE_ROLES) if (state.flags[role] === baseUrl) out.push(role);
201
+ return out;
202
+ }
203
+ function inUseOn(roles, baseUrl) {
204
+ const out = [];
205
+ if (roles.viewer.resolved.includes(baseUrl)) out.push("viewer");
206
+ for (const role of SINGLE_ROLES) if (roles[role].resolved === baseUrl) out.push(role);
207
+ return out;
208
+ }
209
+ /** The Addresses table: app link, detected rows, then custom-only rows. */
210
+ function buildAddressBook(state, inputs) {
211
+ const roles = resolveRoles(state, inputs.rowsPalette, inputs.httpsPalette);
212
+ const customUrls = new Set(state.custom.map((c) => c.baseUrl));
213
+ const detectedUrls = new Set(inputs.rowsPalette.map((e) => e.baseUrl));
214
+ const row = (endpoint, detected) => ({
215
+ ...endpoint,
216
+ detected,
217
+ custom: customUrls.has(endpoint.baseUrl),
218
+ flags: endpoint.kind === "app-link" ? [] : flagsOn(state, endpoint.baseUrl),
219
+ inUse: endpoint.kind === "app-link" ? roles.notifications.resolved === "https://camstack.zentik.app" ? ["notifications"] : [] : inUseOn(roles, endpoint.baseUrl),
220
+ eligible: eligibleRoles(endpoint)
221
+ });
222
+ return {
223
+ rows: [
224
+ row(inputs.appLink, false),
225
+ ...inputs.rowsPalette.map((e) => row(e, true)),
226
+ ...state.custom.filter((c) => !detectedUrls.has(c.baseUrl)).map((c) => row(customEndpoint(c), false))
227
+ ],
228
+ roles,
229
+ appLinkHub: inputs.appLinkHub
230
+ };
231
+ }
232
+ //#endregion
9
233
  //#region src/builtins/local-network/ipv6-lan.ts
10
234
  /**
11
235
  * IPv6 helpers for LAN advertisement: prefix matching (privacy addresses
@@ -199,55 +423,135 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
199
423
  bootSeeded: false,
200
424
  notificationBaseUrl: "",
201
425
  viewerBaseUrls: [],
426
+ customAddresses: [],
202
427
  httpEnabled: true,
203
428
  httpPort: require_tls.DEFAULT_LAN_HTTP_PORT,
204
- localHostname: ""
429
+ localHostname: "",
430
+ agentBaseUrl: "",
431
+ signInBaseUrl: ""
205
432
  });
206
433
  }
207
434
  /**
208
- * The effective flagged notification address: the operator's explicit
209
- * choice, else AUTO — the CamStack app link (D709, operator decision
210
- * 2026-10-06; before it AUTO was the tunnel, then the mesh). The app link is
211
- * not a hub address: the NC's signed media and buttons keep their own hub
212
- * ladder when it is flagged; only viewer taps take the app form.
435
+ * The detected palettes, with the mesh reconciled once: `https` (what agents
436
+ * and sign-in resolve over, and the app link's `hub=`) and `http` (what the
437
+ * viewer race resolves over, as before).
213
438
  */
214
- resolveNotificationEndpoint() {
215
- const explicit = this.config.notificationBaseUrl;
216
- return explicit !== "" ? explicit : require_dist.CAMSTACK_APP_LINK_ORIGIN;
439
+ async palettes() {
440
+ const port = (await this.resolveAdvertisedListenPort(null)).port;
441
+ await this.reconcileMeshEndpoint();
442
+ return {
443
+ http: this.endpointsNow(false, false, "http", port),
444
+ https: this.endpointsNow(false, false, "https", port)
445
+ };
446
+ }
447
+ /** The stored address book, normalised against what is detected now. */
448
+ addressState(detected) {
449
+ return normalizeAddressBook(this.config, new Set(detected.map((e) => e.baseUrl)));
450
+ }
451
+ /** The role flags alone — they do not depend on what is detected. */
452
+ storedFlags() {
453
+ return normalizeAddressBook(this.config, /* @__PURE__ */ new Set()).flags;
454
+ }
455
+ /** `getAddresses`: the Addresses table and every role's answer. */
456
+ async addressBook(scheme) {
457
+ const { http, https } = await this.palettes();
458
+ return buildAddressBook(this.addressState([...http, ...https]), {
459
+ rowsPalette: scheme === "http" ? http : https,
460
+ httpsPalette: https,
461
+ appLink: appLinkEndpoint(),
462
+ appLinkHub: require_dist.pickAppLinkHub(https)
463
+ });
217
464
  }
218
465
  /**
219
- * The `hub=` an app link carries: the best public HTTPS address in this
220
- * cap's own palette (tunnel, then mesh) — so it can never disagree with
221
- * what the Network page lists. See `pickAppLinkHub`.
466
+ * Apply one change to the address book and persist it, normalised (a flag
467
+ * naming an address in no row becomes a custom address in the same write).
222
468
  */
223
- async resolveAppLinkHub() {
224
- const port = (await this.resolveAdvertisedListenPort(null)).port;
225
- return require_dist.pickAppLinkHub(await this.buildAdvertisedEndpoints(false, false, "https", port));
469
+ async changeAddressBook(change) {
470
+ const { http, https } = await this.palettes();
471
+ const detected = [...http, ...https];
472
+ const detectedUrls = new Set(detected.map((e) => e.baseUrl));
473
+ const changed = change(this.addressState(detected), {
474
+ detectedUrls,
475
+ autoViewer: [...new Set(detected.filter(require_dist.isDefaultViewerEndpoint).map((e) => e.baseUrl))]
476
+ });
477
+ const after = normalizeAddressBook({
478
+ ...addressBookPatch(changed),
479
+ customAddresses: changed.custom
480
+ }, detectedUrls);
481
+ await this.updateGlobalSettings(addressBookPatch(after));
482
+ }
483
+ /** The deprecated `getHubAddresses` shape, from the role resolvers. */
484
+ async snapshotHubAddresses() {
485
+ const { roles } = await this.addressBook("https");
486
+ const legacy = (r) => ({
487
+ override: r.flagged,
488
+ resolved: r.resolved,
489
+ source: r.source === "flagged" ? "override" : r.source
490
+ });
491
+ return {
492
+ cluster: legacy(roles.agents),
493
+ browser: legacy(roles.signIn)
494
+ };
495
+ }
496
+ /**
497
+ * Flag / unflag `baseUrl` for `role`, or (`baseUrl: null`) return the role
498
+ * to automatic. Validated: the address must be a row (detected, custom, or
499
+ * the app link for notifications), never loopback, and `signIn` on plain
500
+ * http only when accepted. A refusal stores nothing.
501
+ */
502
+ async setRole(role, baseUrl, enabled, acceptInsecureHttp) {
503
+ if (baseUrl === null || baseUrl.trim() === "" && role !== "viewer") {
504
+ await this.changeAddressBook((state) => withRoleAutomatic(state, role));
505
+ this.ctx.logger.info("local-network: address role set to automatic", { meta: { role } });
506
+ return;
507
+ }
508
+ let target = baseUrl.trim();
509
+ if (enabled) {
510
+ const check = require_dist.checkRoleAddress(role, target, { acceptInsecureHttp });
511
+ if (!check.ok) throw new Error(check.reason);
512
+ target = check.baseUrl;
513
+ }
514
+ await this.changeAddressBook((state, ctx) => {
515
+ const known = require_dist.isAppLinkBase(target) || ctx.detectedUrls.has(target) || state.custom.some((c) => c.baseUrl === target);
516
+ if (enabled && !known && role !== "notifications") throw new Error(`${target} is not in the address list — add it as a custom address first.`);
517
+ return withRoleFlag(state, role, target, enabled, ctx.autoViewer);
518
+ });
519
+ this.ctx.logger.info("local-network: address role set", { meta: {
520
+ role,
521
+ baseUrl: target,
522
+ enabled
523
+ } });
226
524
  }
227
525
  async notificationEndpointSnapshot() {
228
- const explicit = this.config.notificationBaseUrl;
526
+ const resolution = require_dist.resolveNotificationsAddress(this.storedFlags());
527
+ const port = (await this.resolveAdvertisedListenPort(null)).port;
229
528
  return {
230
- baseUrl: explicit === "" ? null : explicit,
231
- resolved: this.resolveNotificationEndpoint(),
232
- appLinkHub: await this.resolveAppLinkHub()
529
+ baseUrl: resolution.flagged,
530
+ resolved: resolution.resolved,
531
+ appLinkHub: require_dist.pickAppLinkHub(await this.buildAdvertisedEndpoints(false, false, "https", port))
233
532
  };
234
533
  }
235
534
  async snapshotViewerEndpoints() {
236
- const selected = selectViewerEndpoints(await this.buildAdvertisedEndpoints(false, false, "http", (await this.resolveAdvertisedListenPort(null)).port), this.config.viewerBaseUrls);
535
+ const resolution = require_dist.resolveViewerAddresses(await this.buildAdvertisedEndpoints(false, false, "http", (await this.resolveAdvertisedListenPort(null)).port), this.storedFlags());
237
536
  return {
238
- baseUrls: this.config.viewerBaseUrls,
239
- resolved: selected.map((e) => e.baseUrl)
537
+ baseUrls: resolution.flagged,
538
+ resolved: resolution.resolved
240
539
  };
241
540
  }
242
541
  /**
243
- * Palette shown in the Network tab: LAN IPv4, stable LAN IPv6, public,
244
- * mesh. The viewer HTTP/WS race is a subset (`selectViewerEndpoints`) —
245
- * IPv6 stays in this list because ICE/WebRTC gathers it regardless.
542
+ * Palette shown to clients: LAN IPv4, stable LAN IPv6, public, mesh, and
543
+ * the custom addresses flagged `viewer` (`kind: 'custom'`). The viewer
544
+ * HTTP/WS race is a subset (`selectViewerEndpoints`) — IPv6 stays in this
545
+ * list because ICE/WebRTC gathers it regardless.
246
546
  */
247
547
  async buildAdvertisedEndpoints(includeLoopback, ipv4Only, scheme, httpsPort) {
248
- const allow = this.config.allowedAddresses;
249
- const interfaces = applyAllowlist(this.enumerate(), allow);
250
548
  await this.reconcileMeshEndpoint();
549
+ const detected = this.endpointsNow(includeLoopback, ipv4Only, scheme, httpsPort);
550
+ return withViewerCustomEndpoints(detected, this.addressState(detected));
551
+ }
552
+ /** The DETECTED palette from the cached interface/tunnel/mesh state. */
553
+ endpointsNow(includeLoopback, ipv4Only, scheme, httpsPort) {
554
+ const interfaces = applyAllowlist(this.enumerate(), this.config.allowedAddresses);
251
555
  const httpState = require_tls.readLanHttpState();
252
556
  return buildEndpoints(interfaces, httpsPort, includeLoopback, ipv4Only, this.publicHostname, scheme, this.meshEndpoint, {
253
557
  enabled: this.config.httpEnabled !== false,
@@ -290,20 +594,50 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
290
594
  },
291
595
  getNotificationEndpoint: async () => this.notificationEndpointSnapshot(),
292
596
  setNotificationEndpoint: async ({ baseUrl }) => {
293
- await this.updateGlobalSettings({ notificationBaseUrl: baseUrl ?? "" });
294
- this.ctx.logger.info("local-network: notification endpoint set", { meta: { baseUrl: baseUrl ?? "(auto)" } });
597
+ await this.setRole("notifications", baseUrl, true, false);
295
598
  return this.notificationEndpointSnapshot();
296
599
  },
297
600
  getViewerEndpoints: async () => this.snapshotViewerEndpoints(),
298
601
  setViewerEndpoints: async ({ baseUrls }) => {
299
- const cleaned = [...new Set(baseUrls.filter((u) => u.length > 0))];
300
- await this.updateGlobalSettings({ viewerBaseUrls: cleaned });
602
+ const viewer = [...new Set(baseUrls.filter((u) => u.length > 0))];
603
+ await this.changeAddressBook((state) => ({
604
+ ...state,
605
+ flags: {
606
+ ...state.flags,
607
+ viewer
608
+ }
609
+ }));
301
610
  this.ctx.logger.info("local-network: viewer endpoints set", { meta: {
302
- count: cleaned.length,
303
- auto: cleaned.length === 0
611
+ count: viewer.length,
612
+ auto: viewer.length === 0
304
613
  } });
305
614
  return this.snapshotViewerEndpoints();
306
615
  },
616
+ getAddresses: async (input) => this.addressBook(input?.scheme ?? "https"),
617
+ addCustomAddress: async ({ baseUrl, label }) => {
618
+ const check = require_dist.validateCustomAddress(baseUrl);
619
+ if (!check.ok) throw new Error(check.reason);
620
+ const entry = {
621
+ baseUrl: check.baseUrl,
622
+ label: label.trim()
623
+ };
624
+ await this.changeAddressBook((state) => withCustomAddress(state, entry));
625
+ this.ctx.logger.info("local-network: custom address added", { meta: {
626
+ baseUrl: entry.baseUrl,
627
+ label: entry.label
628
+ } });
629
+ return this.addressBook("https");
630
+ },
631
+ removeCustomAddress: async ({ baseUrl }) => {
632
+ await this.changeAddressBook((state) => withoutCustomAddress(state, baseUrl));
633
+ this.ctx.logger.info("local-network: custom address removed", { meta: { baseUrl } });
634
+ return this.addressBook("https");
635
+ },
636
+ setAddressRole: async ({ role, baseUrl, enabled, acceptInsecureHttp }) => {
637
+ await this.setRole(role, baseUrl, enabled, acceptInsecureHttp === true);
638
+ return this.addressBook("https");
639
+ },
640
+ getHubAddresses: async () => this.snapshotHubAddresses(),
307
641
  getAllowedAddresses: async () => ({ addresses: this.config.allowedAddresses }),
308
642
  resetAllowlistToBestMatch: async () => {
309
643
  const seed = autoSeedAllowlist(this.enumerate());
@@ -845,25 +1179,6 @@ function buildEndpoints(interfaces, port, includeLoopback, ipv4Only, publicHostn
845
1179
  });
846
1180
  return out.toSorted((a, b) => a.priority - b.priority);
847
1181
  }
848
- /** AUTO viewer race: LAN IPv4 + the public tunnel. Never IPv6, mesh, or loopback. */
849
- function isDefaultViewerEndpoint(endpoint) {
850
- if (endpoint.kind === "lan-ipv4") return true;
851
- return endpoint.kind === "public" && endpoint.interfaceKind === "public";
852
- }
853
- /**
854
- * The URLs the SDK should race for API / viewer access.
855
- *
856
- * Empty `viewerBaseUrls` = AUTO (IPv4 + public tunnel). A non-empty list is
857
- * the operator's pin; if none of those URLs are in the current palette,
858
- * fall back to AUTO rather than racing an empty set.
859
- */
860
- function selectViewerEndpoints(endpoints, viewerBaseUrls) {
861
- const auto = endpoints.filter(isDefaultViewerEndpoint);
862
- if (viewerBaseUrls.length === 0) return auto;
863
- const wanted = new Set(viewerBaseUrls);
864
- const explicit = endpoints.filter((e) => wanted.has(e.baseUrl));
865
- return explicit.length > 0 ? explicit : auto;
866
- }
867
1182
  /**
868
1183
  * First-boot heuristic: which addresses should the allowlist start
869
1184
  * with? Includes LAN + Wi-Fi IPv4 addresses + plausible IPv6:
@@ -1002,12 +1317,10 @@ exports.classifyKind = classifyKind;
1002
1317
  exports.enumerateOsInterfaces = enumerateOsInterfaces;
1003
1318
  exports.explainNonPlausible = explainNonPlausible;
1004
1319
  exports.ipv6Prefix64Key = ipv6Prefix64Key;
1005
- exports.isDefaultViewerEndpoint = isDefaultViewerEndpoint;
1006
1320
  exports.isPlausibleAutoSeed = isPlausibleAutoSeed;
1007
1321
  exports.parseProcIfInet6 = parseProcIfInet6;
1008
1322
  exports.pickPreferred = pickPreferred;
1009
1323
  exports.prefixLen = prefixLen;
1010
1324
  exports.reconcileAllowlist = reconcileAllowlist;
1011
1325
  exports.resolveAdvertisedPort = resolveAdvertisedPort;
1012
- exports.selectViewerEndpoints = selectViewerEndpoints;
1013
1326
  exports.shouldAdvertiseIpv6 = shouldAdvertiseIpv6;