@camstack/core 0.1.16 → 0.1.17

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.
@@ -34,7 +34,15 @@ export declare class LocalNetworkAddon extends BaseAddon<LocalNetworkConfig> {
34
34
  * kind classification + preferred flag. Pure — takes the raw result so
35
35
  * tests can feed synthetic data.
36
36
  */
37
- export declare function enumerateOsInterfaces(ifaces: NodeJS.Dict<NodeJS.NetworkInterfaceInfo[]>): readonly LocalInterface[];
37
+ interface OsNetworkInterfaceInfo {
38
+ readonly family: string;
39
+ readonly address: string;
40
+ readonly cidr?: string | null;
41
+ readonly netmask: string;
42
+ readonly internal: boolean;
43
+ readonly mac: string;
44
+ }
45
+ export declare function enumerateOsInterfaces(ifaces: NodeJS.Dict<readonly OsNetworkInterfaceInfo[]>): readonly LocalInterface[];
38
46
  /**
39
47
  * Filter the interface list by the operator's allowlist. Empty
40
48
  * allowlist = no-op (every interface passes); otherwise only entries
@@ -1 +1 @@
1
- {"version":3,"file":"local-network.addon.d.ts","sourceRoot":"","sources":["../../../src/builtins/local-network/local-network.addon.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EACV,oBAAoB,EAEpB,cAAc,EACd,kBAAkB,EACnB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,SAAS,EAAyC,MAAM,iBAAiB,CAAA;AAclF,UAAU,kBAAkB;IAC1B;;2CAEuC;IACvC,QAAQ,CAAC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAA;IAC5C;;;iEAG6D;IAC7D,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAA;CAC7B;AAED,qBAAa,iBAAkB,SAAQ,SAAS,CAAC,kBAAkB,CAAC;IAClE,OAAO,CAAC,SAAS,CAA8B;IAC/C,OAAO,CAAC,eAAe,CAAK;IAC5B;uCACmC;IACnC,OAAO,CAAC,cAAc,CAAK;;cAMX,YAAY,IAAI,OAAO,CAAC,oBAAoB,EAAE,CAAC;cAgG/C,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAO3C;;;;;OAKG;IACH,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAUzC,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,aAAa;CAkBtB;AAID;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,oBAAoB,EAAE,CAAC,GACjD,SAAS,cAAc,EAAE,CAqD3B;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,UAAU,EAAE,SAAS,cAAc,EAAE,EACrC,OAAO,EAAE,SAAS,MAAM,EAAE,GACzB,SAAS,cAAc,EAAE,CAI3B;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,SAAS,cAAc,EAAE,GAAG,cAAc,GAAG,IAAI,CAoB1F;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAC5B,UAAU,EAAE,SAAS,cAAc,EAAE,EACrC,IAAI,EAAE,MAAM,EACZ,eAAe,EAAE,OAAO,EACxB,QAAQ,EAAE,OAAO,EACjB,cAAc,EAAE,MAAM,EACtB,MAAM,GAAE,MAAM,GAAG,OAAgB,GAChC,kBAAkB,EAAE,CAwFtB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,SAAS,cAAc,EAAE,GAAG,MAAM,EAAE,CAQjF;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE;IACzC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC,MAAM,CAAC,CAAA;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAA;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;CAC3B,GAAG,MAAM,CAcT;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAgBrF;AAED,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,OAAO,GAChB,cAAc,CAAC,MAAM,CAAC,CAaxB;AAED,uEAAuE;AACvE,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAyBjD"}
1
+ {"version":3,"file":"local-network.addon.d.ts","sourceRoot":"","sources":["../../../src/builtins/local-network/local-network.addon.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EACV,oBAAoB,EAEpB,cAAc,EACd,kBAAkB,EACnB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,SAAS,EAAyC,MAAM,iBAAiB,CAAA;AAclF,UAAU,kBAAkB;IAC1B;;2CAEuC;IACvC,QAAQ,CAAC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAA;IAC5C;;;iEAG6D;IAC7D,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAA;CAC7B;AAED,qBAAa,iBAAkB,SAAQ,SAAS,CAAC,kBAAkB,CAAC;IAClE,OAAO,CAAC,SAAS,CAA8B;IAC/C,OAAO,CAAC,eAAe,CAAK;IAC5B;uCACmC;IACnC,OAAO,CAAC,cAAc,CAAK;;cAMX,YAAY,IAAI,OAAO,CAAC,oBAAoB,EAAE,CAAC;cAgG/C,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAO3C;;;;;OAKG;IACH,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAUzC,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,aAAa;CAkBtB;AAID;;;;GAIG;AACH,UAAU,sBAAsB;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CACrB;AAED,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,SAAS,sBAAsB,EAAE,CAAC,GACrD,SAAS,cAAc,EAAE,CAqD3B;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,UAAU,EAAE,SAAS,cAAc,EAAE,EACrC,OAAO,EAAE,SAAS,MAAM,EAAE,GACzB,SAAS,cAAc,EAAE,CAI3B;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,SAAS,cAAc,EAAE,GAAG,cAAc,GAAG,IAAI,CAoB1F;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAC5B,UAAU,EAAE,SAAS,cAAc,EAAE,EACrC,IAAI,EAAE,MAAM,EACZ,eAAe,EAAE,OAAO,EACxB,QAAQ,EAAE,OAAO,EACjB,cAAc,EAAE,MAAM,EACtB,MAAM,GAAE,MAAM,GAAG,OAAgB,GAChC,kBAAkB,EAAE,CAwFtB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,SAAS,cAAc,EAAE,GAAG,MAAM,EAAE,CAQjF;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE;IACzC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC,MAAM,CAAC,CAAA;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAA;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;CAC3B,GAAG,MAAM,CAcT;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAgBrF;AAED,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,OAAO,GAChB,cAAc,CAAC,MAAM,CAAC,CAaxB;AAED,uEAAuE;AACvE,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAyBjD"}
@@ -133,11 +133,6 @@ var LocalNetworkAddon = class extends _camstack_types.BaseAddon {
133
133
  });
134
134
  }
135
135
  };
136
- /**
137
- * Lift `os.networkInterfaces()` into our `LocalInterface[]` shape with
138
- * kind classification + preferred flag. Pure — takes the raw result so
139
- * tests can feed synthetic data.
140
- */
141
136
  function enumerateOsInterfaces(ifaces) {
142
137
  const raw = [];
143
138
  for (const [name, addrs] of Object.entries(ifaces)) {
@@ -1 +1 @@
1
- {"version":3,"file":"local-network.addon.js","names":[],"sources":["../../../src/builtins/local-network/local-network.addon.ts"],"sourcesContent":["/**\n * local-network — hub-only system cap implementation.\n *\n * Wraps `os.networkInterfaces()` with:\n * - Coarse kind classification (lan/wifi/docker/vpn/loopback/other)\n * - Auto-select heuristic for the outbound interface\n * - Periodic poll (30s) + diff to emit `LocalNetworkChanged` events\n * so subscribers can react to DHCP renewals, VPN connect, etc.\n * - `getConnectionEndpoints()` — ranked URL list for SDK clients\n *\n * Does NOT poll the cloudflare-tunnel state directly; the public\n * tunnel hostname (if any) is provided by `network-access` consumers\n * via `NetworkTunnelStarted/Stopped` events on the bus, so the cap\n * stays free of cross-addon dependencies.\n */\nimport * as os from 'node:os'\n\nimport type {\n ProviderRegistration,\n ILocalNetworkProvider,\n LocalInterface,\n ConnectionEndpoint,\n} from '@camstack/types'\nimport { BaseAddon, localNetworkCapability, EventCategory } from '@camstack/types'\n\ninterface RawIface {\n readonly name: string\n readonly family: 'IPv4' | 'IPv6'\n readonly address: string\n readonly cidr: string\n readonly netmask: string\n readonly internal: boolean\n readonly mac: string\n}\n\nconst POLL_INTERVAL_MS = 30_000\n\ninterface LocalNetworkConfig {\n /** Empty = \"auto\" (every non-loopback / non-link-local address\n * participates). Non-empty restricts the candidate set to only\n * those operator-pinned addresses. */\n readonly allowedAddresses: readonly string[]\n /** Sentinel — `true` after the first-boot auto-seed runs. Lets the\n * addon distinguish \"fresh install, never touched\" (false → seed\n * with LAN/Wi-Fi addresses) from \"operator explicitly cleared the\n * allowlist\" (true + empty → respect operator's choice). */\n readonly bootSeeded: boolean\n}\n\nexport class LocalNetworkAddon extends BaseAddon<LocalNetworkConfig> {\n private pollTimer: NodeJS.Timeout | null = null\n private lastSnapshotKey = ''\n /** Optional public hostname tracked from `NetworkTunnelStarted`/\n * `Stopped` events on the bus. */\n private publicHostname = ''\n\n constructor() {\n super({ allowedAddresses: [], bootSeeded: false })\n }\n\n protected async onInitialize(): Promise<ProviderRegistration[]> {\n // First-boot auto-seed: on fresh installs, pre-populate the\n // allowlist with addresses from interfaces classified as `lan` or\n // `wifi`. Operators usually want those participating; docker/vpn\n // entries stay opt-in. Loopback is always implicit (no need to\n // include it explicitly).\n if (!this.config.bootSeeded) {\n const seed = autoSeedAllowlist(this.enumerate())\n await this.updateGlobalSettings({ allowedAddresses: seed, bootSeeded: true })\n this.ctx.logger.info('local-network: first-boot auto-seed', {\n meta: { addresses: seed },\n })\n }\n const provider: ILocalNetworkProvider = {\n list: async () => ({\n interfaces: this.enumerate(),\n probedAt: Date.now(),\n }),\n getPreferred: async () => pickPreferred(\n applyAllowlist(this.enumerate(), this.config.allowedAddresses),\n ),\n getConnectionEndpoints: async (input) => {\n const includeLoopback = input.includeLoopback ?? true\n const ipv4Only = input.ipv4Only ?? false\n const scheme = input.scheme ?? 'http'\n const allow = this.config.allowedAddresses\n const interfaces = applyAllowlist(this.enumerate(), allow)\n return {\n endpoints: buildEndpoints(interfaces, input.port, includeLoopback, ipv4Only, this.publicHostname, scheme),\n }\n },\n getAllowedAddresses: async () => ({ addresses: this.config.allowedAddresses }),\n resetAllowlistToBestMatch: async () => {\n const seed = autoSeedAllowlist(this.enumerate())\n await this.updateGlobalSettings({ allowedAddresses: seed, bootSeeded: true })\n this.ctx.logger.info('local-network: allowlist reset to auto-seed', {\n meta: { count: seed.length },\n })\n return { addresses: seed }\n },\n setAllowedAddresses: async ({ addresses }) => {\n // Validate against the current snapshot — drop anything that\n // doesn't actually exist on this host. Prevents an admin from\n // saving a typo'd address that would then silently filter out\n // every real candidate.\n const known = new Set(this.enumerate().map((i) => i.address))\n const cleaned: readonly string[] = [...new Set(addresses)].filter((a) => known.has(a))\n await this.updateGlobalSettings({ allowedAddresses: cleaned })\n this.ctx.logger.info('local-network: allowlist updated', {\n meta: { count: cleaned.length, dropped: addresses.length - cleaned.length },\n })\n return { success: true as const }\n },\n }\n\n // Seed the snapshot key so the first poll doesn't fire a false\n // change event on boot.\n this.lastSnapshotKey = snapshotKey(this.enumerate())\n\n this.pollTimer = setInterval(() => this.detectChanges(), POLL_INTERVAL_MS)\n this.pollTimer.unref?.()\n\n // Track the active public hostname by tailing the network-access\n // lifecycle events that every tunnel provider already emits. We\n // can't import cross-package so the parsing stays defensive.\n this.ctx.eventBus?.subscribe(\n { category: EventCategory.NetworkTunnelStarted },\n (event) => {\n const data = (event.data ?? {}) as { url?: unknown }\n if (typeof data.url === 'string') {\n try {\n const hostname = new URL(data.url).hostname\n // Ignore the pre-FQDN placeholders the tunnel emits before\n // cloudflared's stdout reports the real *.trycloudflare.com.\n if (hostname && !hostname.endsWith('.placeholder')\n && !hostname.startsWith('pending.')) {\n this.setPublicHostname(hostname)\n }\n } catch {\n // Malformed URL — leave the cached hostname as-is.\n }\n }\n },\n )\n this.ctx.eventBus?.subscribe(\n { category: EventCategory.NetworkTunnelStopped },\n () => this.setPublicHostname(''),\n )\n\n this.ctx.logger.info('local-network initialized', {\n meta: { interfaceCount: this.enumerate().length },\n })\n\n return [{ capability: localNetworkCapability, provider }]\n }\n\n protected async onShutdown(): Promise<void> {\n if (this.pollTimer) {\n clearInterval(this.pollTimer)\n this.pollTimer = null\n }\n }\n\n /**\n * Other hub addons (e.g. cloudflare-tunnel) signal the active public\n * FQDN by emitting `NetworkTunnelStarted` on the bus — handled in\n * `onInitialize`. This setter exists for tests + future direct\n * callers; cleared by passing an empty string.\n */\n setPublicHostname(hostname: string): void {\n if (this.publicHostname === hostname) return\n this.publicHostname = hostname\n this.ctx.logger.info('local-network: public hostname updated', {\n meta: { hostname: hostname || '(cleared)' },\n })\n }\n\n // ── Internals ──────────────────────────────────────────────────────\n\n private enumerate(): readonly LocalInterface[] {\n return enumerateOsInterfaces(os.networkInterfaces())\n }\n\n private detectChanges(): void {\n const interfaces = this.enumerate()\n const key = snapshotKey(interfaces)\n if (key === this.lastSnapshotKey) return\n\n this.ctx.logger.info('local-network: interface set changed', {\n meta: { count: interfaces.length, key },\n })\n this.lastSnapshotKey = key\n\n this.ctx.eventBus?.emit({\n id: `local-network-changed-${Date.now()}`,\n timestamp: new Date(),\n source: { type: 'core', id: 'local-network' },\n category: EventCategory.LocalNetworkChanged,\n data: { count: interfaces.length },\n })\n }\n}\n\n// ── Pure helpers (exported for tests + reusable by other consumers) ──\n\n/**\n * Lift `os.networkInterfaces()` into our `LocalInterface[]` shape with\n * kind classification + preferred flag. Pure — takes the raw result so\n * tests can feed synthetic data.\n */\nexport function enumerateOsInterfaces(\n ifaces: NodeJS.Dict<NodeJS.NetworkInterfaceInfo[]>,\n): readonly LocalInterface[] {\n const raw: RawIface[] = []\n for (const [name, addrs] of Object.entries(ifaces)) {\n if (!addrs) continue\n for (const a of addrs) {\n if (a.family !== 'IPv4' && a.family !== 'IPv6') continue\n raw.push({\n name,\n family: a.family,\n address: a.address,\n cidr: a.cidr ?? '',\n netmask: a.netmask,\n internal: a.internal,\n mac: a.mac,\n })\n }\n }\n\n const classified: LocalInterface[] = raw.map((r) => {\n const kind = classifyKind(r.name, r.address, r.internal)\n // \"Plausible\" = passes the RFC range gate AND comes from an\n // interface kind a client could realistically reach. Operators\n // can still pin a docker / vpn address — we just mark it visually\n // so the choice is intentional.\n const reachableKind = kind === 'lan' || kind === 'wifi'\n const plausible = !r.internal\n && reachableKind\n && isPlausibleAutoSeed(r.address, r.family)\n const plausibleReason = plausible\n ? ''\n : explainNonPlausible({ kind, family: r.family, address: r.address, internal: r.internal })\n return {\n name: r.name,\n family: r.family,\n address: r.address,\n cidr: r.cidr,\n netmask: r.netmask,\n internal: r.internal,\n mac: r.mac,\n kind,\n preferred: false,\n plausible,\n plausibleReason,\n }\n })\n\n const preferred = pickPreferred(classified)\n return classified.map((iface) => ({\n ...iface,\n preferred: preferred !== null\n && iface.name === preferred.name\n && iface.address === preferred.address,\n }))\n}\n\n/**\n * Filter the interface list by the operator's allowlist. Empty\n * allowlist = no-op (every interface passes); otherwise only entries\n * whose `address` matches an allowlist entry remain. Loopback always\n * survives so the SDK keeps `127.0.0.1` as the last-resort fallback.\n */\nexport function applyAllowlist(\n interfaces: readonly LocalInterface[],\n allowed: readonly string[],\n): readonly LocalInterface[] {\n if (allowed.length === 0) return interfaces\n const set = new Set(allowed)\n return interfaces.filter((i) => i.kind === 'loopback' || set.has(i.address))\n}\n\n/**\n * Rank interfaces and pick the auto-selected outbound one. See the\n * cap's `getPreferred` doc for the heuristic.\n */\nexport function pickPreferred(interfaces: readonly LocalInterface[]): LocalInterface | null {\n const candidates = interfaces.filter((i) =>\n !i.internal\n && i.family === 'IPv4'\n && !i.address.startsWith('169.254.') // RFC 3927 link-local\n && i.kind !== 'loopback',\n )\n if (candidates.length === 0) return null\n\n const kindRank: Record<LocalInterface['kind'], number> = {\n lan: 1, wifi: 2, vpn: 3, docker: 4, other: 5, loopback: 99,\n }\n const sorted = [...candidates].sort((a, b) => {\n const ra = kindRank[a.kind]\n const rb = kindRank[b.kind]\n if (ra !== rb) return ra - rb\n // Tie-break on netmask length — `/24` beats `/16`.\n return prefixLen(b.netmask) - prefixLen(a.netmask)\n })\n return sorted[0] ?? null\n}\n\n/**\n * Build the ordered candidate URL list. Priority schema:\n * 0 — preferred LAN IPv4\n * 10+ — other LAN IPv4\n * 100 — public tunnel hostname (always HTTPS)\n * 200+ — LAN IPv6\n * 1000 — loopback (last resort)\n *\n * `scheme` controls LAN + loopback URLs. Browsers running over HTTPS\n * block `http://` candidates as mixed content, so callers loaded over\n * HTTPS should pass `scheme: 'https'` even when probing a LAN IP — the\n * hub's cert manager already issues a SAN-multi cert covering local\n * interfaces. The public tunnel always emits `https://` (Cloudflare\n * terminates TLS for us).\n */\nexport function buildEndpoints(\n interfaces: readonly LocalInterface[],\n port: number,\n includeLoopback: boolean,\n ipv4Only: boolean,\n publicHostname: string,\n scheme: 'http' | 'https' = 'http',\n): ConnectionEndpoint[] {\n const out: ConnectionEndpoint[] = []\n let priority = 0\n const preferred = pickPreferred(interfaces)\n\n const emit = (\n iface: LocalInterface,\n kind: ConnectionEndpoint['kind'],\n label: string,\n baseUrl: string,\n pri: number,\n ): void => {\n out.push({\n label,\n baseUrl,\n kind,\n interfaceKind: iface.kind,\n plausible: iface.plausible,\n plausibleReason: iface.plausibleReason,\n priority: pri,\n })\n }\n\n if (preferred) {\n emit(\n preferred,\n 'lan-ipv4',\n `${formatKind(preferred.kind)} — ${preferred.name}`,\n `${scheme}://${preferred.address}:${port}`,\n priority++,\n )\n }\n\n for (const iface of interfaces) {\n if (iface.internal || iface.family !== 'IPv4') continue\n if (iface.kind === 'loopback') continue\n if (preferred && iface.name === preferred.name && iface.address === preferred.address) continue\n if (iface.address.startsWith('169.254.')) continue\n emit(\n iface,\n 'lan-ipv4',\n `${formatKind(iface.kind)} — ${iface.name}`,\n `${scheme}://${iface.address}:${port}`,\n 10 + priority++,\n )\n }\n\n if (publicHostname) {\n out.push({\n label: 'Public tunnel',\n baseUrl: `https://${publicHostname}`,\n kind: 'public',\n interfaceKind: 'public',\n plausible: true,\n plausibleReason: '',\n priority: 100,\n })\n }\n\n if (!ipv4Only) {\n let v6prio = 200\n for (const iface of interfaces) {\n if (iface.internal || iface.family !== 'IPv6') continue\n if (iface.kind === 'loopback') continue\n if (iface.address.startsWith('fe80:')) continue // link-local\n emit(\n iface,\n 'lan-ipv6',\n `${formatKind(iface.kind)} — ${iface.name} (IPv6)`,\n `${scheme}://[${iface.address}]:${port}`,\n v6prio++,\n )\n }\n }\n\n if (includeLoopback) {\n out.push({\n label: 'Loopback',\n baseUrl: `${scheme}://127.0.0.1:${port}`,\n kind: 'loopback',\n interfaceKind: 'loopback',\n plausible: false,\n plausibleReason: 'Loopback — last-resort fallback when no other endpoint responds.',\n priority: 1000,\n })\n }\n\n return out.sort((a, b) => a.priority - b.priority)\n}\n\n/**\n * First-boot heuristic: which addresses should the allowlist start\n * with? Includes LAN + Wi-Fi IPv4 addresses + plausible IPv6:\n *\n * - **IPv4**: skip link-local (`169.254.*`), keep the rest.\n * - **IPv6**: skip link-local (`fe80::*`), unspecified, and\n * multicast. Keep ULAs (`fc00::/7` → `fc??:` / `fd??:`) and Global\n * Unicast addresses (`2000::/3` → `2???`/`3???`). Privacy-extension\n * temporary addresses get included by default; the operator can\n * prune them from the Network Addresses tab if the rotating IPs\n * become a nuisance.\n *\n * Skips docker/vpn/loopback/other entirely — those stay opt-in.\n */\nexport function autoSeedAllowlist(interfaces: readonly LocalInterface[]): string[] {\n return [...new Set(\n interfaces\n .filter((i) => !i.internal\n && (i.kind === 'lan' || i.kind === 'wifi')\n && isPlausibleAutoSeed(i.address, i.family))\n .map((i) => i.address),\n )]\n}\n\n/**\n * Per-interface tooltip text surfaced on the \"Unlikely usable\" badge.\n * Server-side so the UI doesn't re-derive the rationale (single source\n * of truth). Returns `''` for plausible entries; the addon overlays\n * this on the `LocalInterface.plausibleReason` field.\n */\nexport function explainNonPlausible(input: {\n readonly kind: LocalInterface['kind']\n readonly family: 'IPv4' | 'IPv6'\n readonly address: string\n readonly internal: boolean\n}): string {\n if (input.internal) return 'Internal interface (loopback) — not reachable from clients.'\n if (input.kind === 'docker') return 'Docker bridge — only reachable from inside the container network.'\n if (input.kind === 'vpn') return 'VPN tunnel — only reachable while the VPN is connected.'\n if (input.kind === 'other') return 'Unrecognised interface kind — verify reachability before pinning.'\n if (input.family === 'IPv6') {\n const a = input.address.toLowerCase()\n if (a.startsWith('fe80:')) return 'IPv6 link-local — only reachable on the same link, not routed.'\n if (a.startsWith('ff')) return 'IPv6 multicast — not a unicast address.'\n if (a === '::' || a === '::1') return 'IPv6 loopback / unspecified — not a public address.'\n return 'IPv6 address outside the ULA / GUA ranges — verify before pinning.'\n }\n if (input.address.startsWith('169.254.')) return 'IPv4 link-local (RFC 3927) — only valid when DHCP fails.'\n return 'Address looks unusual for client traffic — verify before pinning.'\n}\n\n/**\n * Per-address gate used by `autoSeedAllowlist`. Exposed for tests so\n * we can pin every classification rule without standing up the addon.\n */\nexport function isPlausibleAutoSeed(address: string, family: 'IPv4' | 'IPv6'): boolean {\n if (family === 'IPv4') {\n if (address.startsWith('169.254.')) return false // RFC 3927 link-local\n return true\n }\n // IPv6 — normalise to lower-case once.\n const a = address.toLowerCase()\n if (a === '::' || a === '::1') return false\n if (a.startsWith('fe80:')) return false // link-local\n if (a.startsWith('ff')) return false // multicast (ff00::/8)\n // ULA (fc00::/7 → first hex group starts with `fc` or `fd`).\n if (/^f[cd][0-9a-f]{0,2}:/.test(a)) return true\n // GUA (2000::/3 → first nibble 2 or 3).\n if (/^[23][0-9a-f]{0,3}:/.test(a)) return true\n // Anything else (deprecated / experimental ranges) — leave opt-in.\n return false\n}\n\nexport function classifyKind(\n name: string,\n address: string,\n internal: boolean,\n): LocalInterface['kind'] {\n if (internal || name === 'lo' || name.startsWith('lo')) return 'loopback'\n const n = name.toLowerCase()\n if (n.startsWith('docker') || n.startsWith('br-') || n.startsWith('veth')) return 'docker'\n if (n.startsWith('tun') || n.startsWith('utun') || n.startsWith('wg') || n.startsWith('tap')) return 'vpn'\n if (n.startsWith('wlan') || n.startsWith('wlp') || n.startsWith('wlx')) return 'wifi'\n if (n.startsWith('eth') || /^en\\d+$/.test(n)) {\n // macOS en1+ is typically wifi; en0 is wired. Default heuristic.\n if (process.platform === 'darwin' && /^en[1-9]\\d*$/.test(n)) return 'wifi'\n return 'lan'\n }\n if (address === '127.0.0.1' || address === '::1') return 'loopback'\n return 'other'\n}\n\n/** Convert an IPv4/IPv6 netmask string to its prefix length (CIDR). */\nexport function prefixLen(netmask: string): number {\n if (!netmask) return 0\n if (netmask.includes(':')) {\n let bits = 0\n for (const group of netmask.split(':')) {\n if (!group) continue\n const n = parseInt(group, 16)\n if (!Number.isFinite(n)) break\n for (let mask = 0x8000; mask; mask >>= 1) {\n if (n & mask) bits++\n else return bits\n }\n }\n return bits\n }\n let bits = 0\n for (const part of netmask.split('.')) {\n const n = parseInt(part, 10)\n if (!Number.isFinite(n)) break\n for (let mask = 0x80; mask; mask >>= 1) {\n if (n & mask) bits++\n else return bits\n }\n }\n return bits\n}\n\nfunction snapshotKey(interfaces: readonly LocalInterface[]): string {\n return [...interfaces]\n .map((i) => `${i.name}|${i.family}|${i.address}|${i.netmask}`)\n .sort()\n .join('\\n')\n}\n\nfunction formatKind(kind: LocalInterface['kind']): string {\n switch (kind) {\n case 'lan': return 'LAN'\n case 'wifi': return 'Wi-Fi'\n case 'vpn': return 'VPN'\n case 'docker': return 'Docker'\n case 'loopback': return 'Loopback'\n case 'other': return 'Other'\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAmCA,IAAM,mBAAmB;AAczB,IAAa,oBAAb,cAAuC,gBAAA,UAA8B;CACnE,YAA2C;CAC3C,kBAA0B;;;CAG1B,iBAAyB;CAEzB,cAAc;EACZ,MAAM;GAAE,kBAAkB,EAAE;GAAE,YAAY;GAAO,CAAC;;CAGpD,MAAgB,eAAgD;EAM9D,IAAI,CAAC,KAAK,OAAO,YAAY;GAC3B,MAAM,OAAO,kBAAkB,KAAK,WAAW,CAAC;GAChD,MAAM,KAAK,qBAAqB;IAAE,kBAAkB;IAAM,YAAY;IAAM,CAAC;GAC7E,KAAK,IAAI,OAAO,KAAK,uCAAuC,EAC1D,MAAM,EAAE,WAAW,MAAM,EAC1B,CAAC;;EAEJ,MAAM,WAAkC;GACtC,MAAM,aAAa;IACjB,YAAY,KAAK,WAAW;IAC5B,UAAU,KAAK,KAAK;IACrB;GACD,cAAc,YAAY,cACxB,eAAe,KAAK,WAAW,EAAE,KAAK,OAAO,iBAAiB,CAC/D;GACD,wBAAwB,OAAO,UAAU;IACvC,MAAM,kBAAkB,MAAM,mBAAmB;IACjD,MAAM,WAAW,MAAM,YAAY;IACnC,MAAM,SAAS,MAAM,UAAU;IAC/B,MAAM,QAAQ,KAAK,OAAO;IAE1B,OAAO,EACL,WAAW,eAFM,eAAe,KAAK,WAAW,EAAE,MAExB,EAAY,MAAM,MAAM,iBAAiB,UAAU,KAAK,gBAAgB,OAAO,EAC1G;;GAEH,qBAAqB,aAAa,EAAE,WAAW,KAAK,OAAO,kBAAkB;GAC7E,2BAA2B,YAAY;IACrC,MAAM,OAAO,kBAAkB,KAAK,WAAW,CAAC;IAChD,MAAM,KAAK,qBAAqB;KAAE,kBAAkB;KAAM,YAAY;KAAM,CAAC;IAC7E,KAAK,IAAI,OAAO,KAAK,+CAA+C,EAClE,MAAM,EAAE,OAAO,KAAK,QAAQ,EAC7B,CAAC;IACF,OAAO,EAAE,WAAW,MAAM;;GAE5B,qBAAqB,OAAO,EAAE,gBAAgB;IAK5C,MAAM,QAAQ,IAAI,IAAI,KAAK,WAAW,CAAC,KAAK,MAAM,EAAE,QAAQ,CAAC;IAC7D,MAAM,UAA6B,CAAC,GAAG,IAAI,IAAI,UAAU,CAAC,CAAC,QAAQ,MAAM,MAAM,IAAI,EAAE,CAAC;IACtF,MAAM,KAAK,qBAAqB,EAAE,kBAAkB,SAAS,CAAC;IAC9D,KAAK,IAAI,OAAO,KAAK,oCAAoC,EACvD,MAAM;KAAE,OAAO,QAAQ;KAAQ,SAAS,UAAU,SAAS,QAAQ;KAAQ,EAC5E,CAAC;IACF,OAAO,EAAE,SAAS,MAAe;;GAEpC;EAID,KAAK,kBAAkB,YAAY,KAAK,WAAW,CAAC;EAEpD,KAAK,YAAY,kBAAkB,KAAK,eAAe,EAAE,iBAAiB;EAC1E,KAAK,UAAU,SAAS;EAKxB,KAAK,IAAI,UAAU,UACjB,EAAE,UAAU,gBAAA,cAAc,sBAAsB,GAC/C,UAAU;GACT,MAAM,OAAQ,MAAM,QAAQ,EAAE;GAC9B,IAAI,OAAO,KAAK,QAAQ,UACtB,IAAI;IACF,MAAM,WAAW,IAAI,IAAI,KAAK,IAAI,CAAC;IAGnC,IAAI,YAAY,CAAC,SAAS,SAAS,eAAe,IAC3C,CAAC,SAAS,WAAW,WAAW,EACrC,KAAK,kBAAkB,SAAS;WAE5B;IAKb;EACD,KAAK,IAAI,UAAU,UACjB,EAAE,UAAU,gBAAA,cAAc,sBAAsB,QAC1C,KAAK,kBAAkB,GAAG,CACjC;EAED,KAAK,IAAI,OAAO,KAAK,6BAA6B,EAChD,MAAM,EAAE,gBAAgB,KAAK,WAAW,CAAC,QAAQ,EAClD,CAAC;EAEF,OAAO,CAAC;GAAE,YAAY,gBAAA;GAAwB;GAAU,CAAC;;CAG3D,MAAgB,aAA4B;EAC1C,IAAI,KAAK,WAAW;GAClB,cAAc,KAAK,UAAU;GAC7B,KAAK,YAAY;;;;;;;;;CAUrB,kBAAkB,UAAwB;EACxC,IAAI,KAAK,mBAAmB,UAAU;EACtC,KAAK,iBAAiB;EACtB,KAAK,IAAI,OAAO,KAAK,0CAA0C,EAC7D,MAAM,EAAE,UAAU,YAAY,aAAa,EAC5C,CAAC;;CAKJ,YAA+C;EAC7C,OAAO,sBAAsB,QAAG,mBAAmB,CAAC;;CAGtD,gBAA8B;EAC5B,MAAM,aAAa,KAAK,WAAW;EACnC,MAAM,MAAM,YAAY,WAAW;EACnC,IAAI,QAAQ,KAAK,iBAAiB;EAElC,KAAK,IAAI,OAAO,KAAK,wCAAwC,EAC3D,MAAM;GAAE,OAAO,WAAW;GAAQ;GAAK,EACxC,CAAC;EACF,KAAK,kBAAkB;EAEvB,KAAK,IAAI,UAAU,KAAK;GACtB,IAAI,yBAAyB,KAAK,KAAK;GACvC,2BAAW,IAAI,MAAM;GACrB,QAAQ;IAAE,MAAM;IAAQ,IAAI;IAAiB;GAC7C,UAAU,gBAAA,cAAc;GACxB,MAAM,EAAE,OAAO,WAAW,QAAQ;GACnC,CAAC;;;;;;;;AAWN,SAAgB,sBACd,QAC2B;CAC3B,MAAM,MAAkB,EAAE;CAC1B,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,OAAO,EAAE;EAClD,IAAI,CAAC,OAAO;EACZ,KAAK,MAAM,KAAK,OAAO;GACrB,IAAI,EAAE,WAAW,UAAU,EAAE,WAAW,QAAQ;GAChD,IAAI,KAAK;IACP;IACA,QAAQ,EAAE;IACV,SAAS,EAAE;IACX,MAAM,EAAE,QAAQ;IAChB,SAAS,EAAE;IACX,UAAU,EAAE;IACZ,KAAK,EAAE;IACR,CAAC;;;CAIN,MAAM,aAA+B,IAAI,KAAK,MAAM;EAClD,MAAM,OAAO,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS;EAKxD,MAAM,gBAAgB,SAAS,SAAS,SAAS;EACjD,MAAM,YAAY,CAAC,EAAE,YAChB,iBACA,oBAAoB,EAAE,SAAS,EAAE,OAAO;EAC7C,MAAM,kBAAkB,YACpB,KACA,oBAAoB;GAAE;GAAM,QAAQ,EAAE;GAAQ,SAAS,EAAE;GAAS,UAAU,EAAE;GAAU,CAAC;EAC7F,OAAO;GACL,MAAM,EAAE;GACR,QAAQ,EAAE;GACV,SAAS,EAAE;GACX,MAAM,EAAE;GACR,SAAS,EAAE;GACX,UAAU,EAAE;GACZ,KAAK,EAAE;GACP;GACA,WAAW;GACX;GACA;GACD;GACD;CAEF,MAAM,YAAY,cAAc,WAAW;CAC3C,OAAO,WAAW,KAAK,WAAW;EAChC,GAAG;EACH,WAAW,cAAc,QACpB,MAAM,SAAS,UAAU,QACzB,MAAM,YAAY,UAAU;EAClC,EAAE;;;;;;;;AASL,SAAgB,eACd,YACA,SAC2B;CAC3B,IAAI,QAAQ,WAAW,GAAG,OAAO;CACjC,MAAM,MAAM,IAAI,IAAI,QAAQ;CAC5B,OAAO,WAAW,QAAQ,MAAM,EAAE,SAAS,cAAc,IAAI,IAAI,EAAE,QAAQ,CAAC;;;;;;AAO9E,SAAgB,cAAc,YAA8D;CAC1F,MAAM,aAAa,WAAW,QAAQ,MACpC,CAAC,EAAE,YACA,EAAE,WAAW,UACb,CAAC,EAAE,QAAQ,WAAW,WAAW,IACjC,EAAE,SAAS,WACf;CACD,IAAI,WAAW,WAAW,GAAG,OAAO;CAEpC,MAAM,WAAmD;EACvD,KAAK;EAAG,MAAM;EAAG,KAAK;EAAG,QAAQ;EAAG,OAAO;EAAG,UAAU;EACzD;CAQD,OAPe,CAAC,GAAG,WAAW,CAAC,MAAM,GAAG,MAAM;EAC5C,MAAM,KAAK,SAAS,EAAE;EACtB,MAAM,KAAK,SAAS,EAAE;EACtB,IAAI,OAAO,IAAI,OAAO,KAAK;EAE3B,OAAO,UAAU,EAAE,QAAQ,GAAG,UAAU,EAAE,QAAQ;GAE7C,CAAO,MAAM;;;;;;;;;;;;;;;;;AAkBtB,SAAgB,eACd,YACA,MACA,iBACA,UACA,gBACA,SAA2B,QACL;CACtB,MAAM,MAA4B,EAAE;CACpC,IAAI,WAAW;CACf,MAAM,YAAY,cAAc,WAAW;CAE3C,MAAM,QACJ,OACA,MACA,OACA,SACA,QACS;EACT,IAAI,KAAK;GACP;GACA;GACA;GACA,eAAe,MAAM;GACrB,WAAW,MAAM;GACjB,iBAAiB,MAAM;GACvB,UAAU;GACX,CAAC;;CAGJ,IAAI,WACF,KACE,WACA,YACA,GAAG,WAAW,UAAU,KAAK,CAAC,KAAK,UAAU,QAC7C,GAAG,OAAO,KAAK,UAAU,QAAQ,GAAG,QACpC,WACD;CAGH,KAAK,MAAM,SAAS,YAAY;EAC9B,IAAI,MAAM,YAAY,MAAM,WAAW,QAAQ;EAC/C,IAAI,MAAM,SAAS,YAAY;EAC/B,IAAI,aAAa,MAAM,SAAS,UAAU,QAAQ,MAAM,YAAY,UAAU,SAAS;EACvF,IAAI,MAAM,QAAQ,WAAW,WAAW,EAAE;EAC1C,KACE,OACA,YACA,GAAG,WAAW,MAAM,KAAK,CAAC,KAAK,MAAM,QACrC,GAAG,OAAO,KAAK,MAAM,QAAQ,GAAG,QAChC,KAAK,WACN;;CAGH,IAAI,gBACF,IAAI,KAAK;EACP,OAAO;EACP,SAAS,WAAW;EACpB,MAAM;EACN,eAAe;EACf,WAAW;EACX,iBAAiB;EACjB,UAAU;EACX,CAAC;CAGJ,IAAI,CAAC,UAAU;EACb,IAAI,SAAS;EACb,KAAK,MAAM,SAAS,YAAY;GAC9B,IAAI,MAAM,YAAY,MAAM,WAAW,QAAQ;GAC/C,IAAI,MAAM,SAAS,YAAY;GAC/B,IAAI,MAAM,QAAQ,WAAW,QAAQ,EAAE;GACvC,KACE,OACA,YACA,GAAG,WAAW,MAAM,KAAK,CAAC,KAAK,MAAM,KAAK,UAC1C,GAAG,OAAO,MAAM,MAAM,QAAQ,IAAI,QAClC,SACD;;;CAIL,IAAI,iBACF,IAAI,KAAK;EACP,OAAO;EACP,SAAS,GAAG,OAAO,eAAe;EAClC,MAAM;EACN,eAAe;EACf,WAAW;EACX,iBAAiB;EACjB,UAAU;EACX,CAAC;CAGJ,OAAO,IAAI,MAAM,GAAG,MAAM,EAAE,WAAW,EAAE,SAAS;;;;;;;;;;;;;;;;AAiBpD,SAAgB,kBAAkB,YAAiD;CACjF,OAAO,CAAC,GAAG,IAAI,IACb,WACG,QAAQ,MAAM,CAAC,EAAE,aACZ,EAAE,SAAS,SAAS,EAAE,SAAS,WAChC,oBAAoB,EAAE,SAAS,EAAE,OAAO,CAAC,CAC7C,KAAK,MAAM,EAAE,QAAQ,CACzB,CAAC;;;;;;;;AASJ,SAAgB,oBAAoB,OAKzB;CACT,IAAI,MAAM,UAAU,OAAO;CAC3B,IAAI,MAAM,SAAS,UAAU,OAAO;CACpC,IAAI,MAAM,SAAS,OAAO,OAAO;CACjC,IAAI,MAAM,SAAS,SAAS,OAAO;CACnC,IAAI,MAAM,WAAW,QAAQ;EAC3B,MAAM,IAAI,MAAM,QAAQ,aAAa;EACrC,IAAI,EAAE,WAAW,QAAQ,EAAE,OAAO;EAClC,IAAI,EAAE,WAAW,KAAK,EAAE,OAAO;EAC/B,IAAI,MAAM,QAAQ,MAAM,OAAO,OAAO;EACtC,OAAO;;CAET,IAAI,MAAM,QAAQ,WAAW,WAAW,EAAE,OAAO;CACjD,OAAO;;;;;;AAOT,SAAgB,oBAAoB,SAAiB,QAAkC;CACrF,IAAI,WAAW,QAAQ;EACrB,IAAI,QAAQ,WAAW,WAAW,EAAE,OAAO;EAC3C,OAAO;;CAGT,MAAM,IAAI,QAAQ,aAAa;CAC/B,IAAI,MAAM,QAAQ,MAAM,OAAO,OAAO;CACtC,IAAI,EAAE,WAAW,QAAQ,EAAE,OAAO;CAClC,IAAI,EAAE,WAAW,KAAK,EAAE,OAAO;CAE/B,IAAI,uBAAuB,KAAK,EAAE,EAAE,OAAO;CAE3C,IAAI,sBAAsB,KAAK,EAAE,EAAE,OAAO;CAE1C,OAAO;;AAGT,SAAgB,aACd,MACA,SACA,UACwB;CACxB,IAAI,YAAY,SAAS,QAAQ,KAAK,WAAW,KAAK,EAAE,OAAO;CAC/D,MAAM,IAAI,KAAK,aAAa;CAC5B,IAAI,EAAE,WAAW,SAAS,IAAI,EAAE,WAAW,MAAM,IAAI,EAAE,WAAW,OAAO,EAAE,OAAO;CAClF,IAAI,EAAE,WAAW,MAAM,IAAI,EAAE,WAAW,OAAO,IAAI,EAAE,WAAW,KAAK,IAAI,EAAE,WAAW,MAAM,EAAE,OAAO;CACrG,IAAI,EAAE,WAAW,OAAO,IAAI,EAAE,WAAW,MAAM,IAAI,EAAE,WAAW,MAAM,EAAE,OAAO;CAC/E,IAAI,EAAE,WAAW,MAAM,IAAI,UAAU,KAAK,EAAE,EAAE;EAE5C,IAAI,QAAQ,aAAa,YAAY,eAAe,KAAK,EAAE,EAAE,OAAO;EACpE,OAAO;;CAET,IAAI,YAAY,eAAe,YAAY,OAAO,OAAO;CACzD,OAAO;;;AAIT,SAAgB,UAAU,SAAyB;CACjD,IAAI,CAAC,SAAS,OAAO;CACrB,IAAI,QAAQ,SAAS,IAAI,EAAE;EACzB,IAAI,OAAO;EACX,KAAK,MAAM,SAAS,QAAQ,MAAM,IAAI,EAAE;GACtC,IAAI,CAAC,OAAO;GACZ,MAAM,IAAI,SAAS,OAAO,GAAG;GAC7B,IAAI,CAAC,OAAO,SAAS,EAAE,EAAE;GACzB,KAAK,IAAI,OAAO,OAAQ,MAAM,SAAS,GACrC,IAAI,IAAI,MAAM;QACT,OAAO;;EAGhB,OAAO;;CAET,IAAI,OAAO;CACX,KAAK,MAAM,QAAQ,QAAQ,MAAM,IAAI,EAAE;EACrC,MAAM,IAAI,SAAS,MAAM,GAAG;EAC5B,IAAI,CAAC,OAAO,SAAS,EAAE,EAAE;EACzB,KAAK,IAAI,OAAO,KAAM,MAAM,SAAS,GACnC,IAAI,IAAI,MAAM;OACT,OAAO;;CAGhB,OAAO;;AAGT,SAAS,YAAY,YAA+C;CAClE,OAAO,CAAC,GAAG,WAAW,CACnB,KAAK,MAAM,GAAG,EAAE,KAAK,GAAG,EAAE,OAAO,GAAG,EAAE,QAAQ,GAAG,EAAE,UAAU,CAC7D,MAAM,CACN,KAAK,KAAK;;AAGf,SAAS,WAAW,MAAsC;CACxD,QAAQ,MAAR;EACE,KAAK,OAAO,OAAO;EACnB,KAAK,QAAQ,OAAO;EACpB,KAAK,OAAO,OAAO;EACnB,KAAK,UAAU,OAAO;EACtB,KAAK,YAAY,OAAO;EACxB,KAAK,SAAS,OAAO"}
1
+ {"version":3,"file":"local-network.addon.js","names":[],"sources":["../../../src/builtins/local-network/local-network.addon.ts"],"sourcesContent":["/**\n * local-network — hub-only system cap implementation.\n *\n * Wraps `os.networkInterfaces()` with:\n * - Coarse kind classification (lan/wifi/docker/vpn/loopback/other)\n * - Auto-select heuristic for the outbound interface\n * - Periodic poll (30s) + diff to emit `LocalNetworkChanged` events\n * so subscribers can react to DHCP renewals, VPN connect, etc.\n * - `getConnectionEndpoints()` — ranked URL list for SDK clients\n *\n * Does NOT poll the cloudflare-tunnel state directly; the public\n * tunnel hostname (if any) is provided by `network-access` consumers\n * via `NetworkTunnelStarted/Stopped` events on the bus, so the cap\n * stays free of cross-addon dependencies.\n */\nimport * as os from 'node:os'\n\nimport type {\n ProviderRegistration,\n ILocalNetworkProvider,\n LocalInterface,\n ConnectionEndpoint,\n} from '@camstack/types'\nimport { BaseAddon, localNetworkCapability, EventCategory } from '@camstack/types'\n\ninterface RawIface {\n readonly name: string\n readonly family: 'IPv4' | 'IPv6'\n readonly address: string\n readonly cidr: string\n readonly netmask: string\n readonly internal: boolean\n readonly mac: string\n}\n\nconst POLL_INTERVAL_MS = 30_000\n\ninterface LocalNetworkConfig {\n /** Empty = \"auto\" (every non-loopback / non-link-local address\n * participates). Non-empty restricts the candidate set to only\n * those operator-pinned addresses. */\n readonly allowedAddresses: readonly string[]\n /** Sentinel — `true` after the first-boot auto-seed runs. Lets the\n * addon distinguish \"fresh install, never touched\" (false → seed\n * with LAN/Wi-Fi addresses) from \"operator explicitly cleared the\n * allowlist\" (true + empty → respect operator's choice). */\n readonly bootSeeded: boolean\n}\n\nexport class LocalNetworkAddon extends BaseAddon<LocalNetworkConfig> {\n private pollTimer: NodeJS.Timeout | null = null\n private lastSnapshotKey = ''\n /** Optional public hostname tracked from `NetworkTunnelStarted`/\n * `Stopped` events on the bus. */\n private publicHostname = ''\n\n constructor() {\n super({ allowedAddresses: [], bootSeeded: false })\n }\n\n protected async onInitialize(): Promise<ProviderRegistration[]> {\n // First-boot auto-seed: on fresh installs, pre-populate the\n // allowlist with addresses from interfaces classified as `lan` or\n // `wifi`. Operators usually want those participating; docker/vpn\n // entries stay opt-in. Loopback is always implicit (no need to\n // include it explicitly).\n if (!this.config.bootSeeded) {\n const seed = autoSeedAllowlist(this.enumerate())\n await this.updateGlobalSettings({ allowedAddresses: seed, bootSeeded: true })\n this.ctx.logger.info('local-network: first-boot auto-seed', {\n meta: { addresses: seed },\n })\n }\n const provider: ILocalNetworkProvider = {\n list: async () => ({\n interfaces: this.enumerate(),\n probedAt: Date.now(),\n }),\n getPreferred: async () => pickPreferred(\n applyAllowlist(this.enumerate(), this.config.allowedAddresses),\n ),\n getConnectionEndpoints: async (input) => {\n const includeLoopback = input.includeLoopback ?? true\n const ipv4Only = input.ipv4Only ?? false\n const scheme = input.scheme ?? 'http'\n const allow = this.config.allowedAddresses\n const interfaces = applyAllowlist(this.enumerate(), allow)\n return {\n endpoints: buildEndpoints(interfaces, input.port, includeLoopback, ipv4Only, this.publicHostname, scheme),\n }\n },\n getAllowedAddresses: async () => ({ addresses: this.config.allowedAddresses }),\n resetAllowlistToBestMatch: async () => {\n const seed = autoSeedAllowlist(this.enumerate())\n await this.updateGlobalSettings({ allowedAddresses: seed, bootSeeded: true })\n this.ctx.logger.info('local-network: allowlist reset to auto-seed', {\n meta: { count: seed.length },\n })\n return { addresses: seed }\n },\n setAllowedAddresses: async ({ addresses }) => {\n // Validate against the current snapshot — drop anything that\n // doesn't actually exist on this host. Prevents an admin from\n // saving a typo'd address that would then silently filter out\n // every real candidate.\n const known = new Set(this.enumerate().map((i) => i.address))\n const cleaned: readonly string[] = [...new Set(addresses)].filter((a) => known.has(a))\n await this.updateGlobalSettings({ allowedAddresses: cleaned })\n this.ctx.logger.info('local-network: allowlist updated', {\n meta: { count: cleaned.length, dropped: addresses.length - cleaned.length },\n })\n return { success: true as const }\n },\n }\n\n // Seed the snapshot key so the first poll doesn't fire a false\n // change event on boot.\n this.lastSnapshotKey = snapshotKey(this.enumerate())\n\n this.pollTimer = setInterval(() => this.detectChanges(), POLL_INTERVAL_MS)\n this.pollTimer.unref?.()\n\n // Track the active public hostname by tailing the network-access\n // lifecycle events that every tunnel provider already emits. We\n // can't import cross-package so the parsing stays defensive.\n this.ctx.eventBus?.subscribe(\n { category: EventCategory.NetworkTunnelStarted },\n (event) => {\n const data = (event.data ?? {}) as { url?: unknown }\n if (typeof data.url === 'string') {\n try {\n const hostname = new URL(data.url).hostname\n // Ignore the pre-FQDN placeholders the tunnel emits before\n // cloudflared's stdout reports the real *.trycloudflare.com.\n if (hostname && !hostname.endsWith('.placeholder')\n && !hostname.startsWith('pending.')) {\n this.setPublicHostname(hostname)\n }\n } catch {\n // Malformed URL — leave the cached hostname as-is.\n }\n }\n },\n )\n this.ctx.eventBus?.subscribe(\n { category: EventCategory.NetworkTunnelStopped },\n () => this.setPublicHostname(''),\n )\n\n this.ctx.logger.info('local-network initialized', {\n meta: { interfaceCount: this.enumerate().length },\n })\n\n return [{ capability: localNetworkCapability, provider }]\n }\n\n protected async onShutdown(): Promise<void> {\n if (this.pollTimer) {\n clearInterval(this.pollTimer)\n this.pollTimer = null\n }\n }\n\n /**\n * Other hub addons (e.g. cloudflare-tunnel) signal the active public\n * FQDN by emitting `NetworkTunnelStarted` on the bus — handled in\n * `onInitialize`. This setter exists for tests + future direct\n * callers; cleared by passing an empty string.\n */\n setPublicHostname(hostname: string): void {\n if (this.publicHostname === hostname) return\n this.publicHostname = hostname\n this.ctx.logger.info('local-network: public hostname updated', {\n meta: { hostname: hostname || '(cleared)' },\n })\n }\n\n // ── Internals ──────────────────────────────────────────────────────\n\n private enumerate(): readonly LocalInterface[] {\n return enumerateOsInterfaces(os.networkInterfaces())\n }\n\n private detectChanges(): void {\n const interfaces = this.enumerate()\n const key = snapshotKey(interfaces)\n if (key === this.lastSnapshotKey) return\n\n this.ctx.logger.info('local-network: interface set changed', {\n meta: { count: interfaces.length, key },\n })\n this.lastSnapshotKey = key\n\n this.ctx.eventBus?.emit({\n id: `local-network-changed-${Date.now()}`,\n timestamp: new Date(),\n source: { type: 'core', id: 'local-network' },\n category: EventCategory.LocalNetworkChanged,\n data: { count: interfaces.length },\n })\n }\n}\n\n// ── Pure helpers (exported for tests + reusable by other consumers) ──\n\n/**\n * Lift `os.networkInterfaces()` into our `LocalInterface[]` shape with\n * kind classification + preferred flag. Pure — takes the raw result so\n * tests can feed synthetic data.\n */\ninterface OsNetworkInterfaceInfo {\n readonly family: string\n readonly address: string\n readonly cidr?: string | null\n readonly netmask: string\n readonly internal: boolean\n readonly mac: string\n}\n\nexport function enumerateOsInterfaces(\n ifaces: NodeJS.Dict<readonly OsNetworkInterfaceInfo[]>,\n): readonly LocalInterface[] {\n const raw: RawIface[] = []\n for (const [name, addrs] of Object.entries(ifaces)) {\n if (!addrs) continue\n for (const a of addrs) {\n if (a.family !== 'IPv4' && a.family !== 'IPv6') continue\n raw.push({\n name,\n family: a.family,\n address: a.address,\n cidr: a.cidr ?? '',\n netmask: a.netmask,\n internal: a.internal,\n mac: a.mac,\n })\n }\n }\n\n const classified: LocalInterface[] = raw.map((r) => {\n const kind = classifyKind(r.name, r.address, r.internal)\n // \"Plausible\" = passes the RFC range gate AND comes from an\n // interface kind a client could realistically reach. Operators\n // can still pin a docker / vpn address — we just mark it visually\n // so the choice is intentional.\n const reachableKind = kind === 'lan' || kind === 'wifi'\n const plausible = !r.internal\n && reachableKind\n && isPlausibleAutoSeed(r.address, r.family)\n const plausibleReason = plausible\n ? ''\n : explainNonPlausible({ kind, family: r.family, address: r.address, internal: r.internal })\n return {\n name: r.name,\n family: r.family,\n address: r.address,\n cidr: r.cidr,\n netmask: r.netmask,\n internal: r.internal,\n mac: r.mac,\n kind,\n preferred: false,\n plausible,\n plausibleReason,\n }\n })\n\n const preferred = pickPreferred(classified)\n return classified.map((iface) => ({\n ...iface,\n preferred: preferred !== null\n && iface.name === preferred.name\n && iface.address === preferred.address,\n }))\n}\n\n/**\n * Filter the interface list by the operator's allowlist. Empty\n * allowlist = no-op (every interface passes); otherwise only entries\n * whose `address` matches an allowlist entry remain. Loopback always\n * survives so the SDK keeps `127.0.0.1` as the last-resort fallback.\n */\nexport function applyAllowlist(\n interfaces: readonly LocalInterface[],\n allowed: readonly string[],\n): readonly LocalInterface[] {\n if (allowed.length === 0) return interfaces\n const set = new Set(allowed)\n return interfaces.filter((i) => i.kind === 'loopback' || set.has(i.address))\n}\n\n/**\n * Rank interfaces and pick the auto-selected outbound one. See the\n * cap's `getPreferred` doc for the heuristic.\n */\nexport function pickPreferred(interfaces: readonly LocalInterface[]): LocalInterface | null {\n const candidates = interfaces.filter((i) =>\n !i.internal\n && i.family === 'IPv4'\n && !i.address.startsWith('169.254.') // RFC 3927 link-local\n && i.kind !== 'loopback',\n )\n if (candidates.length === 0) return null\n\n const kindRank: Record<LocalInterface['kind'], number> = {\n lan: 1, wifi: 2, vpn: 3, docker: 4, other: 5, loopback: 99,\n }\n const sorted = [...candidates].sort((a, b) => {\n const ra = kindRank[a.kind]\n const rb = kindRank[b.kind]\n if (ra !== rb) return ra - rb\n // Tie-break on netmask length — `/24` beats `/16`.\n return prefixLen(b.netmask) - prefixLen(a.netmask)\n })\n return sorted[0] ?? null\n}\n\n/**\n * Build the ordered candidate URL list. Priority schema:\n * 0 — preferred LAN IPv4\n * 10+ — other LAN IPv4\n * 100 — public tunnel hostname (always HTTPS)\n * 200+ — LAN IPv6\n * 1000 — loopback (last resort)\n *\n * `scheme` controls LAN + loopback URLs. Browsers running over HTTPS\n * block `http://` candidates as mixed content, so callers loaded over\n * HTTPS should pass `scheme: 'https'` even when probing a LAN IP — the\n * hub's cert manager already issues a SAN-multi cert covering local\n * interfaces. The public tunnel always emits `https://` (Cloudflare\n * terminates TLS for us).\n */\nexport function buildEndpoints(\n interfaces: readonly LocalInterface[],\n port: number,\n includeLoopback: boolean,\n ipv4Only: boolean,\n publicHostname: string,\n scheme: 'http' | 'https' = 'http',\n): ConnectionEndpoint[] {\n const out: ConnectionEndpoint[] = []\n let priority = 0\n const preferred = pickPreferred(interfaces)\n\n const emit = (\n iface: LocalInterface,\n kind: ConnectionEndpoint['kind'],\n label: string,\n baseUrl: string,\n pri: number,\n ): void => {\n out.push({\n label,\n baseUrl,\n kind,\n interfaceKind: iface.kind,\n plausible: iface.plausible,\n plausibleReason: iface.plausibleReason,\n priority: pri,\n })\n }\n\n if (preferred) {\n emit(\n preferred,\n 'lan-ipv4',\n `${formatKind(preferred.kind)} — ${preferred.name}`,\n `${scheme}://${preferred.address}:${port}`,\n priority++,\n )\n }\n\n for (const iface of interfaces) {\n if (iface.internal || iface.family !== 'IPv4') continue\n if (iface.kind === 'loopback') continue\n if (preferred && iface.name === preferred.name && iface.address === preferred.address) continue\n if (iface.address.startsWith('169.254.')) continue\n emit(\n iface,\n 'lan-ipv4',\n `${formatKind(iface.kind)} — ${iface.name}`,\n `${scheme}://${iface.address}:${port}`,\n 10 + priority++,\n )\n }\n\n if (publicHostname) {\n out.push({\n label: 'Public tunnel',\n baseUrl: `https://${publicHostname}`,\n kind: 'public',\n interfaceKind: 'public',\n plausible: true,\n plausibleReason: '',\n priority: 100,\n })\n }\n\n if (!ipv4Only) {\n let v6prio = 200\n for (const iface of interfaces) {\n if (iface.internal || iface.family !== 'IPv6') continue\n if (iface.kind === 'loopback') continue\n if (iface.address.startsWith('fe80:')) continue // link-local\n emit(\n iface,\n 'lan-ipv6',\n `${formatKind(iface.kind)} — ${iface.name} (IPv6)`,\n `${scheme}://[${iface.address}]:${port}`,\n v6prio++,\n )\n }\n }\n\n if (includeLoopback) {\n out.push({\n label: 'Loopback',\n baseUrl: `${scheme}://127.0.0.1:${port}`,\n kind: 'loopback',\n interfaceKind: 'loopback',\n plausible: false,\n plausibleReason: 'Loopback — last-resort fallback when no other endpoint responds.',\n priority: 1000,\n })\n }\n\n return out.sort((a, b) => a.priority - b.priority)\n}\n\n/**\n * First-boot heuristic: which addresses should the allowlist start\n * with? Includes LAN + Wi-Fi IPv4 addresses + plausible IPv6:\n *\n * - **IPv4**: skip link-local (`169.254.*`), keep the rest.\n * - **IPv6**: skip link-local (`fe80::*`), unspecified, and\n * multicast. Keep ULAs (`fc00::/7` → `fc??:` / `fd??:`) and Global\n * Unicast addresses (`2000::/3` → `2???`/`3???`). Privacy-extension\n * temporary addresses get included by default; the operator can\n * prune them from the Network Addresses tab if the rotating IPs\n * become a nuisance.\n *\n * Skips docker/vpn/loopback/other entirely — those stay opt-in.\n */\nexport function autoSeedAllowlist(interfaces: readonly LocalInterface[]): string[] {\n return [...new Set(\n interfaces\n .filter((i) => !i.internal\n && (i.kind === 'lan' || i.kind === 'wifi')\n && isPlausibleAutoSeed(i.address, i.family))\n .map((i) => i.address),\n )]\n}\n\n/**\n * Per-interface tooltip text surfaced on the \"Unlikely usable\" badge.\n * Server-side so the UI doesn't re-derive the rationale (single source\n * of truth). Returns `''` for plausible entries; the addon overlays\n * this on the `LocalInterface.plausibleReason` field.\n */\nexport function explainNonPlausible(input: {\n readonly kind: LocalInterface['kind']\n readonly family: 'IPv4' | 'IPv6'\n readonly address: string\n readonly internal: boolean\n}): string {\n if (input.internal) return 'Internal interface (loopback) — not reachable from clients.'\n if (input.kind === 'docker') return 'Docker bridge — only reachable from inside the container network.'\n if (input.kind === 'vpn') return 'VPN tunnel — only reachable while the VPN is connected.'\n if (input.kind === 'other') return 'Unrecognised interface kind — verify reachability before pinning.'\n if (input.family === 'IPv6') {\n const a = input.address.toLowerCase()\n if (a.startsWith('fe80:')) return 'IPv6 link-local — only reachable on the same link, not routed.'\n if (a.startsWith('ff')) return 'IPv6 multicast — not a unicast address.'\n if (a === '::' || a === '::1') return 'IPv6 loopback / unspecified — not a public address.'\n return 'IPv6 address outside the ULA / GUA ranges — verify before pinning.'\n }\n if (input.address.startsWith('169.254.')) return 'IPv4 link-local (RFC 3927) — only valid when DHCP fails.'\n return 'Address looks unusual for client traffic — verify before pinning.'\n}\n\n/**\n * Per-address gate used by `autoSeedAllowlist`. Exposed for tests so\n * we can pin every classification rule without standing up the addon.\n */\nexport function isPlausibleAutoSeed(address: string, family: 'IPv4' | 'IPv6'): boolean {\n if (family === 'IPv4') {\n if (address.startsWith('169.254.')) return false // RFC 3927 link-local\n return true\n }\n // IPv6 — normalise to lower-case once.\n const a = address.toLowerCase()\n if (a === '::' || a === '::1') return false\n if (a.startsWith('fe80:')) return false // link-local\n if (a.startsWith('ff')) return false // multicast (ff00::/8)\n // ULA (fc00::/7 → first hex group starts with `fc` or `fd`).\n if (/^f[cd][0-9a-f]{0,2}:/.test(a)) return true\n // GUA (2000::/3 → first nibble 2 or 3).\n if (/^[23][0-9a-f]{0,3}:/.test(a)) return true\n // Anything else (deprecated / experimental ranges) — leave opt-in.\n return false\n}\n\nexport function classifyKind(\n name: string,\n address: string,\n internal: boolean,\n): LocalInterface['kind'] {\n if (internal || name === 'lo' || name.startsWith('lo')) return 'loopback'\n const n = name.toLowerCase()\n if (n.startsWith('docker') || n.startsWith('br-') || n.startsWith('veth')) return 'docker'\n if (n.startsWith('tun') || n.startsWith('utun') || n.startsWith('wg') || n.startsWith('tap')) return 'vpn'\n if (n.startsWith('wlan') || n.startsWith('wlp') || n.startsWith('wlx')) return 'wifi'\n if (n.startsWith('eth') || /^en\\d+$/.test(n)) {\n // macOS en1+ is typically wifi; en0 is wired. Default heuristic.\n if (process.platform === 'darwin' && /^en[1-9]\\d*$/.test(n)) return 'wifi'\n return 'lan'\n }\n if (address === '127.0.0.1' || address === '::1') return 'loopback'\n return 'other'\n}\n\n/** Convert an IPv4/IPv6 netmask string to its prefix length (CIDR). */\nexport function prefixLen(netmask: string): number {\n if (!netmask) return 0\n if (netmask.includes(':')) {\n let bits = 0\n for (const group of netmask.split(':')) {\n if (!group) continue\n const n = parseInt(group, 16)\n if (!Number.isFinite(n)) break\n for (let mask = 0x8000; mask; mask >>= 1) {\n if (n & mask) bits++\n else return bits\n }\n }\n return bits\n }\n let bits = 0\n for (const part of netmask.split('.')) {\n const n = parseInt(part, 10)\n if (!Number.isFinite(n)) break\n for (let mask = 0x80; mask; mask >>= 1) {\n if (n & mask) bits++\n else return bits\n }\n }\n return bits\n}\n\nfunction snapshotKey(interfaces: readonly LocalInterface[]): string {\n return [...interfaces]\n .map((i) => `${i.name}|${i.family}|${i.address}|${i.netmask}`)\n .sort()\n .join('\\n')\n}\n\nfunction formatKind(kind: LocalInterface['kind']): string {\n switch (kind) {\n case 'lan': return 'LAN'\n case 'wifi': return 'Wi-Fi'\n case 'vpn': return 'VPN'\n case 'docker': return 'Docker'\n case 'loopback': return 'Loopback'\n case 'other': return 'Other'\n }\n}\n"],"mappings":"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"}
@@ -130,11 +130,6 @@ var LocalNetworkAddon = class extends BaseAddon {
130
130
  });
131
131
  }
132
132
  };
133
- /**
134
- * Lift `os.networkInterfaces()` into our `LocalInterface[]` shape with
135
- * kind classification + preferred flag. Pure — takes the raw result so
136
- * tests can feed synthetic data.
137
- */
138
133
  function enumerateOsInterfaces(ifaces) {
139
134
  const raw = [];
140
135
  for (const [name, addrs] of Object.entries(ifaces)) {
@@ -1 +1 @@
1
- {"version":3,"file":"local-network.addon.mjs","names":[],"sources":["../../../src/builtins/local-network/local-network.addon.ts"],"sourcesContent":["/**\n * local-network — hub-only system cap implementation.\n *\n * Wraps `os.networkInterfaces()` with:\n * - Coarse kind classification (lan/wifi/docker/vpn/loopback/other)\n * - Auto-select heuristic for the outbound interface\n * - Periodic poll (30s) + diff to emit `LocalNetworkChanged` events\n * so subscribers can react to DHCP renewals, VPN connect, etc.\n * - `getConnectionEndpoints()` — ranked URL list for SDK clients\n *\n * Does NOT poll the cloudflare-tunnel state directly; the public\n * tunnel hostname (if any) is provided by `network-access` consumers\n * via `NetworkTunnelStarted/Stopped` events on the bus, so the cap\n * stays free of cross-addon dependencies.\n */\nimport * as os from 'node:os'\n\nimport type {\n ProviderRegistration,\n ILocalNetworkProvider,\n LocalInterface,\n ConnectionEndpoint,\n} from '@camstack/types'\nimport { BaseAddon, localNetworkCapability, EventCategory } from '@camstack/types'\n\ninterface RawIface {\n readonly name: string\n readonly family: 'IPv4' | 'IPv6'\n readonly address: string\n readonly cidr: string\n readonly netmask: string\n readonly internal: boolean\n readonly mac: string\n}\n\nconst POLL_INTERVAL_MS = 30_000\n\ninterface LocalNetworkConfig {\n /** Empty = \"auto\" (every non-loopback / non-link-local address\n * participates). Non-empty restricts the candidate set to only\n * those operator-pinned addresses. */\n readonly allowedAddresses: readonly string[]\n /** Sentinel — `true` after the first-boot auto-seed runs. Lets the\n * addon distinguish \"fresh install, never touched\" (false → seed\n * with LAN/Wi-Fi addresses) from \"operator explicitly cleared the\n * allowlist\" (true + empty → respect operator's choice). */\n readonly bootSeeded: boolean\n}\n\nexport class LocalNetworkAddon extends BaseAddon<LocalNetworkConfig> {\n private pollTimer: NodeJS.Timeout | null = null\n private lastSnapshotKey = ''\n /** Optional public hostname tracked from `NetworkTunnelStarted`/\n * `Stopped` events on the bus. */\n private publicHostname = ''\n\n constructor() {\n super({ allowedAddresses: [], bootSeeded: false })\n }\n\n protected async onInitialize(): Promise<ProviderRegistration[]> {\n // First-boot auto-seed: on fresh installs, pre-populate the\n // allowlist with addresses from interfaces classified as `lan` or\n // `wifi`. Operators usually want those participating; docker/vpn\n // entries stay opt-in. Loopback is always implicit (no need to\n // include it explicitly).\n if (!this.config.bootSeeded) {\n const seed = autoSeedAllowlist(this.enumerate())\n await this.updateGlobalSettings({ allowedAddresses: seed, bootSeeded: true })\n this.ctx.logger.info('local-network: first-boot auto-seed', {\n meta: { addresses: seed },\n })\n }\n const provider: ILocalNetworkProvider = {\n list: async () => ({\n interfaces: this.enumerate(),\n probedAt: Date.now(),\n }),\n getPreferred: async () => pickPreferred(\n applyAllowlist(this.enumerate(), this.config.allowedAddresses),\n ),\n getConnectionEndpoints: async (input) => {\n const includeLoopback = input.includeLoopback ?? true\n const ipv4Only = input.ipv4Only ?? false\n const scheme = input.scheme ?? 'http'\n const allow = this.config.allowedAddresses\n const interfaces = applyAllowlist(this.enumerate(), allow)\n return {\n endpoints: buildEndpoints(interfaces, input.port, includeLoopback, ipv4Only, this.publicHostname, scheme),\n }\n },\n getAllowedAddresses: async () => ({ addresses: this.config.allowedAddresses }),\n resetAllowlistToBestMatch: async () => {\n const seed = autoSeedAllowlist(this.enumerate())\n await this.updateGlobalSettings({ allowedAddresses: seed, bootSeeded: true })\n this.ctx.logger.info('local-network: allowlist reset to auto-seed', {\n meta: { count: seed.length },\n })\n return { addresses: seed }\n },\n setAllowedAddresses: async ({ addresses }) => {\n // Validate against the current snapshot — drop anything that\n // doesn't actually exist on this host. Prevents an admin from\n // saving a typo'd address that would then silently filter out\n // every real candidate.\n const known = new Set(this.enumerate().map((i) => i.address))\n const cleaned: readonly string[] = [...new Set(addresses)].filter((a) => known.has(a))\n await this.updateGlobalSettings({ allowedAddresses: cleaned })\n this.ctx.logger.info('local-network: allowlist updated', {\n meta: { count: cleaned.length, dropped: addresses.length - cleaned.length },\n })\n return { success: true as const }\n },\n }\n\n // Seed the snapshot key so the first poll doesn't fire a false\n // change event on boot.\n this.lastSnapshotKey = snapshotKey(this.enumerate())\n\n this.pollTimer = setInterval(() => this.detectChanges(), POLL_INTERVAL_MS)\n this.pollTimer.unref?.()\n\n // Track the active public hostname by tailing the network-access\n // lifecycle events that every tunnel provider already emits. We\n // can't import cross-package so the parsing stays defensive.\n this.ctx.eventBus?.subscribe(\n { category: EventCategory.NetworkTunnelStarted },\n (event) => {\n const data = (event.data ?? {}) as { url?: unknown }\n if (typeof data.url === 'string') {\n try {\n const hostname = new URL(data.url).hostname\n // Ignore the pre-FQDN placeholders the tunnel emits before\n // cloudflared's stdout reports the real *.trycloudflare.com.\n if (hostname && !hostname.endsWith('.placeholder')\n && !hostname.startsWith('pending.')) {\n this.setPublicHostname(hostname)\n }\n } catch {\n // Malformed URL — leave the cached hostname as-is.\n }\n }\n },\n )\n this.ctx.eventBus?.subscribe(\n { category: EventCategory.NetworkTunnelStopped },\n () => this.setPublicHostname(''),\n )\n\n this.ctx.logger.info('local-network initialized', {\n meta: { interfaceCount: this.enumerate().length },\n })\n\n return [{ capability: localNetworkCapability, provider }]\n }\n\n protected async onShutdown(): Promise<void> {\n if (this.pollTimer) {\n clearInterval(this.pollTimer)\n this.pollTimer = null\n }\n }\n\n /**\n * Other hub addons (e.g. cloudflare-tunnel) signal the active public\n * FQDN by emitting `NetworkTunnelStarted` on the bus — handled in\n * `onInitialize`. This setter exists for tests + future direct\n * callers; cleared by passing an empty string.\n */\n setPublicHostname(hostname: string): void {\n if (this.publicHostname === hostname) return\n this.publicHostname = hostname\n this.ctx.logger.info('local-network: public hostname updated', {\n meta: { hostname: hostname || '(cleared)' },\n })\n }\n\n // ── Internals ──────────────────────────────────────────────────────\n\n private enumerate(): readonly LocalInterface[] {\n return enumerateOsInterfaces(os.networkInterfaces())\n }\n\n private detectChanges(): void {\n const interfaces = this.enumerate()\n const key = snapshotKey(interfaces)\n if (key === this.lastSnapshotKey) return\n\n this.ctx.logger.info('local-network: interface set changed', {\n meta: { count: interfaces.length, key },\n })\n this.lastSnapshotKey = key\n\n this.ctx.eventBus?.emit({\n id: `local-network-changed-${Date.now()}`,\n timestamp: new Date(),\n source: { type: 'core', id: 'local-network' },\n category: EventCategory.LocalNetworkChanged,\n data: { count: interfaces.length },\n })\n }\n}\n\n// ── Pure helpers (exported for tests + reusable by other consumers) ──\n\n/**\n * Lift `os.networkInterfaces()` into our `LocalInterface[]` shape with\n * kind classification + preferred flag. Pure — takes the raw result so\n * tests can feed synthetic data.\n */\nexport function enumerateOsInterfaces(\n ifaces: NodeJS.Dict<NodeJS.NetworkInterfaceInfo[]>,\n): readonly LocalInterface[] {\n const raw: RawIface[] = []\n for (const [name, addrs] of Object.entries(ifaces)) {\n if (!addrs) continue\n for (const a of addrs) {\n if (a.family !== 'IPv4' && a.family !== 'IPv6') continue\n raw.push({\n name,\n family: a.family,\n address: a.address,\n cidr: a.cidr ?? '',\n netmask: a.netmask,\n internal: a.internal,\n mac: a.mac,\n })\n }\n }\n\n const classified: LocalInterface[] = raw.map((r) => {\n const kind = classifyKind(r.name, r.address, r.internal)\n // \"Plausible\" = passes the RFC range gate AND comes from an\n // interface kind a client could realistically reach. Operators\n // can still pin a docker / vpn address — we just mark it visually\n // so the choice is intentional.\n const reachableKind = kind === 'lan' || kind === 'wifi'\n const plausible = !r.internal\n && reachableKind\n && isPlausibleAutoSeed(r.address, r.family)\n const plausibleReason = plausible\n ? ''\n : explainNonPlausible({ kind, family: r.family, address: r.address, internal: r.internal })\n return {\n name: r.name,\n family: r.family,\n address: r.address,\n cidr: r.cidr,\n netmask: r.netmask,\n internal: r.internal,\n mac: r.mac,\n kind,\n preferred: false,\n plausible,\n plausibleReason,\n }\n })\n\n const preferred = pickPreferred(classified)\n return classified.map((iface) => ({\n ...iface,\n preferred: preferred !== null\n && iface.name === preferred.name\n && iface.address === preferred.address,\n }))\n}\n\n/**\n * Filter the interface list by the operator's allowlist. Empty\n * allowlist = no-op (every interface passes); otherwise only entries\n * whose `address` matches an allowlist entry remain. Loopback always\n * survives so the SDK keeps `127.0.0.1` as the last-resort fallback.\n */\nexport function applyAllowlist(\n interfaces: readonly LocalInterface[],\n allowed: readonly string[],\n): readonly LocalInterface[] {\n if (allowed.length === 0) return interfaces\n const set = new Set(allowed)\n return interfaces.filter((i) => i.kind === 'loopback' || set.has(i.address))\n}\n\n/**\n * Rank interfaces and pick the auto-selected outbound one. See the\n * cap's `getPreferred` doc for the heuristic.\n */\nexport function pickPreferred(interfaces: readonly LocalInterface[]): LocalInterface | null {\n const candidates = interfaces.filter((i) =>\n !i.internal\n && i.family === 'IPv4'\n && !i.address.startsWith('169.254.') // RFC 3927 link-local\n && i.kind !== 'loopback',\n )\n if (candidates.length === 0) return null\n\n const kindRank: Record<LocalInterface['kind'], number> = {\n lan: 1, wifi: 2, vpn: 3, docker: 4, other: 5, loopback: 99,\n }\n const sorted = [...candidates].sort((a, b) => {\n const ra = kindRank[a.kind]\n const rb = kindRank[b.kind]\n if (ra !== rb) return ra - rb\n // Tie-break on netmask length — `/24` beats `/16`.\n return prefixLen(b.netmask) - prefixLen(a.netmask)\n })\n return sorted[0] ?? null\n}\n\n/**\n * Build the ordered candidate URL list. Priority schema:\n * 0 — preferred LAN IPv4\n * 10+ — other LAN IPv4\n * 100 — public tunnel hostname (always HTTPS)\n * 200+ — LAN IPv6\n * 1000 — loopback (last resort)\n *\n * `scheme` controls LAN + loopback URLs. Browsers running over HTTPS\n * block `http://` candidates as mixed content, so callers loaded over\n * HTTPS should pass `scheme: 'https'` even when probing a LAN IP — the\n * hub's cert manager already issues a SAN-multi cert covering local\n * interfaces. The public tunnel always emits `https://` (Cloudflare\n * terminates TLS for us).\n */\nexport function buildEndpoints(\n interfaces: readonly LocalInterface[],\n port: number,\n includeLoopback: boolean,\n ipv4Only: boolean,\n publicHostname: string,\n scheme: 'http' | 'https' = 'http',\n): ConnectionEndpoint[] {\n const out: ConnectionEndpoint[] = []\n let priority = 0\n const preferred = pickPreferred(interfaces)\n\n const emit = (\n iface: LocalInterface,\n kind: ConnectionEndpoint['kind'],\n label: string,\n baseUrl: string,\n pri: number,\n ): void => {\n out.push({\n label,\n baseUrl,\n kind,\n interfaceKind: iface.kind,\n plausible: iface.plausible,\n plausibleReason: iface.plausibleReason,\n priority: pri,\n })\n }\n\n if (preferred) {\n emit(\n preferred,\n 'lan-ipv4',\n `${formatKind(preferred.kind)} — ${preferred.name}`,\n `${scheme}://${preferred.address}:${port}`,\n priority++,\n )\n }\n\n for (const iface of interfaces) {\n if (iface.internal || iface.family !== 'IPv4') continue\n if (iface.kind === 'loopback') continue\n if (preferred && iface.name === preferred.name && iface.address === preferred.address) continue\n if (iface.address.startsWith('169.254.')) continue\n emit(\n iface,\n 'lan-ipv4',\n `${formatKind(iface.kind)} — ${iface.name}`,\n `${scheme}://${iface.address}:${port}`,\n 10 + priority++,\n )\n }\n\n if (publicHostname) {\n out.push({\n label: 'Public tunnel',\n baseUrl: `https://${publicHostname}`,\n kind: 'public',\n interfaceKind: 'public',\n plausible: true,\n plausibleReason: '',\n priority: 100,\n })\n }\n\n if (!ipv4Only) {\n let v6prio = 200\n for (const iface of interfaces) {\n if (iface.internal || iface.family !== 'IPv6') continue\n if (iface.kind === 'loopback') continue\n if (iface.address.startsWith('fe80:')) continue // link-local\n emit(\n iface,\n 'lan-ipv6',\n `${formatKind(iface.kind)} — ${iface.name} (IPv6)`,\n `${scheme}://[${iface.address}]:${port}`,\n v6prio++,\n )\n }\n }\n\n if (includeLoopback) {\n out.push({\n label: 'Loopback',\n baseUrl: `${scheme}://127.0.0.1:${port}`,\n kind: 'loopback',\n interfaceKind: 'loopback',\n plausible: false,\n plausibleReason: 'Loopback — last-resort fallback when no other endpoint responds.',\n priority: 1000,\n })\n }\n\n return out.sort((a, b) => a.priority - b.priority)\n}\n\n/**\n * First-boot heuristic: which addresses should the allowlist start\n * with? Includes LAN + Wi-Fi IPv4 addresses + plausible IPv6:\n *\n * - **IPv4**: skip link-local (`169.254.*`), keep the rest.\n * - **IPv6**: skip link-local (`fe80::*`), unspecified, and\n * multicast. Keep ULAs (`fc00::/7` → `fc??:` / `fd??:`) and Global\n * Unicast addresses (`2000::/3` → `2???`/`3???`). Privacy-extension\n * temporary addresses get included by default; the operator can\n * prune them from the Network Addresses tab if the rotating IPs\n * become a nuisance.\n *\n * Skips docker/vpn/loopback/other entirely — those stay opt-in.\n */\nexport function autoSeedAllowlist(interfaces: readonly LocalInterface[]): string[] {\n return [...new Set(\n interfaces\n .filter((i) => !i.internal\n && (i.kind === 'lan' || i.kind === 'wifi')\n && isPlausibleAutoSeed(i.address, i.family))\n .map((i) => i.address),\n )]\n}\n\n/**\n * Per-interface tooltip text surfaced on the \"Unlikely usable\" badge.\n * Server-side so the UI doesn't re-derive the rationale (single source\n * of truth). Returns `''` for plausible entries; the addon overlays\n * this on the `LocalInterface.plausibleReason` field.\n */\nexport function explainNonPlausible(input: {\n readonly kind: LocalInterface['kind']\n readonly family: 'IPv4' | 'IPv6'\n readonly address: string\n readonly internal: boolean\n}): string {\n if (input.internal) return 'Internal interface (loopback) — not reachable from clients.'\n if (input.kind === 'docker') return 'Docker bridge — only reachable from inside the container network.'\n if (input.kind === 'vpn') return 'VPN tunnel — only reachable while the VPN is connected.'\n if (input.kind === 'other') return 'Unrecognised interface kind — verify reachability before pinning.'\n if (input.family === 'IPv6') {\n const a = input.address.toLowerCase()\n if (a.startsWith('fe80:')) return 'IPv6 link-local — only reachable on the same link, not routed.'\n if (a.startsWith('ff')) return 'IPv6 multicast — not a unicast address.'\n if (a === '::' || a === '::1') return 'IPv6 loopback / unspecified — not a public address.'\n return 'IPv6 address outside the ULA / GUA ranges — verify before pinning.'\n }\n if (input.address.startsWith('169.254.')) return 'IPv4 link-local (RFC 3927) — only valid when DHCP fails.'\n return 'Address looks unusual for client traffic — verify before pinning.'\n}\n\n/**\n * Per-address gate used by `autoSeedAllowlist`. Exposed for tests so\n * we can pin every classification rule without standing up the addon.\n */\nexport function isPlausibleAutoSeed(address: string, family: 'IPv4' | 'IPv6'): boolean {\n if (family === 'IPv4') {\n if (address.startsWith('169.254.')) return false // RFC 3927 link-local\n return true\n }\n // IPv6 — normalise to lower-case once.\n const a = address.toLowerCase()\n if (a === '::' || a === '::1') return false\n if (a.startsWith('fe80:')) return false // link-local\n if (a.startsWith('ff')) return false // multicast (ff00::/8)\n // ULA (fc00::/7 → first hex group starts with `fc` or `fd`).\n if (/^f[cd][0-9a-f]{0,2}:/.test(a)) return true\n // GUA (2000::/3 → first nibble 2 or 3).\n if (/^[23][0-9a-f]{0,3}:/.test(a)) return true\n // Anything else (deprecated / experimental ranges) — leave opt-in.\n return false\n}\n\nexport function classifyKind(\n name: string,\n address: string,\n internal: boolean,\n): LocalInterface['kind'] {\n if (internal || name === 'lo' || name.startsWith('lo')) return 'loopback'\n const n = name.toLowerCase()\n if (n.startsWith('docker') || n.startsWith('br-') || n.startsWith('veth')) return 'docker'\n if (n.startsWith('tun') || n.startsWith('utun') || n.startsWith('wg') || n.startsWith('tap')) return 'vpn'\n if (n.startsWith('wlan') || n.startsWith('wlp') || n.startsWith('wlx')) return 'wifi'\n if (n.startsWith('eth') || /^en\\d+$/.test(n)) {\n // macOS en1+ is typically wifi; en0 is wired. Default heuristic.\n if (process.platform === 'darwin' && /^en[1-9]\\d*$/.test(n)) return 'wifi'\n return 'lan'\n }\n if (address === '127.0.0.1' || address === '::1') return 'loopback'\n return 'other'\n}\n\n/** Convert an IPv4/IPv6 netmask string to its prefix length (CIDR). */\nexport function prefixLen(netmask: string): number {\n if (!netmask) return 0\n if (netmask.includes(':')) {\n let bits = 0\n for (const group of netmask.split(':')) {\n if (!group) continue\n const n = parseInt(group, 16)\n if (!Number.isFinite(n)) break\n for (let mask = 0x8000; mask; mask >>= 1) {\n if (n & mask) bits++\n else return bits\n }\n }\n return bits\n }\n let bits = 0\n for (const part of netmask.split('.')) {\n const n = parseInt(part, 10)\n if (!Number.isFinite(n)) break\n for (let mask = 0x80; mask; mask >>= 1) {\n if (n & mask) bits++\n else return bits\n }\n }\n return bits\n}\n\nfunction snapshotKey(interfaces: readonly LocalInterface[]): string {\n return [...interfaces]\n .map((i) => `${i.name}|${i.family}|${i.address}|${i.netmask}`)\n .sort()\n .join('\\n')\n}\n\nfunction formatKind(kind: LocalInterface['kind']): string {\n switch (kind) {\n case 'lan': return 'LAN'\n case 'wifi': return 'Wi-Fi'\n case 'vpn': return 'VPN'\n case 'docker': return 'Docker'\n case 'loopback': return 'Loopback'\n case 'other': return 'Other'\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"local-network.addon.mjs","names":[],"sources":["../../../src/builtins/local-network/local-network.addon.ts"],"sourcesContent":["/**\n * local-network — hub-only system cap implementation.\n *\n * Wraps `os.networkInterfaces()` with:\n * - Coarse kind classification (lan/wifi/docker/vpn/loopback/other)\n * - Auto-select heuristic for the outbound interface\n * - Periodic poll (30s) + diff to emit `LocalNetworkChanged` events\n * so subscribers can react to DHCP renewals, VPN connect, etc.\n * - `getConnectionEndpoints()` — ranked URL list for SDK clients\n *\n * Does NOT poll the cloudflare-tunnel state directly; the public\n * tunnel hostname (if any) is provided by `network-access` consumers\n * via `NetworkTunnelStarted/Stopped` events on the bus, so the cap\n * stays free of cross-addon dependencies.\n */\nimport * as os from 'node:os'\n\nimport type {\n ProviderRegistration,\n ILocalNetworkProvider,\n LocalInterface,\n ConnectionEndpoint,\n} from '@camstack/types'\nimport { BaseAddon, localNetworkCapability, EventCategory } from '@camstack/types'\n\ninterface RawIface {\n readonly name: string\n readonly family: 'IPv4' | 'IPv6'\n readonly address: string\n readonly cidr: string\n readonly netmask: string\n readonly internal: boolean\n readonly mac: string\n}\n\nconst POLL_INTERVAL_MS = 30_000\n\ninterface LocalNetworkConfig {\n /** Empty = \"auto\" (every non-loopback / non-link-local address\n * participates). Non-empty restricts the candidate set to only\n * those operator-pinned addresses. */\n readonly allowedAddresses: readonly string[]\n /** Sentinel — `true` after the first-boot auto-seed runs. Lets the\n * addon distinguish \"fresh install, never touched\" (false → seed\n * with LAN/Wi-Fi addresses) from \"operator explicitly cleared the\n * allowlist\" (true + empty → respect operator's choice). */\n readonly bootSeeded: boolean\n}\n\nexport class LocalNetworkAddon extends BaseAddon<LocalNetworkConfig> {\n private pollTimer: NodeJS.Timeout | null = null\n private lastSnapshotKey = ''\n /** Optional public hostname tracked from `NetworkTunnelStarted`/\n * `Stopped` events on the bus. */\n private publicHostname = ''\n\n constructor() {\n super({ allowedAddresses: [], bootSeeded: false })\n }\n\n protected async onInitialize(): Promise<ProviderRegistration[]> {\n // First-boot auto-seed: on fresh installs, pre-populate the\n // allowlist with addresses from interfaces classified as `lan` or\n // `wifi`. Operators usually want those participating; docker/vpn\n // entries stay opt-in. Loopback is always implicit (no need to\n // include it explicitly).\n if (!this.config.bootSeeded) {\n const seed = autoSeedAllowlist(this.enumerate())\n await this.updateGlobalSettings({ allowedAddresses: seed, bootSeeded: true })\n this.ctx.logger.info('local-network: first-boot auto-seed', {\n meta: { addresses: seed },\n })\n }\n const provider: ILocalNetworkProvider = {\n list: async () => ({\n interfaces: this.enumerate(),\n probedAt: Date.now(),\n }),\n getPreferred: async () => pickPreferred(\n applyAllowlist(this.enumerate(), this.config.allowedAddresses),\n ),\n getConnectionEndpoints: async (input) => {\n const includeLoopback = input.includeLoopback ?? true\n const ipv4Only = input.ipv4Only ?? false\n const scheme = input.scheme ?? 'http'\n const allow = this.config.allowedAddresses\n const interfaces = applyAllowlist(this.enumerate(), allow)\n return {\n endpoints: buildEndpoints(interfaces, input.port, includeLoopback, ipv4Only, this.publicHostname, scheme),\n }\n },\n getAllowedAddresses: async () => ({ addresses: this.config.allowedAddresses }),\n resetAllowlistToBestMatch: async () => {\n const seed = autoSeedAllowlist(this.enumerate())\n await this.updateGlobalSettings({ allowedAddresses: seed, bootSeeded: true })\n this.ctx.logger.info('local-network: allowlist reset to auto-seed', {\n meta: { count: seed.length },\n })\n return { addresses: seed }\n },\n setAllowedAddresses: async ({ addresses }) => {\n // Validate against the current snapshot — drop anything that\n // doesn't actually exist on this host. Prevents an admin from\n // saving a typo'd address that would then silently filter out\n // every real candidate.\n const known = new Set(this.enumerate().map((i) => i.address))\n const cleaned: readonly string[] = [...new Set(addresses)].filter((a) => known.has(a))\n await this.updateGlobalSettings({ allowedAddresses: cleaned })\n this.ctx.logger.info('local-network: allowlist updated', {\n meta: { count: cleaned.length, dropped: addresses.length - cleaned.length },\n })\n return { success: true as const }\n },\n }\n\n // Seed the snapshot key so the first poll doesn't fire a false\n // change event on boot.\n this.lastSnapshotKey = snapshotKey(this.enumerate())\n\n this.pollTimer = setInterval(() => this.detectChanges(), POLL_INTERVAL_MS)\n this.pollTimer.unref?.()\n\n // Track the active public hostname by tailing the network-access\n // lifecycle events that every tunnel provider already emits. We\n // can't import cross-package so the parsing stays defensive.\n this.ctx.eventBus?.subscribe(\n { category: EventCategory.NetworkTunnelStarted },\n (event) => {\n const data = (event.data ?? {}) as { url?: unknown }\n if (typeof data.url === 'string') {\n try {\n const hostname = new URL(data.url).hostname\n // Ignore the pre-FQDN placeholders the tunnel emits before\n // cloudflared's stdout reports the real *.trycloudflare.com.\n if (hostname && !hostname.endsWith('.placeholder')\n && !hostname.startsWith('pending.')) {\n this.setPublicHostname(hostname)\n }\n } catch {\n // Malformed URL — leave the cached hostname as-is.\n }\n }\n },\n )\n this.ctx.eventBus?.subscribe(\n { category: EventCategory.NetworkTunnelStopped },\n () => this.setPublicHostname(''),\n )\n\n this.ctx.logger.info('local-network initialized', {\n meta: { interfaceCount: this.enumerate().length },\n })\n\n return [{ capability: localNetworkCapability, provider }]\n }\n\n protected async onShutdown(): Promise<void> {\n if (this.pollTimer) {\n clearInterval(this.pollTimer)\n this.pollTimer = null\n }\n }\n\n /**\n * Other hub addons (e.g. cloudflare-tunnel) signal the active public\n * FQDN by emitting `NetworkTunnelStarted` on the bus — handled in\n * `onInitialize`. This setter exists for tests + future direct\n * callers; cleared by passing an empty string.\n */\n setPublicHostname(hostname: string): void {\n if (this.publicHostname === hostname) return\n this.publicHostname = hostname\n this.ctx.logger.info('local-network: public hostname updated', {\n meta: { hostname: hostname || '(cleared)' },\n })\n }\n\n // ── Internals ──────────────────────────────────────────────────────\n\n private enumerate(): readonly LocalInterface[] {\n return enumerateOsInterfaces(os.networkInterfaces())\n }\n\n private detectChanges(): void {\n const interfaces = this.enumerate()\n const key = snapshotKey(interfaces)\n if (key === this.lastSnapshotKey) return\n\n this.ctx.logger.info('local-network: interface set changed', {\n meta: { count: interfaces.length, key },\n })\n this.lastSnapshotKey = key\n\n this.ctx.eventBus?.emit({\n id: `local-network-changed-${Date.now()}`,\n timestamp: new Date(),\n source: { type: 'core', id: 'local-network' },\n category: EventCategory.LocalNetworkChanged,\n data: { count: interfaces.length },\n })\n }\n}\n\n// ── Pure helpers (exported for tests + reusable by other consumers) ──\n\n/**\n * Lift `os.networkInterfaces()` into our `LocalInterface[]` shape with\n * kind classification + preferred flag. Pure — takes the raw result so\n * tests can feed synthetic data.\n */\ninterface OsNetworkInterfaceInfo {\n readonly family: string\n readonly address: string\n readonly cidr?: string | null\n readonly netmask: string\n readonly internal: boolean\n readonly mac: string\n}\n\nexport function enumerateOsInterfaces(\n ifaces: NodeJS.Dict<readonly OsNetworkInterfaceInfo[]>,\n): readonly LocalInterface[] {\n const raw: RawIface[] = []\n for (const [name, addrs] of Object.entries(ifaces)) {\n if (!addrs) continue\n for (const a of addrs) {\n if (a.family !== 'IPv4' && a.family !== 'IPv6') continue\n raw.push({\n name,\n family: a.family,\n address: a.address,\n cidr: a.cidr ?? '',\n netmask: a.netmask,\n internal: a.internal,\n mac: a.mac,\n })\n }\n }\n\n const classified: LocalInterface[] = raw.map((r) => {\n const kind = classifyKind(r.name, r.address, r.internal)\n // \"Plausible\" = passes the RFC range gate AND comes from an\n // interface kind a client could realistically reach. Operators\n // can still pin a docker / vpn address — we just mark it visually\n // so the choice is intentional.\n const reachableKind = kind === 'lan' || kind === 'wifi'\n const plausible = !r.internal\n && reachableKind\n && isPlausibleAutoSeed(r.address, r.family)\n const plausibleReason = plausible\n ? ''\n : explainNonPlausible({ kind, family: r.family, address: r.address, internal: r.internal })\n return {\n name: r.name,\n family: r.family,\n address: r.address,\n cidr: r.cidr,\n netmask: r.netmask,\n internal: r.internal,\n mac: r.mac,\n kind,\n preferred: false,\n plausible,\n plausibleReason,\n }\n })\n\n const preferred = pickPreferred(classified)\n return classified.map((iface) => ({\n ...iface,\n preferred: preferred !== null\n && iface.name === preferred.name\n && iface.address === preferred.address,\n }))\n}\n\n/**\n * Filter the interface list by the operator's allowlist. Empty\n * allowlist = no-op (every interface passes); otherwise only entries\n * whose `address` matches an allowlist entry remain. Loopback always\n * survives so the SDK keeps `127.0.0.1` as the last-resort fallback.\n */\nexport function applyAllowlist(\n interfaces: readonly LocalInterface[],\n allowed: readonly string[],\n): readonly LocalInterface[] {\n if (allowed.length === 0) return interfaces\n const set = new Set(allowed)\n return interfaces.filter((i) => i.kind === 'loopback' || set.has(i.address))\n}\n\n/**\n * Rank interfaces and pick the auto-selected outbound one. See the\n * cap's `getPreferred` doc for the heuristic.\n */\nexport function pickPreferred(interfaces: readonly LocalInterface[]): LocalInterface | null {\n const candidates = interfaces.filter((i) =>\n !i.internal\n && i.family === 'IPv4'\n && !i.address.startsWith('169.254.') // RFC 3927 link-local\n && i.kind !== 'loopback',\n )\n if (candidates.length === 0) return null\n\n const kindRank: Record<LocalInterface['kind'], number> = {\n lan: 1, wifi: 2, vpn: 3, docker: 4, other: 5, loopback: 99,\n }\n const sorted = [...candidates].sort((a, b) => {\n const ra = kindRank[a.kind]\n const rb = kindRank[b.kind]\n if (ra !== rb) return ra - rb\n // Tie-break on netmask length — `/24` beats `/16`.\n return prefixLen(b.netmask) - prefixLen(a.netmask)\n })\n return sorted[0] ?? null\n}\n\n/**\n * Build the ordered candidate URL list. Priority schema:\n * 0 — preferred LAN IPv4\n * 10+ — other LAN IPv4\n * 100 — public tunnel hostname (always HTTPS)\n * 200+ — LAN IPv6\n * 1000 — loopback (last resort)\n *\n * `scheme` controls LAN + loopback URLs. Browsers running over HTTPS\n * block `http://` candidates as mixed content, so callers loaded over\n * HTTPS should pass `scheme: 'https'` even when probing a LAN IP — the\n * hub's cert manager already issues a SAN-multi cert covering local\n * interfaces. The public tunnel always emits `https://` (Cloudflare\n * terminates TLS for us).\n */\nexport function buildEndpoints(\n interfaces: readonly LocalInterface[],\n port: number,\n includeLoopback: boolean,\n ipv4Only: boolean,\n publicHostname: string,\n scheme: 'http' | 'https' = 'http',\n): ConnectionEndpoint[] {\n const out: ConnectionEndpoint[] = []\n let priority = 0\n const preferred = pickPreferred(interfaces)\n\n const emit = (\n iface: LocalInterface,\n kind: ConnectionEndpoint['kind'],\n label: string,\n baseUrl: string,\n pri: number,\n ): void => {\n out.push({\n label,\n baseUrl,\n kind,\n interfaceKind: iface.kind,\n plausible: iface.plausible,\n plausibleReason: iface.plausibleReason,\n priority: pri,\n })\n }\n\n if (preferred) {\n emit(\n preferred,\n 'lan-ipv4',\n `${formatKind(preferred.kind)} — ${preferred.name}`,\n `${scheme}://${preferred.address}:${port}`,\n priority++,\n )\n }\n\n for (const iface of interfaces) {\n if (iface.internal || iface.family !== 'IPv4') continue\n if (iface.kind === 'loopback') continue\n if (preferred && iface.name === preferred.name && iface.address === preferred.address) continue\n if (iface.address.startsWith('169.254.')) continue\n emit(\n iface,\n 'lan-ipv4',\n `${formatKind(iface.kind)} — ${iface.name}`,\n `${scheme}://${iface.address}:${port}`,\n 10 + priority++,\n )\n }\n\n if (publicHostname) {\n out.push({\n label: 'Public tunnel',\n baseUrl: `https://${publicHostname}`,\n kind: 'public',\n interfaceKind: 'public',\n plausible: true,\n plausibleReason: '',\n priority: 100,\n })\n }\n\n if (!ipv4Only) {\n let v6prio = 200\n for (const iface of interfaces) {\n if (iface.internal || iface.family !== 'IPv6') continue\n if (iface.kind === 'loopback') continue\n if (iface.address.startsWith('fe80:')) continue // link-local\n emit(\n iface,\n 'lan-ipv6',\n `${formatKind(iface.kind)} — ${iface.name} (IPv6)`,\n `${scheme}://[${iface.address}]:${port}`,\n v6prio++,\n )\n }\n }\n\n if (includeLoopback) {\n out.push({\n label: 'Loopback',\n baseUrl: `${scheme}://127.0.0.1:${port}`,\n kind: 'loopback',\n interfaceKind: 'loopback',\n plausible: false,\n plausibleReason: 'Loopback — last-resort fallback when no other endpoint responds.',\n priority: 1000,\n })\n }\n\n return out.sort((a, b) => a.priority - b.priority)\n}\n\n/**\n * First-boot heuristic: which addresses should the allowlist start\n * with? Includes LAN + Wi-Fi IPv4 addresses + plausible IPv6:\n *\n * - **IPv4**: skip link-local (`169.254.*`), keep the rest.\n * - **IPv6**: skip link-local (`fe80::*`), unspecified, and\n * multicast. Keep ULAs (`fc00::/7` → `fc??:` / `fd??:`) and Global\n * Unicast addresses (`2000::/3` → `2???`/`3???`). Privacy-extension\n * temporary addresses get included by default; the operator can\n * prune them from the Network Addresses tab if the rotating IPs\n * become a nuisance.\n *\n * Skips docker/vpn/loopback/other entirely — those stay opt-in.\n */\nexport function autoSeedAllowlist(interfaces: readonly LocalInterface[]): string[] {\n return [...new Set(\n interfaces\n .filter((i) => !i.internal\n && (i.kind === 'lan' || i.kind === 'wifi')\n && isPlausibleAutoSeed(i.address, i.family))\n .map((i) => i.address),\n )]\n}\n\n/**\n * Per-interface tooltip text surfaced on the \"Unlikely usable\" badge.\n * Server-side so the UI doesn't re-derive the rationale (single source\n * of truth). Returns `''` for plausible entries; the addon overlays\n * this on the `LocalInterface.plausibleReason` field.\n */\nexport function explainNonPlausible(input: {\n readonly kind: LocalInterface['kind']\n readonly family: 'IPv4' | 'IPv6'\n readonly address: string\n readonly internal: boolean\n}): string {\n if (input.internal) return 'Internal interface (loopback) — not reachable from clients.'\n if (input.kind === 'docker') return 'Docker bridge — only reachable from inside the container network.'\n if (input.kind === 'vpn') return 'VPN tunnel — only reachable while the VPN is connected.'\n if (input.kind === 'other') return 'Unrecognised interface kind — verify reachability before pinning.'\n if (input.family === 'IPv6') {\n const a = input.address.toLowerCase()\n if (a.startsWith('fe80:')) return 'IPv6 link-local — only reachable on the same link, not routed.'\n if (a.startsWith('ff')) return 'IPv6 multicast — not a unicast address.'\n if (a === '::' || a === '::1') return 'IPv6 loopback / unspecified — not a public address.'\n return 'IPv6 address outside the ULA / GUA ranges — verify before pinning.'\n }\n if (input.address.startsWith('169.254.')) return 'IPv4 link-local (RFC 3927) — only valid when DHCP fails.'\n return 'Address looks unusual for client traffic — verify before pinning.'\n}\n\n/**\n * Per-address gate used by `autoSeedAllowlist`. Exposed for tests so\n * we can pin every classification rule without standing up the addon.\n */\nexport function isPlausibleAutoSeed(address: string, family: 'IPv4' | 'IPv6'): boolean {\n if (family === 'IPv4') {\n if (address.startsWith('169.254.')) return false // RFC 3927 link-local\n return true\n }\n // IPv6 — normalise to lower-case once.\n const a = address.toLowerCase()\n if (a === '::' || a === '::1') return false\n if (a.startsWith('fe80:')) return false // link-local\n if (a.startsWith('ff')) return false // multicast (ff00::/8)\n // ULA (fc00::/7 → first hex group starts with `fc` or `fd`).\n if (/^f[cd][0-9a-f]{0,2}:/.test(a)) return true\n // GUA (2000::/3 → first nibble 2 or 3).\n if (/^[23][0-9a-f]{0,3}:/.test(a)) return true\n // Anything else (deprecated / experimental ranges) — leave opt-in.\n return false\n}\n\nexport function classifyKind(\n name: string,\n address: string,\n internal: boolean,\n): LocalInterface['kind'] {\n if (internal || name === 'lo' || name.startsWith('lo')) return 'loopback'\n const n = name.toLowerCase()\n if (n.startsWith('docker') || n.startsWith('br-') || n.startsWith('veth')) return 'docker'\n if (n.startsWith('tun') || n.startsWith('utun') || n.startsWith('wg') || n.startsWith('tap')) return 'vpn'\n if (n.startsWith('wlan') || n.startsWith('wlp') || n.startsWith('wlx')) return 'wifi'\n if (n.startsWith('eth') || /^en\\d+$/.test(n)) {\n // macOS en1+ is typically wifi; en0 is wired. Default heuristic.\n if (process.platform === 'darwin' && /^en[1-9]\\d*$/.test(n)) return 'wifi'\n return 'lan'\n }\n if (address === '127.0.0.1' || address === '::1') return 'loopback'\n return 'other'\n}\n\n/** Convert an IPv4/IPv6 netmask string to its prefix length (CIDR). */\nexport function prefixLen(netmask: string): number {\n if (!netmask) return 0\n if (netmask.includes(':')) {\n let bits = 0\n for (const group of netmask.split(':')) {\n if (!group) continue\n const n = parseInt(group, 16)\n if (!Number.isFinite(n)) break\n for (let mask = 0x8000; mask; mask >>= 1) {\n if (n & mask) bits++\n else return bits\n }\n }\n return bits\n }\n let bits = 0\n for (const part of netmask.split('.')) {\n const n = parseInt(part, 10)\n if (!Number.isFinite(n)) break\n for (let mask = 0x80; mask; mask >>= 1) {\n if (n & mask) bits++\n else return bits\n }\n }\n return bits\n}\n\nfunction snapshotKey(interfaces: readonly LocalInterface[]): string {\n return [...interfaces]\n .map((i) => `${i.name}|${i.family}|${i.address}|${i.netmask}`)\n .sort()\n .join('\\n')\n}\n\nfunction formatKind(kind: LocalInterface['kind']): string {\n switch (kind) {\n case 'lan': return 'LAN'\n case 'wifi': return 'Wi-Fi'\n case 'vpn': return 'VPN'\n case 'docker': return 'Docker'\n case 'loopback': return 'Loopback'\n case 'other': return 'Other'\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/core",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
5
5
  "keywords": [
6
6
  "camstack",