@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,8 +1,234 @@
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 isDefaultAppConnectionEndpoint, Sn as resolveNotificationTapAddress, ar as BaseAddon, cn as pickAppLinkHub, hn as resolveAppConnections, s as CAMSTACK_APP_LINK_ORIGIN, tt as checkRoleAddress, yn as resolveHubAddressRoles } from "../../dist-C9Rk6KVv.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 (D734)
13
+ *
14
+ * One key per role: `appConnectionUrls` (`appConnections`, many),
15
+ * `notificationTapUrl` (`notificationTaps`), `publicEndpointUrl`
16
+ * (`publicEndpoint`) and `internalUrl` (`internal`). A single-valued role is
17
+ * one URL — at most one address carries it, by construction. `''` / `[]` is
18
+ * automatic. The custom addresses are `customAddresses` (url + label).
19
+ *
20
+ * The keys of the earlier role names (`viewerBaseUrls`, `notificationBaseUrl`,
21
+ * `signInBaseUrl`, `agentBaseUrl`) are not read: a value stored under one is
22
+ * ignored, and that role is automatic until the operator flags it again
23
+ * (D734 — one install, re-set by hand, no migration code).
24
+ *
25
+ * ## A flag on an address that is in no row
26
+ *
27
+ * A flagged detected address can disappear (a DHCP move) while it still wins
28
+ * its role. {@link normalizeAddressBook} turns every single-valued flag that
29
+ * names neither a detected address, a custom one nor the app link into a
30
+ * custom address carrying that role, on every read; the next write persists
31
+ * the result — so the address shows as a custom row instead of vanishing.
32
+ */
33
+ var CustomAddressListSchema = z.array(z.object({
34
+ baseUrl: z.string(),
35
+ label: z.string()
36
+ }));
37
+ /** The label a flag promoted to a custom address is given. */
38
+ var PROMOTED_LABEL = {
39
+ notificationTaps: "Notification taps address",
40
+ internal: "Internal address",
41
+ publicEndpoint: "Public endpoint"
42
+ };
43
+ var SINGLE_ROLES = [
44
+ "notificationTaps",
45
+ "internal",
46
+ "publicEndpoint"
47
+ ];
48
+ function flagOf(value) {
49
+ return value === "" ? null : value;
50
+ }
51
+ /** Parse the stored custom list; a malformed store reads as empty, never throws. */
52
+ function parseCustomAddresses(raw) {
53
+ const parsed = CustomAddressListSchema.safeParse(raw ?? []);
54
+ return parsed.success ? parsed.data : [];
55
+ }
56
+ /**
57
+ * The address book as it stands: stored custom addresses, plus every
58
+ * single-valued flag that names neither a detected address, a custom one nor
59
+ * the app link, promoted to a custom address (see the module doc).
60
+ */
61
+ function normalizeAddressBook(stored, detectedUrls) {
62
+ const flags = {
63
+ appConnections: stored.appConnectionUrls,
64
+ notificationTaps: flagOf(stored.notificationTapUrl),
65
+ internal: flagOf(stored.internalUrl),
66
+ publicEndpoint: flagOf(stored.publicEndpointUrl)
67
+ };
68
+ const custom = [...parseCustomAddresses(stored.customAddresses)];
69
+ for (const role of SINGLE_ROLES) {
70
+ const url = flags[role];
71
+ if (url === null || isAppLinkBase(url) || detectedUrls.has(url)) continue;
72
+ if (custom.some((c) => c.baseUrl === url)) continue;
73
+ custom.push({
74
+ baseUrl: url,
75
+ label: PROMOTED_LABEL[role]
76
+ });
77
+ }
78
+ return {
79
+ custom,
80
+ flags
81
+ };
82
+ }
83
+ /** The stored-settings patch that persists `state`. */
84
+ function addressBookPatch(state) {
85
+ return {
86
+ customAddresses: state.custom,
87
+ appConnectionUrls: state.flags.appConnections,
88
+ notificationTapUrl: state.flags.notificationTaps ?? "",
89
+ publicEndpointUrl: state.flags.publicEndpoint ?? "",
90
+ internalUrl: state.flags.internal ?? ""
91
+ };
92
+ }
93
+ function withCustomAddress(state, entry) {
94
+ const custom = state.custom.some((c) => c.baseUrl === entry.baseUrl) ? state.custom.map((c) => c.baseUrl === entry.baseUrl ? entry : c) : [...state.custom, entry];
95
+ return {
96
+ ...state,
97
+ custom
98
+ };
99
+ }
100
+ /** Drop a custom address and every flag it carried (those roles turn automatic). */
101
+ function withoutCustomAddress(state, baseUrl) {
102
+ const clear = (flag) => flag === baseUrl ? null : flag;
103
+ return {
104
+ custom: state.custom.filter((c) => c.baseUrl !== baseUrl),
105
+ flags: {
106
+ appConnections: state.flags.appConnections.filter((u) => u !== baseUrl),
107
+ notificationTaps: clear(state.flags.notificationTaps),
108
+ internal: clear(state.flags.internal),
109
+ publicEndpoint: clear(state.flags.publicEndpoint)
110
+ }
111
+ };
112
+ }
113
+ /** Every role back to automatic for `role`. */
114
+ function withRoleAutomatic(state, role) {
115
+ return role === "appConnections" ? {
116
+ ...state,
117
+ flags: {
118
+ ...state.flags,
119
+ appConnections: []
120
+ }
121
+ } : {
122
+ ...state,
123
+ flags: {
124
+ ...state.flags,
125
+ [role]: null
126
+ }
127
+ };
128
+ }
129
+ /**
130
+ * Flag or unflag `baseUrl` for `role`. A single-valued role flagged here moves
131
+ * off every other address (it is one URL); unflagging the address that carries
132
+ * it returns the role to automatic. The app link flagged for notification
133
+ * taps IS the automatic answer, stored as automatic — one representation.
134
+ * `appConnections` toggles inside the set; toggling while automatic starts
135
+ * from the automatic set (`autoApp`) so the other addresses keep racing.
136
+ */
137
+ function withRoleFlag(state, role, baseUrl, enabled, autoApp) {
138
+ if (role === "appConnections") {
139
+ const without = (state.flags.appConnections.length === 0 ? autoApp : state.flags.appConnections).filter((u) => u !== baseUrl);
140
+ const appConnections = enabled ? [...without, baseUrl] : without;
141
+ return {
142
+ ...state,
143
+ flags: {
144
+ ...state.flags,
145
+ appConnections
146
+ }
147
+ };
148
+ }
149
+ if (role === "notificationTaps" && isAppLinkBase(baseUrl)) return withRoleAutomatic(state, role);
150
+ if (enabled) return {
151
+ ...state,
152
+ flags: {
153
+ ...state.flags,
154
+ [role]: baseUrl
155
+ }
156
+ };
157
+ return state.flags[role] === baseUrl ? withRoleAutomatic(state, role) : state;
158
+ }
159
+ /** What a row may be flagged with. */
160
+ function eligibleRoles(endpoint) {
161
+ if (endpoint.kind === "loopback") return [];
162
+ if (endpoint.kind === "app-link") return ["notificationTaps"];
163
+ return [
164
+ "appConnections",
165
+ "notificationTaps",
166
+ "internal",
167
+ "publicEndpoint"
168
+ ];
169
+ }
170
+ /** A custom address as a palette entry (`kind: 'custom'`). */
171
+ function customEndpoint(entry) {
172
+ return {
173
+ label: entry.label === "" ? "Custom address" : entry.label,
174
+ baseUrl: entry.baseUrl,
175
+ kind: "custom",
176
+ interfaceKind: "other",
177
+ plausible: true,
178
+ plausibleReason: "",
179
+ priority: 200
180
+ };
181
+ }
182
+ /**
183
+ * The palette a client races: the detected endpoints plus the custom
184
+ * addresses flagged `appConnections` (a detected URL is never listed twice).
185
+ */
186
+ function withAppConnectionCustomEndpoints(detected, state) {
187
+ const known = new Set(detected.map((e) => e.baseUrl));
188
+ const wanted = new Set(state.flags.appConnections);
189
+ const extra = state.custom.filter((c) => wanted.has(c.baseUrl) && !known.has(c.baseUrl)).map(customEndpoint);
190
+ return [...detected, ...extra];
191
+ }
192
+ /** Every role's answer — the resolvers from `@camstack/types`, nothing else. */
193
+ function resolveRoles(state, appPalette, httpsPalette) {
194
+ return resolveHubAddressRoles(withAppConnectionCustomEndpoints(appPalette, state), httpsPalette, state.flags);
195
+ }
196
+ function flagsOn(state, baseUrl) {
197
+ const out = [];
198
+ if (state.flags.appConnections.includes(baseUrl)) out.push("appConnections");
199
+ for (const role of SINGLE_ROLES) if (state.flags[role] === baseUrl) out.push(role);
200
+ return out;
201
+ }
202
+ function inUseOn(roles, baseUrl) {
203
+ const out = [];
204
+ if (roles.appConnections.resolved.includes(baseUrl)) out.push("appConnections");
205
+ for (const role of SINGLE_ROLES) if (roles[role].resolved === baseUrl) out.push(role);
206
+ return out;
207
+ }
208
+ /** The Addresses table: app link, detected rows, then custom-only rows. */
209
+ function buildAddressBook(state, inputs) {
210
+ const roles = resolveRoles(state, inputs.rowsPalette, inputs.httpsPalette);
211
+ const customUrls = new Set(state.custom.map((c) => c.baseUrl));
212
+ const detectedUrls = new Set(inputs.rowsPalette.map((e) => e.baseUrl));
213
+ const row = (endpoint, detected) => ({
214
+ ...endpoint,
215
+ detected,
216
+ custom: customUrls.has(endpoint.baseUrl),
217
+ flags: endpoint.kind === "app-link" ? [] : flagsOn(state, endpoint.baseUrl),
218
+ inUse: endpoint.kind === "app-link" ? roles.notificationTaps.resolved === "https://camstack.zentik.app" ? ["notificationTaps"] : [] : inUseOn(roles, endpoint.baseUrl),
219
+ eligible: eligibleRoles(endpoint)
220
+ });
221
+ return {
222
+ rows: [
223
+ row(inputs.appLink, false),
224
+ ...inputs.rowsPalette.map((e) => row(e, true)),
225
+ ...state.custom.filter((c) => !detectedUrls.has(c.baseUrl)).map((c) => row(customEndpoint(c), false))
226
+ ],
227
+ roles,
228
+ appLinkHub: inputs.appLinkHub
229
+ };
230
+ }
231
+ //#endregion
6
232
  //#region src/builtins/local-network/ipv6-lan.ts
7
233
  /**
8
234
  * IPv6 helpers for LAN advertisement: prefix matching (privacy addresses
@@ -194,72 +420,137 @@ var LocalNetworkAddon = class extends BaseAddon {
194
420
  super({
195
421
  allowedAddresses: [],
196
422
  bootSeeded: false,
197
- notificationBaseUrl: "",
198
- viewerBaseUrls: [],
423
+ notificationTapUrl: "",
424
+ appConnectionUrls: [],
425
+ customAddresses: [],
199
426
  httpEnabled: true,
200
427
  httpPort: DEFAULT_LAN_HTTP_PORT,
201
428
  localHostname: "",
202
- agentBaseUrl: "",
203
- signInBaseUrl: ""
429
+ internalUrl: "",
430
+ publicEndpointUrl: ""
204
431
  });
205
432
  }
206
433
  /**
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`.
434
+ * The detected palettes, with the mesh reconciled once: `https` (what
435
+ * `internal` and `publicEndpoint` resolve over, and the app link's `hub=`)
436
+ * and `http` (what the app-connections race resolves over).
210
437
  */
211
- async snapshotHubAddresses() {
438
+ async palettes() {
212
439
  const port = (await this.resolveAdvertisedListenPort(null)).port;
213
- const palette = await this.buildAdvertisedEndpoints(false, false, "https", port);
440
+ await this.reconcileMeshEndpoint();
214
441
  return {
215
- cluster: resolveHubAddress(palette, "cluster", this.config.agentBaseUrl || null),
216
- browser: resolveHubAddress(palette, "browser", this.config.signInBaseUrl || null)
442
+ http: this.endpointsNow(false, false, "http", port),
443
+ https: this.endpointsNow(false, false, "https", port)
217
444
  };
218
445
  }
446
+ /** The stored address book, normalised against what is detected now. */
447
+ addressState(detected) {
448
+ return normalizeAddressBook(this.config, new Set(detected.map((e) => e.baseUrl)));
449
+ }
450
+ /** The role flags alone — they do not depend on what is detected. */
451
+ storedFlags() {
452
+ return normalizeAddressBook(this.config, /* @__PURE__ */ new Set()).flags;
453
+ }
454
+ /** `getAddresses`: the Addresses table and every role's answer. */
455
+ async addressBook(scheme) {
456
+ const { http, https } = await this.palettes();
457
+ return buildAddressBook(this.addressState([...http, ...https]), {
458
+ rowsPalette: scheme === "http" ? http : https,
459
+ httpsPalette: https,
460
+ appLink: appLinkEndpoint(),
461
+ appLinkHub: pickAppLinkHub(https)
462
+ });
463
+ }
219
464
  /**
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.
465
+ * Apply one change to the address book and persist it, normalised (a flag
466
+ * naming an address in no row becomes a custom address in the same write).
225
467
  */
226
- resolveNotificationEndpoint() {
227
- const explicit = this.config.notificationBaseUrl;
228
- return explicit !== "" ? explicit : CAMSTACK_APP_LINK_ORIGIN;
468
+ async changeAddressBook(change) {
469
+ const { http, https } = await this.palettes();
470
+ const detected = [...http, ...https];
471
+ const detectedUrls = new Set(detected.map((e) => e.baseUrl));
472
+ const changed = change(this.addressState(detected), {
473
+ detectedUrls,
474
+ autoApp: [...new Set(detected.filter(isDefaultAppConnectionEndpoint).map((e) => e.baseUrl))]
475
+ });
476
+ const after = normalizeAddressBook({
477
+ ...addressBookPatch(changed),
478
+ customAddresses: changed.custom
479
+ }, detectedUrls);
480
+ await this.updateGlobalSettings(addressBookPatch(after));
481
+ }
482
+ /** The deprecated `getHubAddresses` shape, from the role resolvers. */
483
+ async snapshotHubAddresses() {
484
+ const { roles } = await this.addressBook("https");
485
+ const legacy = (r) => ({
486
+ override: r.flagged,
487
+ resolved: r.resolved,
488
+ source: r.source === "flagged" ? "override" : r.source
489
+ });
490
+ return {
491
+ cluster: legacy(roles.internal),
492
+ browser: legacy(roles.publicEndpoint)
493
+ };
229
494
  }
230
495
  /**
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`.
496
+ * Flag / unflag `baseUrl` for `role`, or (`baseUrl: null`) return the role
497
+ * to automatic. Validated: the address must be a row (detected, custom, or
498
+ * the app link for `notificationTaps`), never loopback, and
499
+ * `publicEndpoint` on plain http only when accepted. A refusal stores nothing.
234
500
  */
235
- async resolveAppLinkHub() {
236
- const port = (await this.resolveAdvertisedListenPort(null)).port;
237
- return pickAppLinkHub(await this.buildAdvertisedEndpoints(false, false, "https", port));
501
+ async setRole(role, baseUrl, enabled, acceptInsecureHttp) {
502
+ if (baseUrl === null || baseUrl.trim() === "" && role !== "appConnections") {
503
+ await this.changeAddressBook((state) => withRoleAutomatic(state, role));
504
+ this.ctx.logger.info("local-network: address role set to automatic", { meta: { role } });
505
+ return;
506
+ }
507
+ let target = baseUrl.trim();
508
+ if (enabled) {
509
+ const check = checkRoleAddress(role, target, { acceptInsecureHttp });
510
+ if (!check.ok) throw new Error(check.reason);
511
+ target = check.baseUrl;
512
+ }
513
+ await this.changeAddressBook((state, ctx) => {
514
+ const known = isAppLinkBase(target) || ctx.detectedUrls.has(target) || state.custom.some((c) => c.baseUrl === target);
515
+ if (enabled && !known && role !== "notificationTaps") throw new Error(`${target} is not in the address list — add it as a custom address first.`);
516
+ return withRoleFlag(state, role, target, enabled, ctx.autoApp);
517
+ });
518
+ this.ctx.logger.info("local-network: address role set", { meta: {
519
+ role,
520
+ baseUrl: target,
521
+ enabled
522
+ } });
238
523
  }
239
524
  async notificationEndpointSnapshot() {
240
- const explicit = this.config.notificationBaseUrl;
525
+ const resolution = resolveNotificationTapAddress(this.storedFlags());
526
+ const port = (await this.resolveAdvertisedListenPort(null)).port;
241
527
  return {
242
- baseUrl: explicit === "" ? null : explicit,
243
- resolved: this.resolveNotificationEndpoint(),
244
- appLinkHub: await this.resolveAppLinkHub()
528
+ baseUrl: resolution.flagged,
529
+ resolved: resolution.resolved,
530
+ appLinkHub: pickAppLinkHub(await this.buildAdvertisedEndpoints(false, false, "https", port))
245
531
  };
246
532
  }
247
533
  async snapshotViewerEndpoints() {
248
- const selected = selectViewerEndpoints(await this.buildAdvertisedEndpoints(false, false, "http", (await this.resolveAdvertisedListenPort(null)).port), this.config.viewerBaseUrls);
534
+ const resolution = resolveAppConnections(await this.buildAdvertisedEndpoints(false, false, "http", (await this.resolveAdvertisedListenPort(null)).port), this.storedFlags());
249
535
  return {
250
- baseUrls: this.config.viewerBaseUrls,
251
- resolved: selected.map((e) => e.baseUrl)
536
+ baseUrls: resolution.flagged,
537
+ resolved: resolution.resolved
252
538
  };
253
539
  }
254
540
  /**
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.
541
+ * Palette shown to clients: LAN IPv4, stable LAN IPv6, public, mesh, and
542
+ * the custom addresses flagged `appConnections` (`kind: 'custom'`). The app
543
+ * HTTP/WS race is a subset (`selectAppConnectionEndpoints`) — IPv6 stays in this
544
+ * list because ICE/WebRTC gathers it regardless.
258
545
  */
259
546
  async buildAdvertisedEndpoints(includeLoopback, ipv4Only, scheme, httpsPort) {
260
- const allow = this.config.allowedAddresses;
261
- const interfaces = applyAllowlist(this.enumerate(), allow);
262
547
  await this.reconcileMeshEndpoint();
548
+ const detected = this.endpointsNow(includeLoopback, ipv4Only, scheme, httpsPort);
549
+ return withAppConnectionCustomEndpoints(detected, this.addressState(detected));
550
+ }
551
+ /** The DETECTED palette from the cached interface/tunnel/mesh state. */
552
+ endpointsNow(includeLoopback, ipv4Only, scheme, httpsPort) {
553
+ const interfaces = applyAllowlist(this.enumerate(), this.config.allowedAddresses);
263
554
  const httpState = readLanHttpState();
264
555
  return buildEndpoints(interfaces, httpsPort, includeLoopback, ipv4Only, this.publicHostname, scheme, this.meshEndpoint, {
265
556
  enabled: this.config.httpEnabled !== false,
@@ -302,35 +593,50 @@ var LocalNetworkAddon = class extends BaseAddon {
302
593
  },
303
594
  getNotificationEndpoint: async () => this.notificationEndpointSnapshot(),
304
595
  setNotificationEndpoint: async ({ baseUrl }) => {
305
- await this.updateGlobalSettings({ notificationBaseUrl: baseUrl ?? "" });
306
- this.ctx.logger.info("local-network: notification endpoint set", { meta: { baseUrl: baseUrl ?? "(auto)" } });
596
+ await this.setRole("notificationTaps", baseUrl, true, false);
307
597
  return this.notificationEndpointSnapshot();
308
598
  },
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
599
  getViewerEndpoints: async () => this.snapshotViewerEndpoints(),
325
600
  setViewerEndpoints: async ({ baseUrls }) => {
326
- const cleaned = [...new Set(baseUrls.filter((u) => u.length > 0))];
327
- await this.updateGlobalSettings({ viewerBaseUrls: cleaned });
328
- this.ctx.logger.info("local-network: viewer endpoints set", { meta: {
329
- count: cleaned.length,
330
- auto: cleaned.length === 0
601
+ const appConnections = [...new Set(baseUrls.filter((u) => u.length > 0))];
602
+ await this.changeAddressBook((state) => ({
603
+ ...state,
604
+ flags: {
605
+ ...state.flags,
606
+ appConnections
607
+ }
608
+ }));
609
+ this.ctx.logger.info("local-network: app connections set", { meta: {
610
+ count: appConnections.length,
611
+ auto: appConnections.length === 0
331
612
  } });
332
613
  return this.snapshotViewerEndpoints();
333
614
  },
615
+ getAddresses: async (input) => this.addressBook(input?.scheme ?? "https"),
616
+ addCustomAddress: async ({ baseUrl, label }) => {
617
+ const check = validateCustomAddress(baseUrl);
618
+ if (!check.ok) throw new Error(check.reason);
619
+ const entry = {
620
+ baseUrl: check.baseUrl,
621
+ label: label.trim()
622
+ };
623
+ await this.changeAddressBook((state) => withCustomAddress(state, entry));
624
+ this.ctx.logger.info("local-network: custom address added", { meta: {
625
+ baseUrl: entry.baseUrl,
626
+ label: entry.label
627
+ } });
628
+ return this.addressBook("https");
629
+ },
630
+ removeCustomAddress: async ({ baseUrl }) => {
631
+ await this.changeAddressBook((state) => withoutCustomAddress(state, baseUrl));
632
+ this.ctx.logger.info("local-network: custom address removed", { meta: { baseUrl } });
633
+ return this.addressBook("https");
634
+ },
635
+ setAddressRole: async ({ role, baseUrl, enabled, acceptInsecureHttp }) => {
636
+ await this.setRole(role, baseUrl, enabled, acceptInsecureHttp === true);
637
+ return this.addressBook("https");
638
+ },
639
+ getHubAddresses: async () => this.snapshotHubAddresses(),
334
640
  getAllowedAddresses: async () => ({ addresses: this.config.allowedAddresses }),
335
641
  resetAllowlistToBestMatch: async () => {
336
642
  const seed = autoSeedAllowlist(this.enumerate());
@@ -872,25 +1178,6 @@ function buildEndpoints(interfaces, port, includeLoopback, ipv4Only, publicHostn
872
1178
  });
873
1179
  return out.toSorted((a, b) => a.priority - b.priority);
874
1180
  }
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
1181
  /**
895
1182
  * First-boot heuristic: which addresses should the allowlist start
896
1183
  * with? Includes LAN + Wi-Fi IPv4 addresses + plausible IPv6:
@@ -1019,4 +1306,4 @@ function readLinuxIpv6Flags() {
1019
1306
  }
1020
1307
  }
1021
1308
  //#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 };
1309
+ 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-Cv1lnnQq.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-C9Rk6KVv.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-Cv1lnnQq.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, gn as resolveBucketMs, hr as createEvent, ir as errMsg, pn as reducePoints } from "../../dist-C9Rk6KVv.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-Cv1lnnQq.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-C9Rk6KVv.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-Cv1lnnQq.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-C9Rk6KVv.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-Cv1lnnQq.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-C9Rk6KVv.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-Cv1lnnQq.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-C9Rk6KVv.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-Cv1lnnQq.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, bn as resolveLocationMode, jn as storageMigrationCapability, nt as collectSecretConfigKeys, tn as modeMayWrite, z as StorageLocationModeSchema, zt as isLocationEnabled } from "../../dist-C9Rk6KVv.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";