@camstack/system 1.2.115 → 1.2.116

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 (61) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  8. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  9. package/dist/builtins/console-logging/index.js +1 -1
  10. package/dist/builtins/console-logging/index.mjs +1 -1
  11. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  12. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  13. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  14. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  15. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  16. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  17. package/dist/builtins/hub-forwarder/index.js +1 -1
  18. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  19. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  20. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  21. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  22. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  23. package/dist/builtins/local-network/local-hostname.d.ts +15 -0
  24. package/dist/builtins/local-network/local-network.addon.d.ts +31 -7
  25. package/dist/builtins/local-network/local-network.addon.js +177 -28
  26. package/dist/builtins/local-network/local-network.addon.mjs +177 -28
  27. package/dist/builtins/loki-logging/index.js +1 -1
  28. package/dist/builtins/loki-logging/index.mjs +1 -1
  29. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  30. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  31. package/dist/builtins/platform-probe/index.js +1 -1
  32. package/dist/builtins/platform-probe/index.mjs +1 -1
  33. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  34. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  35. package/dist/builtins/snapshot/index.js +1 -1
  36. package/dist/builtins/snapshot/index.mjs +1 -1
  37. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  38. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  39. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -2
  40. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +2 -2
  41. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  42. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  43. package/dist/builtins/system-config/system-config.addon.js +1 -1
  44. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  45. package/dist/builtins/winston-logging/index.js +1 -1
  46. package/dist/builtins/winston-logging/index.mjs +1 -1
  47. package/dist/{dist-Drb54SZD.mjs → dist-BpzbLPel.mjs} +139 -5
  48. package/dist/{dist-iAwSA2_f.js → dist-Cri60Jd0.js} +139 -5
  49. package/dist/http/lan-http-bind.d.ts +19 -0
  50. package/dist/index.d.ts +1 -0
  51. package/dist/index.js +29 -472
  52. package/dist/index.mjs +11 -468
  53. package/dist/lan-http-bind-BDJm4d-K.mjs +716 -0
  54. package/dist/lan-http-bind-CZ4EBeD_.js +848 -0
  55. package/dist/{retired-settings-keys-OfhqQio4.js → retired-settings-keys-BVAxT5XL.js} +1 -1
  56. package/dist/{retired-settings-keys-Dz5-7gjt.mjs → retired-settings-keys-SrxMH3OT.mjs} +1 -1
  57. package/dist/tls/cert-manager.d.ts +11 -0
  58. package/dist/tls/index.d.ts +3 -1
  59. package/dist/tls/tls-status.d.ts +16 -0
  60. package/dist/tls/uploaded-cert.d.ts +14 -0
  61. package/package.json +1 -1
@@ -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-iAwSA2_f.js");
6
+ const require_dist = require("../../dist-Cri60Jd0.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Gt as EventCategory, v as addonPagesCapability, vt as errMsg, yt as BaseAddon } from "../../dist-Drb54SZD.mjs";
1
+ import { Gt as EventCategory, v as addonPagesCapability, vt as errMsg, yt as BaseAddon } from "../../dist-BpzbLPel.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import { createHash, randomUUID } from "node:crypto";
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-iAwSA2_f.js");
6
+ const require_dist = require("../../dist-Cri60Jd0.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Gt as EventCategory, b as addonWidgetsCapability, vt as errMsg, yt as BaseAddon } from "../../dist-Drb54SZD.mjs";
1
+ import { Gt as EventCategory, b as addonWidgetsCapability, vt as errMsg, yt as BaseAddon } from "../../dist-BpzbLPel.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import { createHash, randomUUID } from "node:crypto";
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
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-iAwSA2_f.js");
6
+ const require_dist = require("../../dist-Cri60Jd0.js");
7
7
  //#region src/builtins/alerts/prune-policy.ts
8
8
  /**
9
9
  * Ids older than `cutoffMs`, oldest first.
@@ -1,4 +1,4 @@
1
- import { Gt as EventCategory, Mt as createEvent, n as AlertSchema, vt as errMsg, x as alertsCapability, yt as BaseAddon } from "../../dist-Drb54SZD.mjs";
1
+ import { Gt as EventCategory, Mt as createEvent, n as AlertSchema, vt as errMsg, x as alertsCapability, yt as BaseAddon } from "../../dist-BpzbLPel.mjs";
2
2
  //#region src/builtins/alerts/prune-policy.ts
3
3
  /**
4
4
  * Ids older than `cutoffMs`, oldest first.
@@ -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-iAwSA2_f.js");
6
+ const require_dist = require("../../dist-Cri60Jd0.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let buffer = require("buffer");
9
9
  let node_fs_promises = require("node:fs/promises");
@@ -1,4 +1,4 @@
1
- import { C as backupCapability, Gt as EventCategory, yt as BaseAddon } from "../../dist-Drb54SZD.mjs";
1
+ import { C as backupCapability, Gt as EventCategory, yt as BaseAddon } from "../../dist-BpzbLPel.mjs";
2
2
  import { randomBytes, randomUUID } from "node:crypto";
3
3
  import { Buffer as Buffer$1 } from "buffer";
4
4
  import * as fsp from "node:fs/promises";
@@ -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-iAwSA2_f.js");
6
+ const require_dist = require("../../dist-Cri60Jd0.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  //#region src/builtins/console-logging/console-destination.ts
9
9
  var LEVEL_RANK = {
@@ -1,4 +1,4 @@
1
- import { X as logDestinationCapability, yt as BaseAddon } from "../../dist-Drb54SZD.mjs";
1
+ import { X as logDestinationCapability, yt as BaseAddon } from "../../dist-BpzbLPel.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  //#region src/builtins/console-logging/console-destination.ts
4
4
  var LEVEL_RANK = {
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-iAwSA2_f.js");
3
+ const require_dist = require("../../dist-Cri60Jd0.js");
4
4
  let node_crypto = require("node:crypto");
5
5
  let node_fs_promises = require("node:fs/promises");
6
6
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -1,4 +1,4 @@
1
- import { D as coreBlockAddonId, O as coreBlockIdFromAddonId, c as CoreBlockSchema, k as coreBlocksCapability, l as DeclaredDevices, o as CORE_BLOCKS_ADDON_ID, s as CORE_BLOCK_ADDON_PREFIX, vt as errMsg, yt as BaseAddon } from "../../dist-Drb54SZD.mjs";
1
+ import { D as coreBlockAddonId, O as coreBlockIdFromAddonId, c as CoreBlockSchema, k as coreBlocksCapability, l as DeclaredDevices, o as CORE_BLOCKS_ADDON_ID, s as CORE_BLOCK_ADDON_PREFIX, vt as errMsg, yt as BaseAddon } from "../../dist-BpzbLPel.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as path$1 from "node:path";
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-iAwSA2_f.js");
7
- const require_retired_settings_keys = require("../../retired-settings-keys-OfhqQio4.js");
6
+ const require_dist = require("../../dist-Cri60Jd0.js");
7
+ const require_retired_settings_keys = require("../../retired-settings-keys-BVAxT5XL.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let _camstack_types_node = require("@camstack/types/node");
10
10
  //#region src/builtins/device-manager/adoption-job-engine.ts
@@ -1,5 +1,5 @@
1
- import { Ct as DeviceFeature, E as buildStreamParamsConfigSchema, F as deviceStatusCapability, Gt as EventCategory, Lt as isDeviceConfigCap, N as deviceManagerCapability, Ot as WELL_KNOWN_TAB_MAP, P as deviceStateCapability, R as enumerateItemArrayFields, Tt as DeviceType, Wt as sleep, a as CAP_NAMES_WITH_STATUS, at as runtimeStatePolicyFor, et as normalizeUnit, nt as parseStreamParamsFormPatch, p as STREAM_PROFILE_META, t as ALL_CAPABILITY_DEFINITIONS, u as DeviceStatusSchema, vt as errMsg, wt as DeviceRole, yt as BaseAddon, z as enumerateSchemaFields } from "../../dist-Drb54SZD.mjs";
2
- import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-Dz5-7gjt.mjs";
1
+ import { Ct as DeviceFeature, E as buildStreamParamsConfigSchema, F as deviceStatusCapability, Gt as EventCategory, Lt as isDeviceConfigCap, N as deviceManagerCapability, Ot as WELL_KNOWN_TAB_MAP, P as deviceStateCapability, R as enumerateItemArrayFields, Tt as DeviceType, Wt as sleep, a as CAP_NAMES_WITH_STATUS, at as runtimeStatePolicyFor, et as normalizeUnit, nt as parseStreamParamsFormPatch, p as STREAM_PROFILE_META, t as ALL_CAPABILITY_DEFINITIONS, u as DeviceStatusSchema, vt as errMsg, wt as DeviceRole, yt as BaseAddon, z as enumerateSchemaFields } from "../../dist-BpzbLPel.mjs";
2
+ import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-SrxMH3OT.mjs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { canonicalDeviceFingerprint } from "@camstack/types/node";
5
5
  //#region src/builtins/device-manager/adoption-job-engine.ts
@@ -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-iAwSA2_f.js");
6
+ const require_dist = require("../../dist-Cri60Jd0.js");
7
7
  //#region src/builtins/doorbell/trigger-engine.ts
8
8
  /**
9
9
  * Pure trigger logic for the virtual-doorbell builtin — edge detection over
@@ -1,4 +1,4 @@
1
- import { G as isSameAddonId, Gt as EventCategory, I as doorbellCapability, Mt as createEvent, Tt as DeviceType, vt as errMsg, w as bareAddonId, yt as BaseAddon } from "../../dist-Drb54SZD.mjs";
1
+ import { G as isSameAddonId, Gt as EventCategory, I as doorbellCapability, Mt as createEvent, Tt as DeviceType, vt as errMsg, w as bareAddonId, yt as BaseAddon } from "../../dist-BpzbLPel.mjs";
2
2
  //#region src/builtins/doorbell/trigger-engine.ts
3
3
  /**
4
4
  * Pure trigger logic for the virtual-doorbell builtin — edge detection over
@@ -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-iAwSA2_f.js");
6
+ const require_dist = require("../../dist-Cri60Jd0.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  //#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
9
9
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
@@ -1,4 +1,4 @@
1
- import { X as logDestinationCapability, yt as BaseAddon } from "../../dist-Drb54SZD.mjs";
1
+ import { X as logDestinationCapability, yt as BaseAddon } from "../../dist-BpzbLPel.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  //#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
4
4
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-iAwSA2_f.js");
3
+ const require_dist = require("../../dist-Cri60Jd0.js");
4
4
  //#region src/builtins/liveness-monitor/liveness-checks.ts
5
5
  var NO_DEVICES = "liveness:no-devices";
6
6
  var ALL_OFFLINE = "liveness:all-devices-offline";
@@ -1,4 +1,4 @@
1
- import { Gt as EventCategory, Mt as createEvent, vt as errMsg, yt as BaseAddon } from "../../dist-Drb54SZD.mjs";
1
+ import { Gt as EventCategory, Mt as createEvent, vt as errMsg, yt as BaseAddon } from "../../dist-BpzbLPel.mjs";
2
2
  //#region src/builtins/liveness-monitor/liveness-checks.ts
3
3
  var NO_DEVICES = "liveness:no-devices";
4
4
  var ALL_OFFLINE = "liveness:all-devices-offline";
@@ -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-iAwSA2_f.js");
6
+ const require_dist = require("../../dist-Cri60Jd0.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  node_crypto = require_chunk.__toESM(node_crypto);
9
9
  let crypto$1 = require("crypto");
@@ -1,5 +1,5 @@
1
1
  import { i as __require, o as __toESM, t as __commonJSMin } from "../../chunk-CNf5ZN-e.mjs";
2
- import { S as authProviderCapability, _ as UserRecordSchema, ht as userManagementCapability, m as ScopedTokenSchema, r as ApiKeyRecordSchema, yt as BaseAddon } from "../../dist-Drb54SZD.mjs";
2
+ import { S as authProviderCapability, _ as UserRecordSchema, ht as userManagementCapability, m as ScopedTokenSchema, r as ApiKeyRecordSchema, yt as BaseAddon } from "../../dist-BpzbLPel.mjs";
3
3
  import * as crypto$2 from "node:crypto";
4
4
  import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
5
5
  import nodeCrypto from "crypto";
@@ -0,0 +1,15 @@
1
+ export type ParseLocalHostnameResult = {
2
+ readonly ok: true;
3
+ readonly value: string | null;
4
+ } | {
5
+ readonly ok: false;
6
+ readonly error: string;
7
+ };
8
+ /**
9
+ * Parse the operator-typed local hostname.
10
+ *
11
+ * Empty / whitespace → `null` (advertise the raw LAN IP). A DNS name is
12
+ * lowercased. A URL, port suffix, path, or bare IP is rejected so it cannot
13
+ * leak into a SAN or a candidate URL.
14
+ */
15
+ export declare function parseLocalHostname(raw: string): ParseLocalHostnameResult;
@@ -37,6 +37,12 @@ interface LocalNetworkConfig {
37
37
  * different address after a reboot.
38
38
  */
39
39
  readonly notificationBaseUrl: string;
40
+ /** LAN HTTP listener. Default true — native apps skip the private cert. */
41
+ readonly httpEnabled: boolean;
42
+ /** Dedicated HTTP port. Must not equal the HTTPS listen port. */
43
+ readonly httpPort: number;
44
+ /** Operator-declared DNS name advertised instead of the raw LAN IP. Empty = none. */
45
+ readonly localHostname: string;
40
46
  }
41
47
  export declare class LocalNetworkAddon extends BaseAddon<LocalNetworkConfig> {
42
48
  private pollTimer;
@@ -61,6 +67,8 @@ export declare class LocalNetworkAddon extends BaseAddon<LocalNetworkConfig> {
61
67
  */
62
68
  private resolveNotificationEndpoint;
63
69
  protected onInitialize(): Promise<ProviderRegistration[]>;
70
+ protected globalSettingsSchema(): import('@camstack/types').ConfigUISchema;
71
+ protected onConfigChanged(): Promise<void>;
64
72
  protected onShutdown(): Promise<void>;
65
73
  /**
66
74
  * Other hub addons (e.g. cloudflare-tunnel) signal the active public
@@ -86,6 +94,15 @@ export declare class LocalNetworkAddon extends BaseAddon<LocalNetworkConfig> {
86
94
  * not joined simply contributes nothing.
87
95
  */
88
96
  private reconcileMeshEndpoint;
97
+ private resolvedHttpPort;
98
+ private tlsDataDir;
99
+ private snapshotTls;
100
+ /**
101
+ * Bind or unbind the LAN HTTP sibling from the addon's own settings, and
102
+ * keep the leaf SAN in sync with `localHostname`. This is the settings
103
+ * side-effect path — there is no second persist API.
104
+ */
105
+ private applyLanAccessFromSettings;
89
106
  /**
90
107
  * The port this hub is actually listening on, plus the provenance of the
91
108
  * number. The boot loader merges the `CAMSTACK_PORT` env override into the
@@ -152,14 +169,21 @@ export declare function pickPreferred(interfaces: readonly LocalInterface[]): Lo
152
169
  * but above the IPv6 / loopback fallbacks — giving remote, mesh-joined
153
170
  * clients a working candidate when no public tunnel is up.
154
171
  *
155
- * `scheme` controls LAN + loopback URLs. Browsers running over HTTPS
156
- * block `http://` candidates as mixed content, so callers loaded over
157
- * HTTPS should pass `scheme: 'https'` even when probing a LAN IP — the
158
- * hub's cert manager already issues a SAN-multi cert covering local
159
- * interfaces. The public tunnel + mesh endpoint always emit `https://`
160
- * (the tunnel edge / the hub itself terminates TLS).
172
+ * `scheme` controls loopback URLs and, when LAN HTTP is off, LAN URLs.
173
+ * When {@link LanHttpAdvertisement} is enabled and listening, LAN (and
174
+ * the local-hostname alias) switch to `http://…:httpPort` so a native
175
+ * client can skip the private certificate. Public tunnel + mesh always
176
+ * emit `https://`. Loopback stays on `scheme`/`port` it is not a LAN
177
+ * candidate.
161
178
  */
162
- export declare function buildEndpoints(interfaces: readonly LocalInterface[], port: number, includeLoopback: boolean, ipv4Only: boolean, publicHostname: string, scheme?: 'http' | 'https', meshEndpoint?: MeshEndpointCache | null): ConnectionEndpoint[];
179
+ export interface LanHttpAdvertisement {
180
+ readonly enabled: boolean;
181
+ readonly listening: boolean;
182
+ readonly httpPort: number;
183
+ readonly httpsPort: number;
184
+ readonly localHostname: string | null;
185
+ }
186
+ export declare function buildEndpoints(interfaces: readonly LocalInterface[], port: number, includeLoopback: boolean, ipv4Only: boolean, publicHostname: string, scheme?: 'http' | 'https', meshEndpoint?: MeshEndpointCache | null, lanHttp?: LanHttpAdvertisement | null): ConnectionEndpoint[];
163
187
  /**
164
188
  * First-boot heuristic: which addresses should the allowlist start
165
189
  * with? Includes LAN + Wi-Fi IPv4 addresses + plausible IPv6:
@@ -1,8 +1,46 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-iAwSA2_f.js");
3
+ const require_dist = require("../../dist-Cri60Jd0.js");
4
+ const require_lan_http_bind = require("../../lan-http-bind-CZ4EBeD_.js");
5
+ let node_fs_promises = require("node:fs/promises");
6
+ let node_path = require("node:path");
4
7
  let node_os = require("node:os");
5
8
  node_os = require_chunk.__toESM(node_os);
9
+ let node_net = require("node:net");
10
+ //#region src/builtins/local-network/local-hostname.ts
11
+ var DNS_NAME = /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])$/;
12
+ /**
13
+ * Parse the operator-typed local hostname.
14
+ *
15
+ * Empty / whitespace → `null` (advertise the raw LAN IP). A DNS name is
16
+ * lowercased. A URL, port suffix, path, or bare IP is rejected so it cannot
17
+ * leak into a SAN or a candidate URL.
18
+ */
19
+ function parseLocalHostname(raw) {
20
+ const trimmed = raw.trim();
21
+ if (trimmed === "") return {
22
+ ok: true,
23
+ value: null
24
+ };
25
+ if (/[\s/:?#]/.test(trimmed) || trimmed.includes("\\")) return {
26
+ ok: false,
27
+ error: "Enter a DNS name such as camstack.lan, without a scheme or port."
28
+ };
29
+ const lowered = trimmed.toLowerCase();
30
+ if ((0, node_net.isIP)(lowered) !== 0) return {
31
+ ok: false,
32
+ error: "Use the address allowlist for IPs — this field is a DNS name."
33
+ };
34
+ if (!DNS_NAME.test(lowered)) return {
35
+ ok: false,
36
+ error: "Enter a DNS name such as camstack.lan, without a scheme or port."
37
+ };
38
+ return {
39
+ ok: true,
40
+ value: lowered
41
+ };
42
+ }
43
+ //#endregion
6
44
  //#region src/builtins/local-network/local-network.addon.ts
7
45
  /**
8
46
  * local-network — hub-only system cap implementation.
@@ -51,7 +89,10 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
51
89
  super({
52
90
  allowedAddresses: [],
53
91
  bootSeeded: false,
54
- notificationBaseUrl: ""
92
+ notificationBaseUrl: "",
93
+ httpEnabled: true,
94
+ httpPort: require_lan_http_bind.DEFAULT_LAN_HTTP_PORT,
95
+ localHostname: ""
55
96
  });
56
97
  }
57
98
  /**
@@ -97,8 +138,15 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
97
138
  source: localPort.source
98
139
  } });
99
140
  await this.reconcileMeshEndpoint();
141
+ const httpState = require_lan_http_bind.readLanHttpState();
100
142
  return {
101
- endpoints: buildEndpoints(interfaces, localPort.port, includeLoopback, ipv4Only, this.publicHostname, scheme, this.meshEndpoint),
143
+ endpoints: buildEndpoints(interfaces, localPort.port, includeLoopback, ipv4Only, this.publicHostname, scheme, this.meshEndpoint, {
144
+ enabled: this.config.httpEnabled !== false,
145
+ listening: httpState.listening,
146
+ httpPort: this.resolvedHttpPort(),
147
+ httpsPort: localPort.port,
148
+ localHostname: this.config.localHostname || null
149
+ }),
102
150
  localPort
103
151
  };
104
152
  },
@@ -133,6 +181,33 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
133
181
  dropped: addresses.length - cleaned.length
134
182
  } });
135
183
  return { success: true };
184
+ },
185
+ getTlsStatus: async () => this.snapshotTls(false),
186
+ regenerateCertificate: async (_input) => {
187
+ if (require_lan_http_bind.readTlsMode(this.tlsDataDir()) === "uploaded") throw new Error("Cannot regenerate an uploaded certificate — revert to generated first.");
188
+ const extraSans = [...require_lan_http_bind.readExtraSans(this.tlsDataDir())];
189
+ await require_lan_http_bind.reissueTlsLeaf(this.tlsDataDir(), { extraSans });
190
+ return this.snapshotTls(true);
191
+ },
192
+ uploadCertificate: async ({ certPem, keyPem, caPem }) => {
193
+ const valid = require_lan_http_bind.validateUploadedTls(certPem, keyPem);
194
+ if (!valid.ok) throw new Error(valid.error);
195
+ const dir = (0, node_path.join)(this.tlsDataDir(), "tls");
196
+ await (0, node_fs_promises.mkdir)(dir, { recursive: true });
197
+ await (0, node_fs_promises.writeFile)((0, node_path.join)(dir, "camstack.crt"), certPem.endsWith("\n") ? certPem : `${certPem}\n`);
198
+ await (0, node_fs_promises.writeFile)((0, node_path.join)(dir, "camstack.key"), keyPem.endsWith("\n") ? keyPem : `${keyPem}\n`, { mode: 384 });
199
+ if (caPem && caPem.trim() !== "") await (0, node_fs_promises.writeFile)((0, node_path.join)(dir, "camstack-ca.crt"), caPem.endsWith("\n") ? caPem : `${caPem}\n`);
200
+ require_lan_http_bind.writeTlsMode(this.tlsDataDir(), "uploaded");
201
+ return this.snapshotTls(true);
202
+ },
203
+ downloadCa: async () => {
204
+ return { pem: this.snapshotTls(false).caCertPem ?? "" };
205
+ },
206
+ revertToGeneratedCertificate: async () => {
207
+ require_lan_http_bind.writeTlsMode(this.tlsDataDir(), "generated");
208
+ const extraSans = [...require_lan_http_bind.readExtraSans(this.tlsDataDir())];
209
+ await require_lan_http_bind.ensureTlsCert(this.tlsDataDir(), { extraSans });
210
+ return this.snapshotTls(true);
136
211
  }
137
212
  };
138
213
  this.lastSnapshotKey = snapshotKey(this.enumerate());
@@ -156,11 +231,58 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
156
231
  } : null);
157
232
  });
158
233
  this.ctx.logger.info("local-network initialized", { meta: { interfaceCount: this.enumerate().length } });
234
+ try {
235
+ await this.applyLanAccessFromSettings();
236
+ } catch (err) {
237
+ this.ctx.logger.warn("local-network: LAN HTTP not applied at boot", { meta: { error: err instanceof Error ? err.message : String(err) } });
238
+ }
159
239
  return [{
160
240
  capability: require_dist.localNetworkCapability,
161
241
  provider
162
242
  }];
163
243
  }
244
+ globalSettingsSchema() {
245
+ return this.schema({ sections: [{
246
+ id: "local-access",
247
+ title: "LAN HTTP",
248
+ description: "Native apps on this Wi-Fi use http:// and skip the private certificate. The tunnel and this admin UI stay on HTTPS. Turn off once a publicly-trusted certificate covers the LAN name.",
249
+ columns: 1,
250
+ fields: [
251
+ this.field({
252
+ type: "boolean",
253
+ key: "httpEnabled",
254
+ label: "Enable HTTP on the LAN",
255
+ style: "switch",
256
+ default: true
257
+ }),
258
+ this.field({
259
+ type: "number",
260
+ key: "httpPort",
261
+ label: "HTTP port",
262
+ description: "Dedicated listener. Must not equal the HTTPS port (4443 by default).",
263
+ min: 1,
264
+ max: 65535,
265
+ step: 1,
266
+ default: require_lan_http_bind.DEFAULT_LAN_HTTP_PORT,
267
+ showWhen: {
268
+ field: "httpEnabled",
269
+ equals: true
270
+ }
271
+ }),
272
+ this.field({
273
+ type: "text",
274
+ key: "localHostname",
275
+ label: "Local hostname",
276
+ description: "Advertised to the viewer instead of the raw LAN IP, and added to the certificate SAN. Requires this name to resolve on the LAN (router DNS, mDNS, or a hosts file). Leave empty to keep using the IP.",
277
+ maxLength: 253,
278
+ placeholder: "camstack.lan"
279
+ })
280
+ ]
281
+ }] });
282
+ }
283
+ async onConfigChanged() {
284
+ await this.applyLanAccessFromSettings();
285
+ }
164
286
  async onShutdown() {
165
287
  if (this.pollTimer) {
166
288
  clearInterval(this.pollTimer);
@@ -229,6 +351,42 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
229
351
  }
230
352
  }
231
353
  }
354
+ resolvedHttpPort() {
355
+ const port = this.config.httpPort;
356
+ return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : require_lan_http_bind.DEFAULT_LAN_HTTP_PORT;
357
+ }
358
+ tlsDataDir() {
359
+ return this.ctx.nodeDataDir ?? this.ctx.dataDir;
360
+ }
361
+ snapshotTls(restartRequired) {
362
+ const status = require_lan_http_bind.readTlsAccessStatus(this.tlsDataDir(), restartRequired);
363
+ return {
364
+ ...status,
365
+ sans: [...status.sans]
366
+ };
367
+ }
368
+ /**
369
+ * Bind or unbind the LAN HTTP sibling from the addon's own settings, and
370
+ * keep the leaf SAN in sync with `localHostname`. This is the settings
371
+ * side-effect path — there is no second persist API.
372
+ */
373
+ async applyLanAccessFromSettings() {
374
+ const parsed = parseLocalHostname(this.config.localHostname ?? "");
375
+ if (!parsed.ok) throw new Error(parsed.error);
376
+ const hostname = parsed.value ?? "";
377
+ const httpsPort = (await this.resolveAdvertisedListenPort(null)).port;
378
+ const httpPort = this.resolvedHttpPort();
379
+ if (this.config.httpEnabled !== false && httpPort === httpsPort) throw new Error(`HTTP port ${httpPort} cannot equal the HTTPS listen port`);
380
+ await require_lan_http_bind.applyLanHttp({
381
+ enabled: this.config.httpEnabled !== false,
382
+ port: httpPort,
383
+ host: "0.0.0.0"
384
+ });
385
+ if (hostname !== "") {
386
+ require_lan_http_bind.writeExtraSans(this.tlsDataDir(), [hostname]);
387
+ if (require_lan_http_bind.readTlsMode(this.tlsDataDir()) === "generated") await require_lan_http_bind.ensureTlsCert(this.tlsDataDir(), { extraSans: [hostname] });
388
+ } else require_lan_http_bind.writeExtraSans(this.tlsDataDir(), []);
389
+ }
232
390
  /**
233
391
  * The port this hub is actually listening on, plus the provenance of the
234
392
  * number. The boot loader merges the `CAMSTACK_PORT` env override into the
@@ -402,31 +560,13 @@ function pickPreferred(interfaces) {
402
560
  return prefixLen(b.netmask) - prefixLen(a.netmask);
403
561
  })[0] ?? null;
404
562
  }
405
- /**
406
- * Build the ordered candidate URL list. Priority schema:
407
- * 0 — preferred LAN IPv4
408
- * 10+ — other LAN IPv4
409
- * 100 — public tunnel hostname (always HTTPS)
410
- * 150 — mesh endpoint (Tailscale MagicDNS / 100.x, always HTTPS)
411
- * 200+ — LAN IPv6
412
- * 1000 — loopback (last resort)
413
- *
414
- * The mesh endpoint ranks just below a true public funnel (a Funnel is
415
- * reachable by ANY client; the mesh needs the peer on the same tailnet)
416
- * but above the IPv6 / loopback fallbacks — giving remote, mesh-joined
417
- * clients a working candidate when no public tunnel is up.
418
- *
419
- * `scheme` controls LAN + loopback URLs. Browsers running over HTTPS
420
- * block `http://` candidates as mixed content, so callers loaded over
421
- * HTTPS should pass `scheme: 'https'` even when probing a LAN IP — the
422
- * hub's cert manager already issues a SAN-multi cert covering local
423
- * interfaces. The public tunnel + mesh endpoint always emit `https://`
424
- * (the tunnel edge / the hub itself terminates TLS).
425
- */
426
- function buildEndpoints(interfaces, port, includeLoopback, ipv4Only, publicHostname, scheme = "http", meshEndpoint = null) {
563
+ function buildEndpoints(interfaces, port, includeLoopback, ipv4Only, publicHostname, scheme = "http", meshEndpoint = null, lanHttp = null) {
427
564
  const out = [];
428
565
  let priority = 0;
429
566
  const preferred = pickPreferred(interfaces);
567
+ const httpLan = lanHttp?.enabled === true && lanHttp.listening === true;
568
+ const lanScheme = httpLan ? "http" : scheme;
569
+ const lanPort = httpLan ? lanHttp.httpPort : port;
430
570
  const emit = (iface, kind, label, baseUrl, pri) => {
431
571
  out.push({
432
572
  label,
@@ -438,13 +578,22 @@ function buildEndpoints(interfaces, port, includeLoopback, ipv4Only, publicHostn
438
578
  priority: pri
439
579
  });
440
580
  };
441
- if (preferred) emit(preferred, "lan-ipv4", `${formatKind(preferred.kind)} — ${preferred.name}`, `${scheme}://${preferred.address}:${port}`, priority++);
581
+ if (httpLan && lanHttp.localHostname) out.push({
582
+ label: "Local name",
583
+ baseUrl: `${lanScheme}://${lanHttp.localHostname}:${lanPort}`,
584
+ kind: "lan-ipv4",
585
+ interfaceKind: "lan",
586
+ plausible: true,
587
+ plausibleReason: "",
588
+ priority: priority++
589
+ });
590
+ if (preferred) emit(preferred, "lan-ipv4", `${formatKind(preferred.kind)} — ${preferred.name}`, `${lanScheme}://${preferred.address}:${lanPort}`, priority++);
442
591
  for (const iface of interfaces) {
443
592
  if (iface.internal || iface.family !== "IPv4") continue;
444
593
  if (iface.kind === "loopback") continue;
445
594
  if (preferred && iface.name === preferred.name && iface.address === preferred.address) continue;
446
595
  if (iface.address.startsWith("169.254.")) continue;
447
- emit(iface, "lan-ipv4", `${formatKind(iface.kind)} — ${iface.name}`, `${scheme}://${iface.address}:${port}`, 10 + priority++);
596
+ emit(iface, "lan-ipv4", `${formatKind(iface.kind)} — ${iface.name}`, `${lanScheme}://${iface.address}:${lanPort}`, 10 + priority++);
448
597
  }
449
598
  if (publicHostname) out.push({
450
599
  label: "Public tunnel",
@@ -470,7 +619,7 @@ function buildEndpoints(interfaces, port, includeLoopback, ipv4Only, publicHostn
470
619
  if (iface.internal || iface.family !== "IPv6") continue;
471
620
  if (iface.kind === "loopback") continue;
472
621
  if (iface.address.startsWith("fe80:")) continue;
473
- emit(iface, "lan-ipv6", `${formatKind(iface.kind)} — ${iface.name} (IPv6)`, `${scheme}://[${iface.address}]:${port}`, v6prio++);
622
+ emit(iface, "lan-ipv6", `${formatKind(iface.kind)} — ${iface.name} (IPv6)`, `${lanScheme}://[${iface.address}]:${lanPort}`, v6prio++);
474
623
  }
475
624
  }
476
625
  if (includeLoopback) out.push({