@camstack/system 1.2.358 → 1.2.360

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 +43 -47
  33. package/dist/builtins/local-network/local-network.addon.js +366 -81
  34. package/dist/builtins/local-network/local-network.addon.mjs +367 -80
  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-StBHObCA.mjs} +1 -1
  56. package/dist/{child-cap-dispatch-BWk99kmA.js → child-cap-dispatch-T3Kdhbb3.js} +1 -1
  57. package/dist/{composition-sources-D8Hapyav.mjs → composition-sources-DASc7NBj.mjs} +1 -1
  58. package/dist/{composition-sources-BaHPH661.js → composition-sources-DFgayxCc.js} +1 -1
  59. package/dist/{dist-D3bhbMY3.mjs → dist-C9Rk6KVv.mjs} +381 -115
  60. package/dist/{dist-DzAHit0a.js → dist-Cv1lnnQq.js} +414 -118
  61. package/dist/index.js +2 -2
  62. package/dist/index.mjs +2 -2
  63. package/dist/{retired-settings-keys-CkXoPjKy.js → retired-settings-keys-BGNqRNhu.js} +1 -1
  64. package/dist/{retired-settings-keys-B6AFmtlO.mjs → retired-settings-keys-BjG8bQVI.mjs} +1 -1
  65. package/package.json +1 -1
@@ -1,11 +1,237 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-DzAHit0a.js");
3
+ const require_dist = require("../../dist-Cv1lnnQq.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 (D734)
16
+ *
17
+ * One key per role: `appConnectionUrls` (`appConnections`, many),
18
+ * `notificationTapUrl` (`notificationTaps`), `publicEndpointUrl`
19
+ * (`publicEndpoint`) and `internalUrl` (`internal`). A single-valued role is
20
+ * one URL — at most one address carries it, by construction. `''` / `[]` is
21
+ * automatic. The custom addresses are `customAddresses` (url + label).
22
+ *
23
+ * The keys of the earlier role names (`viewerBaseUrls`, `notificationBaseUrl`,
24
+ * `signInBaseUrl`, `agentBaseUrl`) are not read: a value stored under one is
25
+ * ignored, and that role is automatic until the operator flags it again
26
+ * (D734 — one install, re-set by hand, no migration code).
27
+ *
28
+ * ## A flag on an address that is in no row
29
+ *
30
+ * A flagged detected address can disappear (a DHCP move) while it still wins
31
+ * its role. {@link normalizeAddressBook} turns every single-valued flag that
32
+ * names neither a detected address, a custom one nor the app link into a
33
+ * custom address carrying that role, on every read; the next write persists
34
+ * the result — so the address shows as a custom row instead of vanishing.
35
+ */
36
+ var CustomAddressListSchema = zod.z.array(zod.z.object({
37
+ baseUrl: zod.z.string(),
38
+ label: zod.z.string()
39
+ }));
40
+ /** The label a flag promoted to a custom address is given. */
41
+ var PROMOTED_LABEL = {
42
+ notificationTaps: "Notification taps address",
43
+ internal: "Internal address",
44
+ publicEndpoint: "Public endpoint"
45
+ };
46
+ var SINGLE_ROLES = [
47
+ "notificationTaps",
48
+ "internal",
49
+ "publicEndpoint"
50
+ ];
51
+ function flagOf(value) {
52
+ return value === "" ? null : value;
53
+ }
54
+ /** Parse the stored custom list; a malformed store reads as empty, never throws. */
55
+ function parseCustomAddresses(raw) {
56
+ const parsed = CustomAddressListSchema.safeParse(raw ?? []);
57
+ return parsed.success ? parsed.data : [];
58
+ }
59
+ /**
60
+ * The address book as it stands: stored custom addresses, plus every
61
+ * single-valued flag that names neither a detected address, a custom one nor
62
+ * the app link, promoted to a custom address (see the module doc).
63
+ */
64
+ function normalizeAddressBook(stored, detectedUrls) {
65
+ const flags = {
66
+ appConnections: stored.appConnectionUrls,
67
+ notificationTaps: flagOf(stored.notificationTapUrl),
68
+ internal: flagOf(stored.internalUrl),
69
+ publicEndpoint: flagOf(stored.publicEndpointUrl)
70
+ };
71
+ const custom = [...parseCustomAddresses(stored.customAddresses)];
72
+ for (const role of SINGLE_ROLES) {
73
+ const url = flags[role];
74
+ if (url === null || require_dist.isAppLinkBase(url) || detectedUrls.has(url)) continue;
75
+ if (custom.some((c) => c.baseUrl === url)) continue;
76
+ custom.push({
77
+ baseUrl: url,
78
+ label: PROMOTED_LABEL[role]
79
+ });
80
+ }
81
+ return {
82
+ custom,
83
+ flags
84
+ };
85
+ }
86
+ /** The stored-settings patch that persists `state`. */
87
+ function addressBookPatch(state) {
88
+ return {
89
+ customAddresses: state.custom,
90
+ appConnectionUrls: state.flags.appConnections,
91
+ notificationTapUrl: state.flags.notificationTaps ?? "",
92
+ publicEndpointUrl: state.flags.publicEndpoint ?? "",
93
+ internalUrl: state.flags.internal ?? ""
94
+ };
95
+ }
96
+ function withCustomAddress(state, entry) {
97
+ const custom = state.custom.some((c) => c.baseUrl === entry.baseUrl) ? state.custom.map((c) => c.baseUrl === entry.baseUrl ? entry : c) : [...state.custom, entry];
98
+ return {
99
+ ...state,
100
+ custom
101
+ };
102
+ }
103
+ /** Drop a custom address and every flag it carried (those roles turn automatic). */
104
+ function withoutCustomAddress(state, baseUrl) {
105
+ const clear = (flag) => flag === baseUrl ? null : flag;
106
+ return {
107
+ custom: state.custom.filter((c) => c.baseUrl !== baseUrl),
108
+ flags: {
109
+ appConnections: state.flags.appConnections.filter((u) => u !== baseUrl),
110
+ notificationTaps: clear(state.flags.notificationTaps),
111
+ internal: clear(state.flags.internal),
112
+ publicEndpoint: clear(state.flags.publicEndpoint)
113
+ }
114
+ };
115
+ }
116
+ /** Every role back to automatic for `role`. */
117
+ function withRoleAutomatic(state, role) {
118
+ return role === "appConnections" ? {
119
+ ...state,
120
+ flags: {
121
+ ...state.flags,
122
+ appConnections: []
123
+ }
124
+ } : {
125
+ ...state,
126
+ flags: {
127
+ ...state.flags,
128
+ [role]: null
129
+ }
130
+ };
131
+ }
132
+ /**
133
+ * Flag or unflag `baseUrl` for `role`. A single-valued role flagged here moves
134
+ * off every other address (it is one URL); unflagging the address that carries
135
+ * it returns the role to automatic. The app link flagged for notification
136
+ * taps IS the automatic answer, stored as automatic — one representation.
137
+ * `appConnections` toggles inside the set; toggling while automatic starts
138
+ * from the automatic set (`autoApp`) so the other addresses keep racing.
139
+ */
140
+ function withRoleFlag(state, role, baseUrl, enabled, autoApp) {
141
+ if (role === "appConnections") {
142
+ const without = (state.flags.appConnections.length === 0 ? autoApp : state.flags.appConnections).filter((u) => u !== baseUrl);
143
+ const appConnections = enabled ? [...without, baseUrl] : without;
144
+ return {
145
+ ...state,
146
+ flags: {
147
+ ...state.flags,
148
+ appConnections
149
+ }
150
+ };
151
+ }
152
+ if (role === "notificationTaps" && require_dist.isAppLinkBase(baseUrl)) return withRoleAutomatic(state, role);
153
+ if (enabled) return {
154
+ ...state,
155
+ flags: {
156
+ ...state.flags,
157
+ [role]: baseUrl
158
+ }
159
+ };
160
+ return state.flags[role] === baseUrl ? withRoleAutomatic(state, role) : state;
161
+ }
162
+ /** What a row may be flagged with. */
163
+ function eligibleRoles(endpoint) {
164
+ if (endpoint.kind === "loopback") return [];
165
+ if (endpoint.kind === "app-link") return ["notificationTaps"];
166
+ return [
167
+ "appConnections",
168
+ "notificationTaps",
169
+ "internal",
170
+ "publicEndpoint"
171
+ ];
172
+ }
173
+ /** A custom address as a palette entry (`kind: 'custom'`). */
174
+ function customEndpoint(entry) {
175
+ return {
176
+ label: entry.label === "" ? "Custom address" : entry.label,
177
+ baseUrl: entry.baseUrl,
178
+ kind: "custom",
179
+ interfaceKind: "other",
180
+ plausible: true,
181
+ plausibleReason: "",
182
+ priority: 200
183
+ };
184
+ }
185
+ /**
186
+ * The palette a client races: the detected endpoints plus the custom
187
+ * addresses flagged `appConnections` (a detected URL is never listed twice).
188
+ */
189
+ function withAppConnectionCustomEndpoints(detected, state) {
190
+ const known = new Set(detected.map((e) => e.baseUrl));
191
+ const wanted = new Set(state.flags.appConnections);
192
+ const extra = state.custom.filter((c) => wanted.has(c.baseUrl) && !known.has(c.baseUrl)).map(customEndpoint);
193
+ return [...detected, ...extra];
194
+ }
195
+ /** Every role's answer — the resolvers from `@camstack/types`, nothing else. */
196
+ function resolveRoles(state, appPalette, httpsPalette) {
197
+ return require_dist.resolveHubAddressRoles(withAppConnectionCustomEndpoints(appPalette, state), httpsPalette, state.flags);
198
+ }
199
+ function flagsOn(state, baseUrl) {
200
+ const out = [];
201
+ if (state.flags.appConnections.includes(baseUrl)) out.push("appConnections");
202
+ for (const role of SINGLE_ROLES) if (state.flags[role] === baseUrl) out.push(role);
203
+ return out;
204
+ }
205
+ function inUseOn(roles, baseUrl) {
206
+ const out = [];
207
+ if (roles.appConnections.resolved.includes(baseUrl)) out.push("appConnections");
208
+ for (const role of SINGLE_ROLES) if (roles[role].resolved === baseUrl) out.push(role);
209
+ return out;
210
+ }
211
+ /** The Addresses table: app link, detected rows, then custom-only rows. */
212
+ function buildAddressBook(state, inputs) {
213
+ const roles = resolveRoles(state, inputs.rowsPalette, inputs.httpsPalette);
214
+ const customUrls = new Set(state.custom.map((c) => c.baseUrl));
215
+ const detectedUrls = new Set(inputs.rowsPalette.map((e) => e.baseUrl));
216
+ const row = (endpoint, detected) => ({
217
+ ...endpoint,
218
+ detected,
219
+ custom: customUrls.has(endpoint.baseUrl),
220
+ flags: endpoint.kind === "app-link" ? [] : flagsOn(state, endpoint.baseUrl),
221
+ inUse: endpoint.kind === "app-link" ? roles.notificationTaps.resolved === "https://camstack.zentik.app" ? ["notificationTaps"] : [] : inUseOn(roles, endpoint.baseUrl),
222
+ eligible: eligibleRoles(endpoint)
223
+ });
224
+ return {
225
+ rows: [
226
+ row(inputs.appLink, false),
227
+ ...inputs.rowsPalette.map((e) => row(e, true)),
228
+ ...state.custom.filter((c) => !detectedUrls.has(c.baseUrl)).map((c) => row(customEndpoint(c), false))
229
+ ],
230
+ roles,
231
+ appLinkHub: inputs.appLinkHub
232
+ };
233
+ }
234
+ //#endregion
9
235
  //#region src/builtins/local-network/ipv6-lan.ts
10
236
  /**
11
237
  * IPv6 helpers for LAN advertisement: prefix matching (privacy addresses
@@ -197,72 +423,137 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
197
423
  super({
198
424
  allowedAddresses: [],
199
425
  bootSeeded: false,
200
- notificationBaseUrl: "",
201
- viewerBaseUrls: [],
426
+ notificationTapUrl: "",
427
+ appConnectionUrls: [],
428
+ customAddresses: [],
202
429
  httpEnabled: true,
203
430
  httpPort: require_tls.DEFAULT_LAN_HTTP_PORT,
204
431
  localHostname: "",
205
- agentBaseUrl: "",
206
- signInBaseUrl: ""
432
+ internalUrl: "",
433
+ publicEndpointUrl: ""
207
434
  });
208
435
  }
209
436
  /**
210
- * Both audiences' hub address: the override when set, else the automatic
211
- * pick over the HTTPS palette (`resolveHubAddress`). The one reading every
212
- * consumer goes through — `getHubAddresses`.
437
+ * The detected palettes, with the mesh reconciled once: `https` (what
438
+ * `internal` and `publicEndpoint` resolve over, and the app link's `hub=`)
439
+ * and `http` (what the app-connections race resolves over).
213
440
  */
214
- async snapshotHubAddresses() {
441
+ async palettes() {
215
442
  const port = (await this.resolveAdvertisedListenPort(null)).port;
216
- const palette = await this.buildAdvertisedEndpoints(false, false, "https", port);
443
+ await this.reconcileMeshEndpoint();
217
444
  return {
218
- cluster: require_dist.resolveHubAddress(palette, "cluster", this.config.agentBaseUrl || null),
219
- browser: require_dist.resolveHubAddress(palette, "browser", this.config.signInBaseUrl || null)
445
+ http: this.endpointsNow(false, false, "http", port),
446
+ https: this.endpointsNow(false, false, "https", port)
220
447
  };
221
448
  }
449
+ /** The stored address book, normalised against what is detected now. */
450
+ addressState(detected) {
451
+ return normalizeAddressBook(this.config, new Set(detected.map((e) => e.baseUrl)));
452
+ }
453
+ /** The role flags alone — they do not depend on what is detected. */
454
+ storedFlags() {
455
+ return normalizeAddressBook(this.config, /* @__PURE__ */ new Set()).flags;
456
+ }
457
+ /** `getAddresses`: the Addresses table and every role's answer. */
458
+ async addressBook(scheme) {
459
+ const { http, https } = await this.palettes();
460
+ return buildAddressBook(this.addressState([...http, ...https]), {
461
+ rowsPalette: scheme === "http" ? http : https,
462
+ httpsPalette: https,
463
+ appLink: appLinkEndpoint(),
464
+ appLinkHub: require_dist.pickAppLinkHub(https)
465
+ });
466
+ }
222
467
  /**
223
- * The effective flagged notification address: the operator's explicit
224
- * choice, else AUTO — the CamStack app link (D709, operator decision
225
- * 2026-10-06; before it AUTO was the tunnel, then the mesh). The app link is
226
- * not a hub address: the NC's signed media and buttons keep their own hub
227
- * ladder when it is flagged; only viewer taps take the app form.
468
+ * Apply one change to the address book and persist it, normalised (a flag
469
+ * naming an address in no row becomes a custom address in the same write).
228
470
  */
229
- resolveNotificationEndpoint() {
230
- const explicit = this.config.notificationBaseUrl;
231
- return explicit !== "" ? explicit : require_dist.CAMSTACK_APP_LINK_ORIGIN;
471
+ async changeAddressBook(change) {
472
+ const { http, https } = await this.palettes();
473
+ const detected = [...http, ...https];
474
+ const detectedUrls = new Set(detected.map((e) => e.baseUrl));
475
+ const changed = change(this.addressState(detected), {
476
+ detectedUrls,
477
+ autoApp: [...new Set(detected.filter(require_dist.isDefaultAppConnectionEndpoint).map((e) => e.baseUrl))]
478
+ });
479
+ const after = normalizeAddressBook({
480
+ ...addressBookPatch(changed),
481
+ customAddresses: changed.custom
482
+ }, detectedUrls);
483
+ await this.updateGlobalSettings(addressBookPatch(after));
484
+ }
485
+ /** The deprecated `getHubAddresses` shape, from the role resolvers. */
486
+ async snapshotHubAddresses() {
487
+ const { roles } = await this.addressBook("https");
488
+ const legacy = (r) => ({
489
+ override: r.flagged,
490
+ resolved: r.resolved,
491
+ source: r.source === "flagged" ? "override" : r.source
492
+ });
493
+ return {
494
+ cluster: legacy(roles.internal),
495
+ browser: legacy(roles.publicEndpoint)
496
+ };
232
497
  }
233
498
  /**
234
- * The `hub=` an app link carries: the best public HTTPS address in this
235
- * cap's own palette (tunnel, then mesh) — so it can never disagree with
236
- * what the Network page lists. See `pickAppLinkHub`.
499
+ * Flag / unflag `baseUrl` for `role`, or (`baseUrl: null`) return the role
500
+ * to automatic. Validated: the address must be a row (detected, custom, or
501
+ * the app link for `notificationTaps`), never loopback, and
502
+ * `publicEndpoint` on plain http only when accepted. A refusal stores nothing.
237
503
  */
238
- async resolveAppLinkHub() {
239
- const port = (await this.resolveAdvertisedListenPort(null)).port;
240
- return require_dist.pickAppLinkHub(await this.buildAdvertisedEndpoints(false, false, "https", port));
504
+ async setRole(role, baseUrl, enabled, acceptInsecureHttp) {
505
+ if (baseUrl === null || baseUrl.trim() === "" && role !== "appConnections") {
506
+ await this.changeAddressBook((state) => withRoleAutomatic(state, role));
507
+ this.ctx.logger.info("local-network: address role set to automatic", { meta: { role } });
508
+ return;
509
+ }
510
+ let target = baseUrl.trim();
511
+ if (enabled) {
512
+ const check = require_dist.checkRoleAddress(role, target, { acceptInsecureHttp });
513
+ if (!check.ok) throw new Error(check.reason);
514
+ target = check.baseUrl;
515
+ }
516
+ await this.changeAddressBook((state, ctx) => {
517
+ const known = require_dist.isAppLinkBase(target) || ctx.detectedUrls.has(target) || state.custom.some((c) => c.baseUrl === target);
518
+ if (enabled && !known && role !== "notificationTaps") throw new Error(`${target} is not in the address list — add it as a custom address first.`);
519
+ return withRoleFlag(state, role, target, enabled, ctx.autoApp);
520
+ });
521
+ this.ctx.logger.info("local-network: address role set", { meta: {
522
+ role,
523
+ baseUrl: target,
524
+ enabled
525
+ } });
241
526
  }
242
527
  async notificationEndpointSnapshot() {
243
- const explicit = this.config.notificationBaseUrl;
528
+ const resolution = require_dist.resolveNotificationTapAddress(this.storedFlags());
529
+ const port = (await this.resolveAdvertisedListenPort(null)).port;
244
530
  return {
245
- baseUrl: explicit === "" ? null : explicit,
246
- resolved: this.resolveNotificationEndpoint(),
247
- appLinkHub: await this.resolveAppLinkHub()
531
+ baseUrl: resolution.flagged,
532
+ resolved: resolution.resolved,
533
+ appLinkHub: require_dist.pickAppLinkHub(await this.buildAdvertisedEndpoints(false, false, "https", port))
248
534
  };
249
535
  }
250
536
  async snapshotViewerEndpoints() {
251
- const selected = selectViewerEndpoints(await this.buildAdvertisedEndpoints(false, false, "http", (await this.resolveAdvertisedListenPort(null)).port), this.config.viewerBaseUrls);
537
+ const resolution = require_dist.resolveAppConnections(await this.buildAdvertisedEndpoints(false, false, "http", (await this.resolveAdvertisedListenPort(null)).port), this.storedFlags());
252
538
  return {
253
- baseUrls: this.config.viewerBaseUrls,
254
- resolved: selected.map((e) => e.baseUrl)
539
+ baseUrls: resolution.flagged,
540
+ resolved: resolution.resolved
255
541
  };
256
542
  }
257
543
  /**
258
- * Palette shown in the Network tab: LAN IPv4, stable LAN IPv6, public,
259
- * mesh. The viewer HTTP/WS race is a subset (`selectViewerEndpoints`) —
260
- * IPv6 stays in this list because ICE/WebRTC gathers it regardless.
544
+ * Palette shown to clients: LAN IPv4, stable LAN IPv6, public, mesh, and
545
+ * the custom addresses flagged `appConnections` (`kind: 'custom'`). The app
546
+ * HTTP/WS race is a subset (`selectAppConnectionEndpoints`) — IPv6 stays in this
547
+ * list because ICE/WebRTC gathers it regardless.
261
548
  */
262
549
  async buildAdvertisedEndpoints(includeLoopback, ipv4Only, scheme, httpsPort) {
263
- const allow = this.config.allowedAddresses;
264
- const interfaces = applyAllowlist(this.enumerate(), allow);
265
550
  await this.reconcileMeshEndpoint();
551
+ const detected = this.endpointsNow(includeLoopback, ipv4Only, scheme, httpsPort);
552
+ return withAppConnectionCustomEndpoints(detected, this.addressState(detected));
553
+ }
554
+ /** The DETECTED palette from the cached interface/tunnel/mesh state. */
555
+ endpointsNow(includeLoopback, ipv4Only, scheme, httpsPort) {
556
+ const interfaces = applyAllowlist(this.enumerate(), this.config.allowedAddresses);
266
557
  const httpState = require_tls.readLanHttpState();
267
558
  return buildEndpoints(interfaces, httpsPort, includeLoopback, ipv4Only, this.publicHostname, scheme, this.meshEndpoint, {
268
559
  enabled: this.config.httpEnabled !== false,
@@ -305,35 +596,50 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
305
596
  },
306
597
  getNotificationEndpoint: async () => this.notificationEndpointSnapshot(),
307
598
  setNotificationEndpoint: async ({ baseUrl }) => {
308
- await this.updateGlobalSettings({ notificationBaseUrl: baseUrl ?? "" });
309
- this.ctx.logger.info("local-network: notification endpoint set", { meta: { baseUrl: baseUrl ?? "(auto)" } });
599
+ await this.setRole("notificationTaps", baseUrl, true, false);
310
600
  return this.notificationEndpointSnapshot();
311
601
  },
312
- getHubAddresses: async () => this.snapshotHubAddresses(),
313
- setHubAddress: async ({ audience, baseUrl }) => {
314
- let stored = "";
315
- if (baseUrl !== null && baseUrl.trim() !== "") {
316
- const check = require_dist.validateHubAddressOverride(baseUrl);
317
- if (!check.ok) throw new Error(check.reason);
318
- stored = check.baseUrl;
319
- }
320
- await this.updateGlobalSettings(audience === "cluster" ? { agentBaseUrl: stored } : { signInBaseUrl: stored });
321
- this.ctx.logger.info("local-network: hub address override set", { meta: {
322
- audience,
323
- baseUrl: stored === "" ? "(auto)" : stored
324
- } });
325
- return this.snapshotHubAddresses();
326
- },
327
602
  getViewerEndpoints: async () => this.snapshotViewerEndpoints(),
328
603
  setViewerEndpoints: async ({ baseUrls }) => {
329
- const cleaned = [...new Set(baseUrls.filter((u) => u.length > 0))];
330
- await this.updateGlobalSettings({ viewerBaseUrls: cleaned });
331
- this.ctx.logger.info("local-network: viewer endpoints set", { meta: {
332
- count: cleaned.length,
333
- auto: cleaned.length === 0
604
+ const appConnections = [...new Set(baseUrls.filter((u) => u.length > 0))];
605
+ await this.changeAddressBook((state) => ({
606
+ ...state,
607
+ flags: {
608
+ ...state.flags,
609
+ appConnections
610
+ }
611
+ }));
612
+ this.ctx.logger.info("local-network: app connections set", { meta: {
613
+ count: appConnections.length,
614
+ auto: appConnections.length === 0
334
615
  } });
335
616
  return this.snapshotViewerEndpoints();
336
617
  },
618
+ getAddresses: async (input) => this.addressBook(input?.scheme ?? "https"),
619
+ addCustomAddress: async ({ baseUrl, label }) => {
620
+ const check = require_dist.validateCustomAddress(baseUrl);
621
+ if (!check.ok) throw new Error(check.reason);
622
+ const entry = {
623
+ baseUrl: check.baseUrl,
624
+ label: label.trim()
625
+ };
626
+ await this.changeAddressBook((state) => withCustomAddress(state, entry));
627
+ this.ctx.logger.info("local-network: custom address added", { meta: {
628
+ baseUrl: entry.baseUrl,
629
+ label: entry.label
630
+ } });
631
+ return this.addressBook("https");
632
+ },
633
+ removeCustomAddress: async ({ baseUrl }) => {
634
+ await this.changeAddressBook((state) => withoutCustomAddress(state, baseUrl));
635
+ this.ctx.logger.info("local-network: custom address removed", { meta: { baseUrl } });
636
+ return this.addressBook("https");
637
+ },
638
+ setAddressRole: async ({ role, baseUrl, enabled, acceptInsecureHttp }) => {
639
+ await this.setRole(role, baseUrl, enabled, acceptInsecureHttp === true);
640
+ return this.addressBook("https");
641
+ },
642
+ getHubAddresses: async () => this.snapshotHubAddresses(),
337
643
  getAllowedAddresses: async () => ({ addresses: this.config.allowedAddresses }),
338
644
  resetAllowlistToBestMatch: async () => {
339
645
  const seed = autoSeedAllowlist(this.enumerate());
@@ -875,25 +1181,6 @@ function buildEndpoints(interfaces, port, includeLoopback, ipv4Only, publicHostn
875
1181
  });
876
1182
  return out.toSorted((a, b) => a.priority - b.priority);
877
1183
  }
878
- /** AUTO viewer race: LAN IPv4 + the public tunnel. Never IPv6, mesh, or loopback. */
879
- function isDefaultViewerEndpoint(endpoint) {
880
- if (endpoint.kind === "lan-ipv4") return true;
881
- return endpoint.kind === "public" && endpoint.interfaceKind === "public";
882
- }
883
- /**
884
- * The URLs the SDK should race for API / viewer access.
885
- *
886
- * Empty `viewerBaseUrls` = AUTO (IPv4 + public tunnel). A non-empty list is
887
- * the operator's pin; if none of those URLs are in the current palette,
888
- * fall back to AUTO rather than racing an empty set.
889
- */
890
- function selectViewerEndpoints(endpoints, viewerBaseUrls) {
891
- const auto = endpoints.filter(isDefaultViewerEndpoint);
892
- if (viewerBaseUrls.length === 0) return auto;
893
- const wanted = new Set(viewerBaseUrls);
894
- const explicit = endpoints.filter((e) => wanted.has(e.baseUrl));
895
- return explicit.length > 0 ? explicit : auto;
896
- }
897
1184
  /**
898
1185
  * First-boot heuristic: which addresses should the allowlist start
899
1186
  * with? Includes LAN + Wi-Fi IPv4 addresses + plausible IPv6:
@@ -1032,12 +1319,10 @@ exports.classifyKind = classifyKind;
1032
1319
  exports.enumerateOsInterfaces = enumerateOsInterfaces;
1033
1320
  exports.explainNonPlausible = explainNonPlausible;
1034
1321
  exports.ipv6Prefix64Key = ipv6Prefix64Key;
1035
- exports.isDefaultViewerEndpoint = isDefaultViewerEndpoint;
1036
1322
  exports.isPlausibleAutoSeed = isPlausibleAutoSeed;
1037
1323
  exports.parseProcIfInet6 = parseProcIfInet6;
1038
1324
  exports.pickPreferred = pickPreferred;
1039
1325
  exports.prefixLen = prefixLen;
1040
1326
  exports.reconcileAllowlist = reconcileAllowlist;
1041
1327
  exports.resolveAdvertisedPort = resolveAdvertisedPort;
1042
- exports.selectViewerEndpoints = selectViewerEndpoints;
1043
1328
  exports.shouldAdvertiseIpv6 = shouldAdvertiseIpv6;