@hermespilot/link 0.2.7 → 0.2.9
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.
- package/dist/{chunk-SSJ7WDD7.js → chunk-VLTX75SY.js} +862 -373
- package/dist/cli/index.js +179 -14
- package/dist/http/app.js +1 -1
- package/package.json +1 -1
|
@@ -3724,7 +3724,7 @@ import os2 from "os";
|
|
|
3724
3724
|
import path5 from "path";
|
|
3725
3725
|
|
|
3726
3726
|
// src/constants.ts
|
|
3727
|
-
var LINK_VERSION = "0.2.
|
|
3727
|
+
var LINK_VERSION = "0.2.9";
|
|
3728
3728
|
var LINK_COMMAND = "hermeslink";
|
|
3729
3729
|
var LINK_DEFAULT_PORT = 52379;
|
|
3730
3730
|
var LINK_RUNTIME_DIR_NAME = ".hermeslink";
|
|
@@ -6541,6 +6541,7 @@ function readAttachmentWaveform(attachment) {
|
|
|
6541
6541
|
// src/config/config.ts
|
|
6542
6542
|
var defaultLinkConfig = {
|
|
6543
6543
|
port: LINK_DEFAULT_PORT,
|
|
6544
|
+
lanHost: null,
|
|
6544
6545
|
serverBaseUrl: "https://hermes-server.clawpilot.me",
|
|
6545
6546
|
relayBaseUrl: "https://hermes-relay.clawpilot.me",
|
|
6546
6547
|
appConnectTokenIssuer: "https://hermes-server.clawpilot.me",
|
|
@@ -6550,18 +6551,51 @@ var defaultLinkConfig = {
|
|
|
6550
6551
|
async function loadConfig(paths = resolveRuntimePaths()) {
|
|
6551
6552
|
const existing = await readJsonFile(paths.configFile);
|
|
6552
6553
|
const language = normalizeConfiguredLanguage(existing?.language);
|
|
6554
|
+
const lanHost = normalizeLanHost(existing?.lanHost);
|
|
6553
6555
|
return {
|
|
6554
6556
|
...defaultLinkConfig,
|
|
6555
6557
|
...existing ?? {},
|
|
6556
|
-
language
|
|
6558
|
+
language,
|
|
6559
|
+
lanHost
|
|
6557
6560
|
};
|
|
6558
6561
|
}
|
|
6562
|
+
async function saveConfig(patch, paths = resolveRuntimePaths()) {
|
|
6563
|
+
const current = await loadConfig(paths);
|
|
6564
|
+
const next = { ...current, ...patch };
|
|
6565
|
+
await writeJsonFile(paths.configFile, next);
|
|
6566
|
+
return next;
|
|
6567
|
+
}
|
|
6559
6568
|
function normalizeConfiguredLanguage(language) {
|
|
6560
6569
|
if (language === "zh-CN" || language === "en" || language === "auto") {
|
|
6561
6570
|
return language;
|
|
6562
6571
|
}
|
|
6563
6572
|
return defaultLinkConfig.language;
|
|
6564
6573
|
}
|
|
6574
|
+
function normalizeLanHost(value) {
|
|
6575
|
+
if (value === null || value === void 0) {
|
|
6576
|
+
return null;
|
|
6577
|
+
}
|
|
6578
|
+
if (typeof value !== "string") {
|
|
6579
|
+
return null;
|
|
6580
|
+
}
|
|
6581
|
+
const host = value.trim().replace(/^\[/u, "").replace(/\]$/u, "");
|
|
6582
|
+
if (!host) {
|
|
6583
|
+
return null;
|
|
6584
|
+
}
|
|
6585
|
+
if (!isUsableLanIpv4(host)) {
|
|
6586
|
+
return null;
|
|
6587
|
+
}
|
|
6588
|
+
return host;
|
|
6589
|
+
}
|
|
6590
|
+
function isUsableLanIpv4(value) {
|
|
6591
|
+
const parts = value.split(".").map((part) => Number.parseInt(part, 10));
|
|
6592
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
6593
|
+
return false;
|
|
6594
|
+
}
|
|
6595
|
+
const [first, second, , fourth] = parts;
|
|
6596
|
+
const privateRange = first === 10 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168;
|
|
6597
|
+
return privateRange && fourth !== 0 && fourth !== 255;
|
|
6598
|
+
}
|
|
6565
6599
|
|
|
6566
6600
|
// src/hermes/session-title.ts
|
|
6567
6601
|
import { stat as stat6 } from "fs/promises";
|
|
@@ -11709,7 +11743,10 @@ async function saveAssignedLinkId(linkId, paths = resolveRuntimePaths()) {
|
|
|
11709
11743
|
return next;
|
|
11710
11744
|
}
|
|
11711
11745
|
function signRelayNonce(identity, nonce) {
|
|
11712
|
-
|
|
11746
|
+
return signIdentityPayload(identity, nonce);
|
|
11747
|
+
}
|
|
11748
|
+
function signIdentityPayload(identity, payload) {
|
|
11749
|
+
const signature = sign(null, Buffer.from(payload, "utf8"), identity.private_key_pem);
|
|
11713
11750
|
return signature.toString("base64url");
|
|
11714
11751
|
}
|
|
11715
11752
|
function getIdentityStatus(identity) {
|
|
@@ -12447,6 +12484,8 @@ function registerConversationRoutes(router, options) {
|
|
|
12447
12484
|
});
|
|
12448
12485
|
router.get("/api/v1/conversations/events", async (ctx) => {
|
|
12449
12486
|
await authenticateRequest(ctx, paths);
|
|
12487
|
+
const mode = readQueryString(ctx.query.mode);
|
|
12488
|
+
const notificationOnly = mode === "notifications";
|
|
12450
12489
|
ctx.respond = false;
|
|
12451
12490
|
const response = ctx.res;
|
|
12452
12491
|
response.statusCode = 200;
|
|
@@ -12454,6 +12493,9 @@ function registerConversationRoutes(router, options) {
|
|
|
12454
12493
|
response.setHeader("cache-control", "no-store");
|
|
12455
12494
|
response.setHeader("connection", "keep-alive");
|
|
12456
12495
|
const unsubscribe = conversations.subscribeAll((event) => {
|
|
12496
|
+
if (notificationOnly && !isConversationNotificationEvent(event)) {
|
|
12497
|
+
return;
|
|
12498
|
+
}
|
|
12457
12499
|
writeSseEvent(response, event);
|
|
12458
12500
|
});
|
|
12459
12501
|
const cleanup = () => {
|
|
@@ -12691,6 +12733,16 @@ function registerConversationRoutes(router, options) {
|
|
|
12691
12733
|
}
|
|
12692
12734
|
);
|
|
12693
12735
|
}
|
|
12736
|
+
function isConversationNotificationEvent(event) {
|
|
12737
|
+
const type = event.type.toLowerCase();
|
|
12738
|
+
return type === "conversation.created" || type === "conversation.updated" || type === "conversation.deleted" || type === "message.created" || type === "message.completed" || type === "message.failed" || type === "run.completed" || type === "run.failed" || type === "run.cancelled" || type === "run.canceled" || type === "approval.requested" || readPayloadBool(event.payload, "requires_action") || readPayloadBool(event.payload, "requires_user_action") || readPayloadBool(event.payload, "requires_approval");
|
|
12739
|
+
}
|
|
12740
|
+
function readPayloadBool(payload, key) {
|
|
12741
|
+
if (!payload || typeof payload !== "object") {
|
|
12742
|
+
return false;
|
|
12743
|
+
}
|
|
12744
|
+
return payload[key] === true;
|
|
12745
|
+
}
|
|
12694
12746
|
|
|
12695
12747
|
// src/http/middleware/error-handler.ts
|
|
12696
12748
|
function createHttpErrorMiddleware(logger) {
|
|
@@ -17573,218 +17625,807 @@ async function handleFrame(socket, raw, localPort, abortControllers) {
|
|
|
17573
17625
|
}
|
|
17574
17626
|
}
|
|
17575
17627
|
|
|
17576
|
-
// src/
|
|
17577
|
-
|
|
17578
|
-
|
|
17579
|
-
|
|
17580
|
-
|
|
17581
|
-
|
|
17582
|
-
|
|
17583
|
-
|
|
17584
|
-
|
|
17585
|
-
await syncHermesLinkCronDeliveries(
|
|
17586
|
-
options.paths,
|
|
17587
|
-
options.conversations,
|
|
17588
|
-
options.logger
|
|
17589
|
-
);
|
|
17590
|
-
} catch (error) {
|
|
17591
|
-
void options.logger.warn("cron_link_delivery_sync_failed", {
|
|
17592
|
-
error: error instanceof Error ? error.message : String(error)
|
|
17593
|
-
});
|
|
17594
|
-
} finally {
|
|
17595
|
-
running = false;
|
|
17596
|
-
}
|
|
17597
|
-
};
|
|
17598
|
-
const timer = setInterval(() => {
|
|
17599
|
-
void syncCronDeliveries();
|
|
17600
|
-
}, options.intervalMs ?? 3e4);
|
|
17601
|
-
timer.unref?.();
|
|
17628
|
+
// src/runtime/system-info.ts
|
|
17629
|
+
import { execFileSync } from "child_process";
|
|
17630
|
+
import { readFileSync } from "fs";
|
|
17631
|
+
import os5 from "os";
|
|
17632
|
+
function readLinkSystemInfo() {
|
|
17633
|
+
const platform = process.platform;
|
|
17634
|
+
const hostname = readHostname(platform);
|
|
17635
|
+
const osLabel = readOsLabel(platform);
|
|
17636
|
+
const defaultDisplayName = buildDefaultDisplayName({ hostname, osLabel, platform });
|
|
17602
17637
|
return {
|
|
17603
|
-
|
|
17604
|
-
|
|
17605
|
-
|
|
17638
|
+
platform,
|
|
17639
|
+
hostname,
|
|
17640
|
+
osLabel,
|
|
17641
|
+
defaultDisplayName
|
|
17606
17642
|
};
|
|
17607
17643
|
}
|
|
17608
|
-
|
|
17609
|
-
|
|
17610
|
-
|
|
17611
|
-
|
|
17612
|
-
|
|
17613
|
-
const [identity, config] = await Promise.all([loadIdentity(paths), loadConfig(paths)]);
|
|
17614
|
-
await logger.info("service_starting", {
|
|
17615
|
-
port: config.port,
|
|
17616
|
-
mode: identity?.link_id ? "paired" : "local-only"
|
|
17617
|
-
});
|
|
17618
|
-
const migration = await migrateLinkDatabase(paths);
|
|
17619
|
-
if (migration.appliedVersions.length > 0) {
|
|
17620
|
-
await logger.info("database_migrated", {
|
|
17621
|
-
database_file: migration.databaseFile,
|
|
17622
|
-
applied_versions: migration.appliedVersions,
|
|
17623
|
-
current_version: migration.currentVersion
|
|
17624
|
-
});
|
|
17644
|
+
function buildDefaultDisplayName(input) {
|
|
17645
|
+
const hostname = normalizeText(input.hostname);
|
|
17646
|
+
const osLabel = normalizeText(input.osLabel);
|
|
17647
|
+
if (hostname && osLabel && hostname.toLowerCase() !== osLabel.toLowerCase()) {
|
|
17648
|
+
return truncateText(`${hostname} - ${osLabel}`, 128);
|
|
17625
17649
|
}
|
|
17626
|
-
|
|
17627
|
-
|
|
17628
|
-
|
|
17629
|
-
|
|
17630
|
-
|
|
17631
|
-
|
|
17632
|
-
|
|
17633
|
-
|
|
17634
|
-
|
|
17635
|
-
|
|
17636
|
-
await listenServer(server, config.port);
|
|
17637
|
-
} catch (error) {
|
|
17638
|
-
await logger.error("service_start_failed", {
|
|
17639
|
-
port: config.port,
|
|
17640
|
-
error: error instanceof Error ? error.message : String(error)
|
|
17641
|
-
});
|
|
17642
|
-
await logger.flush();
|
|
17643
|
-
throw error;
|
|
17650
|
+
return truncateText(hostname ?? osLabel ?? `Hermes Link ${input.platform}`, 128);
|
|
17651
|
+
}
|
|
17652
|
+
function parseLinuxOsRelease(content) {
|
|
17653
|
+
const values = /* @__PURE__ */ new Map();
|
|
17654
|
+
for (const line of content.split(/\r?\n/u)) {
|
|
17655
|
+
const match = /^([A-Z0-9_]+)=(.*)$/u.exec(line.trim());
|
|
17656
|
+
if (!match) {
|
|
17657
|
+
continue;
|
|
17658
|
+
}
|
|
17659
|
+
values.set(match[1], unquoteOsReleaseValue(match[2]));
|
|
17644
17660
|
}
|
|
17645
|
-
|
|
17646
|
-
|
|
17647
|
-
|
|
17648
|
-
|
|
17649
|
-
|
|
17650
|
-
|
|
17651
|
-
|
|
17652
|
-
|
|
17653
|
-
paths,
|
|
17654
|
-
conversations,
|
|
17655
|
-
logger
|
|
17656
|
-
});
|
|
17657
|
-
let relay = null;
|
|
17658
|
-
if (identity?.link_id) {
|
|
17659
|
-
relay = connectRelayControl({
|
|
17660
|
-
relayBaseUrl: config.relayBaseUrl,
|
|
17661
|
-
linkId: identity.link_id,
|
|
17662
|
-
localPort: config.port,
|
|
17663
|
-
maxReconnectAttempts: options.relayMaxReconnectAttempts ?? 5,
|
|
17664
|
-
backoffBaseMs: 1e3,
|
|
17665
|
-
backoffMaxMs: 3e4,
|
|
17666
|
-
onStatus: (status) => {
|
|
17667
|
-
void logger.info("relay_status", status);
|
|
17668
|
-
}
|
|
17669
|
-
});
|
|
17670
|
-
} else {
|
|
17671
|
-
void logger.info("relay_skipped", { reason: "link_not_paired" });
|
|
17661
|
+
return normalizeText(values.get("PRETTY_NAME")) ?? buildLinuxName(values);
|
|
17662
|
+
}
|
|
17663
|
+
function readHostname(platform) {
|
|
17664
|
+
if (platform === "darwin") {
|
|
17665
|
+
const computerName = readCommandOutput("scutil", ["--get", "ComputerName"]);
|
|
17666
|
+
if (computerName) {
|
|
17667
|
+
return computerName;
|
|
17668
|
+
}
|
|
17672
17669
|
}
|
|
17673
|
-
|
|
17674
|
-
|
|
17670
|
+
return normalizeText(os5.hostname());
|
|
17671
|
+
}
|
|
17672
|
+
function readOsLabel(platform) {
|
|
17673
|
+
if (platform === "darwin") {
|
|
17674
|
+
const version = readCommandOutput("sw_vers", ["-productVersion"]);
|
|
17675
|
+
return version ? `macOS ${version}` : "macOS";
|
|
17675
17676
|
}
|
|
17676
|
-
|
|
17677
|
-
|
|
17678
|
-
|
|
17679
|
-
|
|
17680
|
-
|
|
17681
|
-
|
|
17682
|
-
|
|
17683
|
-
|
|
17684
|
-
|
|
17685
|
-
|
|
17677
|
+
if (platform === "linux") {
|
|
17678
|
+
return readLinuxOsRelease() ?? `Linux ${os5.release()}`;
|
|
17679
|
+
}
|
|
17680
|
+
if (platform === "win32") {
|
|
17681
|
+
return `Windows ${os5.release()}`;
|
|
17682
|
+
}
|
|
17683
|
+
return `${os5.type()} ${os5.release()}`.trim();
|
|
17684
|
+
}
|
|
17685
|
+
function readLinuxOsRelease() {
|
|
17686
|
+
for (const file of ["/etc/os-release", "/usr/lib/os-release"]) {
|
|
17687
|
+
try {
|
|
17688
|
+
return parseLinuxOsRelease(readFileSync(file, "utf8"));
|
|
17689
|
+
} catch {
|
|
17686
17690
|
}
|
|
17687
|
-
}
|
|
17691
|
+
}
|
|
17692
|
+
return null;
|
|
17688
17693
|
}
|
|
17689
|
-
function
|
|
17690
|
-
|
|
17694
|
+
function readCommandOutput(command, args) {
|
|
17695
|
+
try {
|
|
17696
|
+
const output = execFileSync(command, args, {
|
|
17697
|
+
encoding: "utf8",
|
|
17698
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
17699
|
+
timeout: 1e3
|
|
17700
|
+
});
|
|
17701
|
+
return normalizeText(output);
|
|
17702
|
+
} catch {
|
|
17703
|
+
return null;
|
|
17704
|
+
}
|
|
17691
17705
|
}
|
|
17692
|
-
|
|
17693
|
-
|
|
17694
|
-
|
|
17695
|
-
|
|
17706
|
+
function buildLinuxName(values) {
|
|
17707
|
+
const name = normalizeText(values.get("NAME"));
|
|
17708
|
+
const version = normalizeText(values.get("VERSION_ID")) ?? normalizeText(values.get("VERSION"));
|
|
17709
|
+
if (name && version) {
|
|
17710
|
+
return `${name} ${version}`;
|
|
17711
|
+
}
|
|
17712
|
+
return name ?? version;
|
|
17696
17713
|
}
|
|
17697
|
-
|
|
17698
|
-
|
|
17699
|
-
|
|
17700
|
-
|
|
17701
|
-
|
|
17702
|
-
|
|
17703
|
-
if (settled) {
|
|
17704
|
-
return;
|
|
17705
|
-
}
|
|
17706
|
-
settled = true;
|
|
17707
|
-
clearTimeout(forceCloseTimer);
|
|
17708
|
-
clearTimeout(timeoutTimer);
|
|
17709
|
-
if (error) {
|
|
17710
|
-
reject(error);
|
|
17711
|
-
return;
|
|
17712
|
-
}
|
|
17713
|
-
resolve();
|
|
17714
|
-
};
|
|
17715
|
-
forceCloseTimer = setTimeout(() => {
|
|
17716
|
-
server.closeIdleConnections?.();
|
|
17717
|
-
server.closeAllConnections?.();
|
|
17718
|
-
}, 250);
|
|
17719
|
-
timeoutTimer = setTimeout(() => {
|
|
17720
|
-
server.closeAllConnections?.();
|
|
17721
|
-
settle();
|
|
17722
|
-
}, 5e3);
|
|
17723
|
-
server.close((error) => {
|
|
17724
|
-
if (error) {
|
|
17725
|
-
settle(error);
|
|
17726
|
-
return;
|
|
17727
|
-
}
|
|
17728
|
-
settle();
|
|
17729
|
-
});
|
|
17730
|
-
server.closeIdleConnections?.();
|
|
17731
|
-
});
|
|
17714
|
+
function unquoteOsReleaseValue(value) {
|
|
17715
|
+
const trimmed = value.trim();
|
|
17716
|
+
if (trimmed.length >= 2 && (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
17717
|
+
return trimmed.slice(1, -1).replace(/\\(["'`$\\])/gu, "$1");
|
|
17718
|
+
}
|
|
17719
|
+
return trimmed;
|
|
17732
17720
|
}
|
|
17733
|
-
|
|
17734
|
-
|
|
17735
|
-
|
|
17736
|
-
|
|
17737
|
-
|
|
17738
|
-
|
|
17739
|
-
|
|
17740
|
-
|
|
17741
|
-
|
|
17721
|
+
function normalizeText(value) {
|
|
17722
|
+
const normalized = value?.replace(/\s+/gu, " ").trim();
|
|
17723
|
+
return normalized ? normalized : null;
|
|
17724
|
+
}
|
|
17725
|
+
function truncateText(value, maxLength) {
|
|
17726
|
+
return value.length > maxLength ? value.slice(0, maxLength).trimEnd() : value;
|
|
17727
|
+
}
|
|
17728
|
+
|
|
17729
|
+
// src/topology/network.ts
|
|
17730
|
+
import os7 from "os";
|
|
17731
|
+
|
|
17732
|
+
// src/topology/environment.ts
|
|
17733
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
17734
|
+
import os6 from "os";
|
|
17735
|
+
function detectRuntimeEnvironment(env = process.env) {
|
|
17736
|
+
if (isWsl(env)) {
|
|
17737
|
+
return {
|
|
17738
|
+
kind: "wsl",
|
|
17739
|
+
lanAutoDiscoveryUsable: false,
|
|
17740
|
+
warning: "Detected WSL. The LAN IP found inside WSL is usually a private VM address and is not reachable from your phone. Use Relay or set `hermeslink config set lan-host <Windows LAN IP>`."
|
|
17742
17741
|
};
|
|
17743
|
-
|
|
17744
|
-
|
|
17745
|
-
|
|
17742
|
+
}
|
|
17743
|
+
if (isContainer(env)) {
|
|
17744
|
+
return {
|
|
17745
|
+
kind: "container",
|
|
17746
|
+
lanAutoDiscoveryUsable: false,
|
|
17747
|
+
warning: "Detected a container environment. Container LAN IPs are usually not reachable from your phone. Use Relay or set `hermeslink config set lan-host <host LAN IP>`."
|
|
17746
17748
|
};
|
|
17747
|
-
|
|
17748
|
-
|
|
17749
|
-
|
|
17750
|
-
|
|
17749
|
+
}
|
|
17750
|
+
return {
|
|
17751
|
+
kind: "native",
|
|
17752
|
+
lanAutoDiscoveryUsable: true,
|
|
17753
|
+
warning: null
|
|
17754
|
+
};
|
|
17751
17755
|
}
|
|
17752
|
-
|
|
17753
|
-
|
|
17754
|
-
|
|
17755
|
-
const config = await loadConfig(paths);
|
|
17756
|
-
let status = await getDaemonStatus(paths);
|
|
17757
|
-
if (status.running) {
|
|
17758
|
-
const probe = await probeLocalLinkService({ port: config.port, timeoutMs: 500 });
|
|
17759
|
-
if (probe.reachable) {
|
|
17760
|
-
return status;
|
|
17761
|
-
}
|
|
17762
|
-
await stopDaemonProcess(paths);
|
|
17763
|
-
status = await getDaemonStatus(paths);
|
|
17764
|
-
if (status.running) {
|
|
17765
|
-
return status;
|
|
17766
|
-
}
|
|
17756
|
+
function isWsl(env) {
|
|
17757
|
+
if (process.platform !== "linux") {
|
|
17758
|
+
return false;
|
|
17767
17759
|
}
|
|
17768
|
-
|
|
17769
|
-
|
|
17770
|
-
const scriptPath = currentCliScriptPath();
|
|
17771
|
-
const child = spawn4(process.execPath, [scriptPath, "daemon-supervisor"], {
|
|
17772
|
-
detached: true,
|
|
17773
|
-
stdio: "ignore",
|
|
17774
|
-
env: process.env
|
|
17775
|
-
});
|
|
17776
|
-
child.unref();
|
|
17777
|
-
for (let index = 0; index < 12; index += 1) {
|
|
17778
|
-
await wait(250);
|
|
17779
|
-
const next = await getDaemonStatus(paths);
|
|
17780
|
-
if (next.running && (await probeLocalLinkService({ port: config.port, timeoutMs: 500 })).reachable) {
|
|
17781
|
-
return next;
|
|
17782
|
-
}
|
|
17760
|
+
if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) {
|
|
17761
|
+
return true;
|
|
17783
17762
|
}
|
|
17784
|
-
|
|
17763
|
+
const release = os6.release().toLowerCase();
|
|
17764
|
+
return release.includes("microsoft") || release.includes("wsl");
|
|
17785
17765
|
}
|
|
17786
|
-
|
|
17787
|
-
|
|
17766
|
+
function isContainer(env) {
|
|
17767
|
+
if (env.container || env.CONTAINER || env.KUBERNETES_SERVICE_HOST) {
|
|
17768
|
+
return true;
|
|
17769
|
+
}
|
|
17770
|
+
if (existsSync("/.dockerenv")) {
|
|
17771
|
+
return true;
|
|
17772
|
+
}
|
|
17773
|
+
try {
|
|
17774
|
+
const cgroup = readFileSync2("/proc/1/cgroup", "utf8").toLowerCase();
|
|
17775
|
+
return /docker|containerd|kubepods|libpod|podman/u.test(cgroup);
|
|
17776
|
+
} catch {
|
|
17777
|
+
return false;
|
|
17778
|
+
}
|
|
17779
|
+
}
|
|
17780
|
+
|
|
17781
|
+
// src/topology/network.ts
|
|
17782
|
+
var VIRTUAL_INTERFACE_NAME_PATTERN = /(docker|veth|vmnet|vmenet|vbox|virtualbox|vmware|tailscale|zerotier|wireguard|utun|virbr|hyper-v|vethernet|loopback|\blo\b|^lo\d*$|^br-|^bridge\d+$|^zt|^tun|^tap|awdl|llw|anpi|gif|stf|ipsec|ppp)/iu;
|
|
17783
|
+
var MAX_LAN_IPS = 4;
|
|
17784
|
+
var MAX_PUBLIC_IPV4S = 2;
|
|
17785
|
+
var MAX_PUBLIC_IPV6S = 2;
|
|
17786
|
+
async function discoverRouteCandidates(options) {
|
|
17787
|
+
const environment = detectRuntimeEnvironment();
|
|
17788
|
+
const configuredLanHost = normalizeLanHost(options.configuredLanHost);
|
|
17789
|
+
const lanIps = configuredLanHost ? [configuredLanHost] : environment.lanAutoDiscoveryUsable ? discoverLanIps() : [];
|
|
17790
|
+
const publicIps = options.relayBootstrapToken ? await observePublicRoute(options).catch(() => ({ publicIpv4s: [], publicIpv6s: [] })) : { publicIpv4s: [], publicIpv6s: [] };
|
|
17791
|
+
const publicIpv4s = unique(publicIps.publicIpv4s.filter(isUsablePublicIpv4)).slice(0, MAX_PUBLIC_IPV4S);
|
|
17792
|
+
const publicIpv6s = unique(publicIps.publicIpv6s.filter(isUsablePublicIpv6)).slice(0, MAX_PUBLIC_IPV6S);
|
|
17793
|
+
const preferredUrls = [
|
|
17794
|
+
...lanIps.map((ip) => buildDirectUrl(ip, options.port)),
|
|
17795
|
+
...publicIpv4s.map((ip) => buildDirectUrl(ip, options.port)),
|
|
17796
|
+
...publicIpv6s.map((ip) => buildDirectUrl(ip, options.port)),
|
|
17797
|
+
`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/links/${options.linkId}`
|
|
17798
|
+
];
|
|
17799
|
+
return {
|
|
17800
|
+
lanIps,
|
|
17801
|
+
publicIpv4s,
|
|
17802
|
+
publicIpv6s,
|
|
17803
|
+
preferredUrls,
|
|
17804
|
+
environment
|
|
17805
|
+
};
|
|
17806
|
+
}
|
|
17807
|
+
function discoverLanIps() {
|
|
17808
|
+
return discoverLanIpsFromInterfaces(os7.networkInterfaces());
|
|
17809
|
+
}
|
|
17810
|
+
function discoverLanIpsFromInterfaces(interfaces) {
|
|
17811
|
+
const result = /* @__PURE__ */ new Set();
|
|
17812
|
+
const candidates = [];
|
|
17813
|
+
for (const [name, items] of Object.entries(interfaces)) {
|
|
17814
|
+
if (shouldIgnoreInterface(name)) {
|
|
17815
|
+
continue;
|
|
17816
|
+
}
|
|
17817
|
+
for (const item of items ?? []) {
|
|
17818
|
+
if (!item.internal && item.address && item.family === "IPv4" && isUsableLanIpv42(item.address, item.netmask)) {
|
|
17819
|
+
candidates.push({ name, address: item.address });
|
|
17820
|
+
}
|
|
17821
|
+
}
|
|
17822
|
+
}
|
|
17823
|
+
for (const candidate of candidates.sort(compareLanCandidate)) {
|
|
17824
|
+
result.add(candidate.address);
|
|
17825
|
+
}
|
|
17826
|
+
return [...result].slice(0, MAX_LAN_IPS);
|
|
17827
|
+
}
|
|
17828
|
+
async function observePublicRoute(options) {
|
|
17829
|
+
const fetcher = options.fetchImpl ?? fetch;
|
|
17830
|
+
const response = await fetcher(`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/public-route/observe`, {
|
|
17831
|
+
method: "POST",
|
|
17832
|
+
headers: {
|
|
17833
|
+
authorization: `Bearer ${options.relayBootstrapToken}`,
|
|
17834
|
+
"content-type": "application/json"
|
|
17835
|
+
},
|
|
17836
|
+
body: JSON.stringify({
|
|
17837
|
+
install_id: options.installId,
|
|
17838
|
+
link_id: options.linkId,
|
|
17839
|
+
public_key_pem: options.publicKeyPem
|
|
17840
|
+
})
|
|
17841
|
+
});
|
|
17842
|
+
const payload = await response.json().catch(() => null);
|
|
17843
|
+
const record = typeof payload?.record === "object" && payload.record !== null ? payload.record : null;
|
|
17844
|
+
const observed = typeof payload?.observed === "object" && payload.observed !== null ? payload.observed : null;
|
|
17845
|
+
const values = [
|
|
17846
|
+
readIpRecord(record?.ipv4),
|
|
17847
|
+
readIpRecord(record?.ipv6),
|
|
17848
|
+
typeof observed?.ip === "string" ? observed.ip : null
|
|
17849
|
+
].filter((value) => Boolean(value));
|
|
17850
|
+
return {
|
|
17851
|
+
publicIpv4s: unique(values.filter(isUsablePublicIpv4)),
|
|
17852
|
+
publicIpv6s: unique(values.filter(isUsablePublicIpv6))
|
|
17853
|
+
};
|
|
17854
|
+
}
|
|
17855
|
+
function readIpRecord(value) {
|
|
17856
|
+
if (typeof value !== "object" || value === null) {
|
|
17857
|
+
return null;
|
|
17858
|
+
}
|
|
17859
|
+
const ip = value.ip;
|
|
17860
|
+
return typeof ip === "string" && ip.trim() ? ip.trim() : null;
|
|
17861
|
+
}
|
|
17862
|
+
function buildDirectUrl(ip, port) {
|
|
17863
|
+
return `http://${ip.includes(":") ? `[${ip}]` : ip}:${port}`;
|
|
17864
|
+
}
|
|
17865
|
+
function shouldIgnoreInterface(name) {
|
|
17866
|
+
return !name.trim() || VIRTUAL_INTERFACE_NAME_PATTERN.test(name);
|
|
17867
|
+
}
|
|
17868
|
+
function compareLanCandidate(left, right) {
|
|
17869
|
+
const priority = interfacePriority(left.name) - interfacePriority(right.name);
|
|
17870
|
+
return priority || left.name.localeCompare(right.name) || left.address.localeCompare(right.address);
|
|
17871
|
+
}
|
|
17872
|
+
function interfacePriority(name) {
|
|
17873
|
+
if (/^(en|eth|wlan|wi-fi|wifi)/iu.test(name)) {
|
|
17874
|
+
return 0;
|
|
17875
|
+
}
|
|
17876
|
+
return 1;
|
|
17877
|
+
}
|
|
17878
|
+
function isUsableLanIpv42(address, netmask) {
|
|
17879
|
+
return isPrivateIpv4(address) && !isNetworkOrBroadcastIpv4Address(address, netmask);
|
|
17880
|
+
}
|
|
17881
|
+
function isUsablePublicIpv4(address) {
|
|
17882
|
+
return isValidIpv4(address) && !isSpecialIpv4(address);
|
|
17883
|
+
}
|
|
17884
|
+
function isUsablePublicIpv6(address) {
|
|
17885
|
+
const normalized = address.toLowerCase();
|
|
17886
|
+
return normalized.includes(":") && !normalized.startsWith("fe80:") && !normalized.startsWith("fc") && !normalized.startsWith("fd") && !normalized.startsWith("ff") && normalized !== "::" && normalized !== "::1";
|
|
17887
|
+
}
|
|
17888
|
+
function isPrivateIpv4(address) {
|
|
17889
|
+
const parts = parseIpv4Segments(address);
|
|
17890
|
+
if (!parts) {
|
|
17891
|
+
return false;
|
|
17892
|
+
}
|
|
17893
|
+
const [first, second] = parts;
|
|
17894
|
+
return first === 10 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168;
|
|
17895
|
+
}
|
|
17896
|
+
function isSpecialIpv4(address) {
|
|
17897
|
+
const parts = parseIpv4Segments(address);
|
|
17898
|
+
if (!parts) {
|
|
17899
|
+
return true;
|
|
17900
|
+
}
|
|
17901
|
+
const [first, second, third, fourth] = parts;
|
|
17902
|
+
return first === 0 || first === 10 || first === 127 || first >= 224 || first === 100 && second >= 64 && second <= 127 || first === 169 && second === 254 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168 || first === 192 && second === 0 && third === 2 || first === 198 && (second === 18 || second === 19) || first === 198 && second === 51 && third === 100 || first === 203 && second === 0 && third === 113 || first === 255 && second === 255 && third === 255 && fourth === 255;
|
|
17903
|
+
}
|
|
17904
|
+
function isNetworkOrBroadcastIpv4Address(address, netmask) {
|
|
17905
|
+
const addressParts = parseIpv4Segments(address);
|
|
17906
|
+
const netmaskParts = netmask ? parseIpv4Segments(netmask) : null;
|
|
17907
|
+
if (!addressParts) {
|
|
17908
|
+
return true;
|
|
17909
|
+
}
|
|
17910
|
+
if (!netmaskParts) {
|
|
17911
|
+
const last = addressParts[3];
|
|
17912
|
+
return last === 0 || last === 255;
|
|
17913
|
+
}
|
|
17914
|
+
const addressInt = ipv4SegmentsToInt(addressParts);
|
|
17915
|
+
const netmaskInt = ipv4SegmentsToInt(netmaskParts);
|
|
17916
|
+
const hostMask = ~netmaskInt >>> 0;
|
|
17917
|
+
if (hostMask === 0) {
|
|
17918
|
+
return false;
|
|
17919
|
+
}
|
|
17920
|
+
const networkInt = addressInt & netmaskInt;
|
|
17921
|
+
const broadcastInt = (networkInt | hostMask) >>> 0;
|
|
17922
|
+
return addressInt === networkInt || addressInt === broadcastInt;
|
|
17923
|
+
}
|
|
17924
|
+
function isValidIpv4(address) {
|
|
17925
|
+
return Boolean(parseIpv4Segments(address));
|
|
17926
|
+
}
|
|
17927
|
+
function parseIpv4Segments(address) {
|
|
17928
|
+
if (!/^\d{1,3}(?:\.\d{1,3}){3}$/u.test(address)) {
|
|
17929
|
+
return null;
|
|
17930
|
+
}
|
|
17931
|
+
const parts = address.split(".").map((part) => Number.parseInt(part, 10));
|
|
17932
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
17933
|
+
return null;
|
|
17934
|
+
}
|
|
17935
|
+
return parts;
|
|
17936
|
+
}
|
|
17937
|
+
function ipv4SegmentsToInt(parts) {
|
|
17938
|
+
return (parts[0] << 24 >>> 0 | parts[1] << 16 | parts[2] << 8 | parts[3]) >>> 0;
|
|
17939
|
+
}
|
|
17940
|
+
function unique(values) {
|
|
17941
|
+
return [...new Set(values)];
|
|
17942
|
+
}
|
|
17943
|
+
|
|
17944
|
+
// src/link/network-report-state.ts
|
|
17945
|
+
var DEFAULT_AUTO_DAILY_LIMIT = 20;
|
|
17946
|
+
async function markNetworkStatusReported(paths, lanIps, reportedAt = /* @__PURE__ */ new Date()) {
|
|
17947
|
+
await updateNetworkReportState(paths, (current) => ({
|
|
17948
|
+
...current,
|
|
17949
|
+
lastReportedLanIps: normalizeLanIps(lanIps),
|
|
17950
|
+
lastReportedAt: reportedAt.toISOString(),
|
|
17951
|
+
lastAutoAttempt: current.lastAutoAttempt ? { ...current.lastAutoAttempt, success: true } : null
|
|
17952
|
+
}));
|
|
17953
|
+
}
|
|
17954
|
+
async function reserveAutomaticNetworkReport(paths, lanIps, options = {}) {
|
|
17955
|
+
const snapshot = normalizeLanIps(lanIps);
|
|
17956
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
17957
|
+
const dailyLimit = Math.max(0, Math.floor(options.dailyLimit ?? DEFAULT_AUTO_DAILY_LIMIT));
|
|
17958
|
+
let reservation = { allowed: false, reason: "unchanged" };
|
|
17959
|
+
await updateNetworkReportState(paths, (current) => {
|
|
17960
|
+
if (sameLanIps(current.lastReportedLanIps, snapshot)) {
|
|
17961
|
+
reservation = { allowed: false, reason: "unchanged" };
|
|
17962
|
+
return current;
|
|
17963
|
+
}
|
|
17964
|
+
if (current.lastAutoAttempt && !current.lastAutoAttempt.success && sameLanIps(current.lastAutoAttempt.lanIps, snapshot)) {
|
|
17965
|
+
reservation = { allowed: false, reason: "failed_snapshot_not_retried" };
|
|
17966
|
+
return current;
|
|
17967
|
+
}
|
|
17968
|
+
const quotaDay = formatUtcDay(now);
|
|
17969
|
+
const reportsToday = current.autoQuotaDay === quotaDay ? current.autoReportsToday : 0;
|
|
17970
|
+
if (reportsToday >= dailyLimit) {
|
|
17971
|
+
reservation = { allowed: false, reason: "daily_limit_reached" };
|
|
17972
|
+
return current;
|
|
17973
|
+
}
|
|
17974
|
+
reservation = { allowed: true };
|
|
17975
|
+
return {
|
|
17976
|
+
...current,
|
|
17977
|
+
autoQuotaDay: quotaDay,
|
|
17978
|
+
autoReportsToday: reportsToday + 1,
|
|
17979
|
+
lastAutoAttempt: {
|
|
17980
|
+
lanIps: snapshot,
|
|
17981
|
+
attemptedAt: now.toISOString(),
|
|
17982
|
+
success: false
|
|
17983
|
+
}
|
|
17984
|
+
};
|
|
17985
|
+
});
|
|
17986
|
+
return reservation;
|
|
17987
|
+
}
|
|
17988
|
+
async function updateNetworkReportState(paths, update) {
|
|
17989
|
+
const state = await readLinkState(paths);
|
|
17990
|
+
const next = {
|
|
17991
|
+
...state,
|
|
17992
|
+
networkReport: update(normalizeNetworkReportState(state.networkReport))
|
|
17993
|
+
};
|
|
17994
|
+
await writeJsonFile(paths.stateFile, next);
|
|
17995
|
+
}
|
|
17996
|
+
async function readLinkState(paths) {
|
|
17997
|
+
const state = await readJsonFile(paths.stateFile);
|
|
17998
|
+
return state && typeof state === "object" ? state : {};
|
|
17999
|
+
}
|
|
18000
|
+
function normalizeNetworkReportState(value) {
|
|
18001
|
+
const record = value && typeof value === "object" ? value : {};
|
|
18002
|
+
return {
|
|
18003
|
+
lastReportedLanIps: normalizeLanIps(record.lastReportedLanIps),
|
|
18004
|
+
lastReportedAt: typeof record.lastReportedAt === "string" ? record.lastReportedAt : null,
|
|
18005
|
+
autoQuotaDay: typeof record.autoQuotaDay === "string" ? record.autoQuotaDay : null,
|
|
18006
|
+
autoReportsToday: Number.isFinite(record.autoReportsToday) ? Math.max(0, Math.floor(record.autoReportsToday ?? 0)) : 0,
|
|
18007
|
+
lastAutoAttempt: normalizeAttempt(record.lastAutoAttempt)
|
|
18008
|
+
};
|
|
18009
|
+
}
|
|
18010
|
+
function normalizeAttempt(value) {
|
|
18011
|
+
if (!value || typeof value !== "object") {
|
|
18012
|
+
return null;
|
|
18013
|
+
}
|
|
18014
|
+
const record = value;
|
|
18015
|
+
if (typeof record.attemptedAt !== "string") {
|
|
18016
|
+
return null;
|
|
18017
|
+
}
|
|
18018
|
+
return {
|
|
18019
|
+
lanIps: normalizeLanIps(record.lanIps),
|
|
18020
|
+
attemptedAt: record.attemptedAt,
|
|
18021
|
+
success: record.success === true
|
|
18022
|
+
};
|
|
18023
|
+
}
|
|
18024
|
+
function normalizeLanIps(value) {
|
|
18025
|
+
if (!Array.isArray(value)) {
|
|
18026
|
+
return [];
|
|
18027
|
+
}
|
|
18028
|
+
return [
|
|
18029
|
+
...new Set(
|
|
18030
|
+
value.filter((item) => typeof item === "string" && item.trim().length > 0).map((item) => item.trim())
|
|
18031
|
+
)
|
|
18032
|
+
];
|
|
18033
|
+
}
|
|
18034
|
+
function sameLanIps(left, right) {
|
|
18035
|
+
if (left.length !== right.length) {
|
|
18036
|
+
return false;
|
|
18037
|
+
}
|
|
18038
|
+
return left.every((value, index) => value === right[index]);
|
|
18039
|
+
}
|
|
18040
|
+
function formatUtcDay(date) {
|
|
18041
|
+
return date.toISOString().slice(0, 10);
|
|
18042
|
+
}
|
|
18043
|
+
|
|
18044
|
+
// src/link/server-report.ts
|
|
18045
|
+
async function reportLinkStatusToServer(options = {}) {
|
|
18046
|
+
const paths = options.paths ?? resolveRuntimePaths();
|
|
18047
|
+
const [identity, config] = await Promise.all([loadIdentity(paths), loadConfig(paths)]);
|
|
18048
|
+
if (!identity?.link_id) {
|
|
18049
|
+
return null;
|
|
18050
|
+
}
|
|
18051
|
+
const routes = await discoverRouteCandidates({
|
|
18052
|
+
port: config.port,
|
|
18053
|
+
relayBaseUrl: config.relayBaseUrl,
|
|
18054
|
+
linkId: identity.link_id,
|
|
18055
|
+
installId: identity.install_id,
|
|
18056
|
+
publicKeyPem: identity.public_key_pem,
|
|
18057
|
+
configuredLanHost: config.lanHost,
|
|
18058
|
+
fetchImpl: options.fetchImpl
|
|
18059
|
+
});
|
|
18060
|
+
const systemInfo = readLinkSystemInfo();
|
|
18061
|
+
const payload = {
|
|
18062
|
+
type: "hermes_link_status_report",
|
|
18063
|
+
link_id: identity.link_id,
|
|
18064
|
+
install_id: identity.install_id,
|
|
18065
|
+
link_version: LINK_VERSION,
|
|
18066
|
+
display_name: systemInfo.defaultDisplayName,
|
|
18067
|
+
platform: systemInfo.platform,
|
|
18068
|
+
hostname: systemInfo.hostname ?? void 0,
|
|
18069
|
+
lan_ips: routes.lanIps,
|
|
18070
|
+
public_ipv4s: routes.publicIpv4s,
|
|
18071
|
+
public_ipv6s: routes.publicIpv6s,
|
|
18072
|
+
reported_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
18073
|
+
};
|
|
18074
|
+
const signature = signIdentityPayload(identity, canonicalJson(payload));
|
|
18075
|
+
const fetcher = options.fetchImpl ?? fetch;
|
|
18076
|
+
const response = await fetcher(
|
|
18077
|
+
`${config.serverBaseUrl.replace(/\/+$/u, "")}/api/v1/links/${encodeURIComponent(identity.link_id)}/report`,
|
|
18078
|
+
{
|
|
18079
|
+
method: "POST",
|
|
18080
|
+
headers: {
|
|
18081
|
+
accept: "application/json",
|
|
18082
|
+
"content-type": "application/json"
|
|
18083
|
+
},
|
|
18084
|
+
body: JSON.stringify({
|
|
18085
|
+
...payload,
|
|
18086
|
+
public_key_pem: identity.public_key_pem,
|
|
18087
|
+
signature
|
|
18088
|
+
})
|
|
18089
|
+
}
|
|
18090
|
+
);
|
|
18091
|
+
const body = await response.json().catch(() => null);
|
|
18092
|
+
if (!response.ok || !body) {
|
|
18093
|
+
const message = readErrorMessage3(body) ?? `HermesPilot Server request failed with HTTP ${response.status}`;
|
|
18094
|
+
throw new LinkHttpError(response.status, "server_request_failed", message);
|
|
18095
|
+
}
|
|
18096
|
+
await markNetworkStatusReported(paths, routes.lanIps);
|
|
18097
|
+
return body;
|
|
18098
|
+
}
|
|
18099
|
+
function canonicalJson(value) {
|
|
18100
|
+
return JSON.stringify(sortJsonValue(value));
|
|
18101
|
+
}
|
|
18102
|
+
function sortJsonValue(value) {
|
|
18103
|
+
if (Array.isArray(value)) {
|
|
18104
|
+
return value.map(sortJsonValue);
|
|
18105
|
+
}
|
|
18106
|
+
if (value && typeof value === "object") {
|
|
18107
|
+
const record = value;
|
|
18108
|
+
const sorted = {};
|
|
18109
|
+
for (const key of Object.keys(record).sort()) {
|
|
18110
|
+
sorted[key] = sortJsonValue(record[key]);
|
|
18111
|
+
}
|
|
18112
|
+
return sorted;
|
|
18113
|
+
}
|
|
18114
|
+
return value;
|
|
18115
|
+
}
|
|
18116
|
+
function readErrorMessage3(payload) {
|
|
18117
|
+
if (typeof payload !== "object" || payload === null) {
|
|
18118
|
+
return null;
|
|
18119
|
+
}
|
|
18120
|
+
const error = payload.error;
|
|
18121
|
+
if (typeof error !== "object" || error === null) {
|
|
18122
|
+
return null;
|
|
18123
|
+
}
|
|
18124
|
+
const message = error.message;
|
|
18125
|
+
return typeof message === "string" ? message : null;
|
|
18126
|
+
}
|
|
18127
|
+
|
|
18128
|
+
// src/daemon/lan-ip-monitor.ts
|
|
18129
|
+
var DEFAULT_INTERVAL_MS = 5 * 6e4;
|
|
18130
|
+
var DEFAULT_DAILY_REPORT_LIMIT = 20;
|
|
18131
|
+
function startLanIpMonitor(options) {
|
|
18132
|
+
let running = false;
|
|
18133
|
+
let closed = false;
|
|
18134
|
+
const check = async () => {
|
|
18135
|
+
if (running || closed) {
|
|
18136
|
+
return;
|
|
18137
|
+
}
|
|
18138
|
+
running = true;
|
|
18139
|
+
try {
|
|
18140
|
+
await checkLanIpChange(options);
|
|
18141
|
+
} catch (error) {
|
|
18142
|
+
void options.logger.warn("lan_ip_monitor_failed", {
|
|
18143
|
+
error: error instanceof Error ? error.message : String(error)
|
|
18144
|
+
});
|
|
18145
|
+
} finally {
|
|
18146
|
+
running = false;
|
|
18147
|
+
}
|
|
18148
|
+
};
|
|
18149
|
+
void check();
|
|
18150
|
+
const timer = setInterval(() => {
|
|
18151
|
+
void check();
|
|
18152
|
+
}, options.intervalMs ?? DEFAULT_INTERVAL_MS);
|
|
18153
|
+
timer.unref?.();
|
|
18154
|
+
return {
|
|
18155
|
+
close() {
|
|
18156
|
+
closed = true;
|
|
18157
|
+
clearInterval(timer);
|
|
18158
|
+
}
|
|
18159
|
+
};
|
|
18160
|
+
}
|
|
18161
|
+
async function checkLanIpChange(options) {
|
|
18162
|
+
const [identity, config] = await Promise.all([
|
|
18163
|
+
loadIdentity(options.paths),
|
|
18164
|
+
loadConfig(options.paths)
|
|
18165
|
+
]);
|
|
18166
|
+
if (!identity?.link_id) {
|
|
18167
|
+
return;
|
|
18168
|
+
}
|
|
18169
|
+
const routes = await discoverRouteCandidates({
|
|
18170
|
+
port: config.port,
|
|
18171
|
+
relayBaseUrl: config.relayBaseUrl,
|
|
18172
|
+
linkId: identity.link_id,
|
|
18173
|
+
installId: identity.install_id,
|
|
18174
|
+
publicKeyPem: identity.public_key_pem,
|
|
18175
|
+
configuredLanHost: config.lanHost,
|
|
18176
|
+
fetchImpl: options.fetchImpl
|
|
18177
|
+
});
|
|
18178
|
+
const reservation = await reserveAutomaticNetworkReport(options.paths, routes.lanIps, {
|
|
18179
|
+
dailyLimit: options.dailyReportLimit ?? DEFAULT_DAILY_REPORT_LIMIT
|
|
18180
|
+
});
|
|
18181
|
+
if (!reservation.allowed) {
|
|
18182
|
+
const logFields = { lan_ips: routes.lanIps, reason: reservation.reason };
|
|
18183
|
+
if (reservation.reason === "daily_limit_reached") {
|
|
18184
|
+
void options.logger.warn("lan_ip_report_skipped", logFields);
|
|
18185
|
+
} else {
|
|
18186
|
+
void options.logger.debug("lan_ip_report_skipped", logFields);
|
|
18187
|
+
}
|
|
18188
|
+
return;
|
|
18189
|
+
}
|
|
18190
|
+
try {
|
|
18191
|
+
const result = await reportLinkStatusToServer({
|
|
18192
|
+
paths: options.paths,
|
|
18193
|
+
fetchImpl: options.fetchImpl
|
|
18194
|
+
});
|
|
18195
|
+
if (result) {
|
|
18196
|
+
void options.logger.info("lan_ip_change_reported", {
|
|
18197
|
+
link_id: result.linkId,
|
|
18198
|
+
lan_ips: routes.lanIps
|
|
18199
|
+
});
|
|
18200
|
+
}
|
|
18201
|
+
} catch (error) {
|
|
18202
|
+
void options.logger.warn("lan_ip_change_report_failed", {
|
|
18203
|
+
lan_ips: routes.lanIps,
|
|
18204
|
+
error: error instanceof Error ? error.message : String(error)
|
|
18205
|
+
});
|
|
18206
|
+
}
|
|
18207
|
+
}
|
|
18208
|
+
|
|
18209
|
+
// src/daemon/scheduler.ts
|
|
18210
|
+
function startCronDeliveryScheduler(options) {
|
|
18211
|
+
let running = false;
|
|
18212
|
+
const syncCronDeliveries = async () => {
|
|
18213
|
+
if (running) {
|
|
18214
|
+
return;
|
|
18215
|
+
}
|
|
18216
|
+
running = true;
|
|
18217
|
+
try {
|
|
18218
|
+
await syncHermesLinkCronDeliveries(
|
|
18219
|
+
options.paths,
|
|
18220
|
+
options.conversations,
|
|
18221
|
+
options.logger
|
|
18222
|
+
);
|
|
18223
|
+
} catch (error) {
|
|
18224
|
+
void options.logger.warn("cron_link_delivery_sync_failed", {
|
|
18225
|
+
error: error instanceof Error ? error.message : String(error)
|
|
18226
|
+
});
|
|
18227
|
+
} finally {
|
|
18228
|
+
running = false;
|
|
18229
|
+
}
|
|
18230
|
+
};
|
|
18231
|
+
const timer = setInterval(() => {
|
|
18232
|
+
void syncCronDeliveries();
|
|
18233
|
+
}, options.intervalMs ?? 3e4);
|
|
18234
|
+
timer.unref?.();
|
|
18235
|
+
return {
|
|
18236
|
+
close() {
|
|
18237
|
+
clearInterval(timer);
|
|
18238
|
+
}
|
|
18239
|
+
};
|
|
18240
|
+
}
|
|
18241
|
+
|
|
18242
|
+
// src/daemon/service.ts
|
|
18243
|
+
async function startLinkService(options = {}) {
|
|
18244
|
+
const paths = options.paths ?? resolveRuntimePaths();
|
|
18245
|
+
const logger = createFileLogger({ paths });
|
|
18246
|
+
const [identity, config] = await Promise.all([loadIdentity(paths), loadConfig(paths)]);
|
|
18247
|
+
await logger.info("service_starting", {
|
|
18248
|
+
port: config.port,
|
|
18249
|
+
mode: identity?.link_id ? "paired" : "local-only"
|
|
18250
|
+
});
|
|
18251
|
+
const migration = await migrateLinkDatabase(paths);
|
|
18252
|
+
if (migration.appliedVersions.length > 0) {
|
|
18253
|
+
await logger.info("database_migrated", {
|
|
18254
|
+
database_file: migration.databaseFile,
|
|
18255
|
+
applied_versions: migration.appliedVersions,
|
|
18256
|
+
current_version: migration.currentVersion
|
|
18257
|
+
});
|
|
18258
|
+
}
|
|
18259
|
+
const conversations = new ConversationService(paths, logger);
|
|
18260
|
+
await conversations.rebuildStatisticsIndex();
|
|
18261
|
+
const app = await createApp({
|
|
18262
|
+
paths,
|
|
18263
|
+
logger,
|
|
18264
|
+
conversations,
|
|
18265
|
+
onPairingClaimed: options.onPairingClaimed
|
|
18266
|
+
});
|
|
18267
|
+
const server = createServer(app.callback());
|
|
18268
|
+
try {
|
|
18269
|
+
await listenServer(server, config.port);
|
|
18270
|
+
} catch (error) {
|
|
18271
|
+
await logger.error("service_start_failed", {
|
|
18272
|
+
port: config.port,
|
|
18273
|
+
error: error instanceof Error ? error.message : String(error)
|
|
18274
|
+
});
|
|
18275
|
+
await logger.flush();
|
|
18276
|
+
throw error;
|
|
18277
|
+
}
|
|
18278
|
+
server.on("error", (error) => {
|
|
18279
|
+
void logger.error("service_error", { error: error.message });
|
|
18280
|
+
});
|
|
18281
|
+
void logger.info("service_started", {
|
|
18282
|
+
port: config.port,
|
|
18283
|
+
link_id: identity?.link_id ?? null
|
|
18284
|
+
});
|
|
18285
|
+
const scheduler = startCronDeliveryScheduler({
|
|
18286
|
+
paths,
|
|
18287
|
+
conversations,
|
|
18288
|
+
logger
|
|
18289
|
+
});
|
|
18290
|
+
const lanIpMonitor = startLanIpMonitor({
|
|
18291
|
+
paths,
|
|
18292
|
+
logger,
|
|
18293
|
+
intervalMs: options.lanIpMonitorIntervalMs,
|
|
18294
|
+
dailyReportLimit: options.lanIpMonitorDailyReportLimit,
|
|
18295
|
+
fetchImpl: options.lanIpMonitorFetchImpl
|
|
18296
|
+
});
|
|
18297
|
+
let relay = null;
|
|
18298
|
+
if (identity?.link_id) {
|
|
18299
|
+
relay = connectRelayControl({
|
|
18300
|
+
relayBaseUrl: config.relayBaseUrl,
|
|
18301
|
+
linkId: identity.link_id,
|
|
18302
|
+
localPort: config.port,
|
|
18303
|
+
maxReconnectAttempts: options.relayMaxReconnectAttempts ?? 5,
|
|
18304
|
+
backoffBaseMs: 1e3,
|
|
18305
|
+
backoffMaxMs: 3e4,
|
|
18306
|
+
onStatus: (status) => {
|
|
18307
|
+
void logger.info("relay_status", status);
|
|
18308
|
+
}
|
|
18309
|
+
});
|
|
18310
|
+
} else {
|
|
18311
|
+
void logger.info("relay_skipped", { reason: "link_not_paired" });
|
|
18312
|
+
}
|
|
18313
|
+
if (options.writePidFile) {
|
|
18314
|
+
await writePidFile(paths);
|
|
18315
|
+
}
|
|
18316
|
+
return {
|
|
18317
|
+
async close() {
|
|
18318
|
+
scheduler.close();
|
|
18319
|
+
lanIpMonitor.close();
|
|
18320
|
+
relay?.close();
|
|
18321
|
+
await closeServer(server);
|
|
18322
|
+
await logger.info("service_stopped");
|
|
18323
|
+
await logger.flush();
|
|
18324
|
+
if (options.writePidFile) {
|
|
18325
|
+
await rm9(pidFilePath(paths), { force: true }).catch(() => void 0);
|
|
18326
|
+
}
|
|
18327
|
+
}
|
|
18328
|
+
};
|
|
18329
|
+
}
|
|
18330
|
+
function pidFilePath(paths = resolveRuntimePaths()) {
|
|
18331
|
+
return `${paths.runDir}/hermeslink.pid`;
|
|
18332
|
+
}
|
|
18333
|
+
async function writePidFile(paths) {
|
|
18334
|
+
await mkdir15(paths.runDir, { recursive: true, mode: 448 });
|
|
18335
|
+
await writeFile7(pidFilePath(paths), `${process.pid}
|
|
18336
|
+
`, { mode: 384 });
|
|
18337
|
+
}
|
|
18338
|
+
async function closeServer(server) {
|
|
18339
|
+
await new Promise((resolve, reject) => {
|
|
18340
|
+
let settled = false;
|
|
18341
|
+
let forceCloseTimer;
|
|
18342
|
+
let timeoutTimer;
|
|
18343
|
+
const settle = (error) => {
|
|
18344
|
+
if (settled) {
|
|
18345
|
+
return;
|
|
18346
|
+
}
|
|
18347
|
+
settled = true;
|
|
18348
|
+
clearTimeout(forceCloseTimer);
|
|
18349
|
+
clearTimeout(timeoutTimer);
|
|
18350
|
+
if (error) {
|
|
18351
|
+
reject(error);
|
|
18352
|
+
return;
|
|
18353
|
+
}
|
|
18354
|
+
resolve();
|
|
18355
|
+
};
|
|
18356
|
+
forceCloseTimer = setTimeout(() => {
|
|
18357
|
+
server.closeIdleConnections?.();
|
|
18358
|
+
server.closeAllConnections?.();
|
|
18359
|
+
}, 250);
|
|
18360
|
+
timeoutTimer = setTimeout(() => {
|
|
18361
|
+
server.closeAllConnections?.();
|
|
18362
|
+
settle();
|
|
18363
|
+
}, 5e3);
|
|
18364
|
+
server.close((error) => {
|
|
18365
|
+
if (error) {
|
|
18366
|
+
settle(error);
|
|
18367
|
+
return;
|
|
18368
|
+
}
|
|
18369
|
+
settle();
|
|
18370
|
+
});
|
|
18371
|
+
server.closeIdleConnections?.();
|
|
18372
|
+
});
|
|
18373
|
+
}
|
|
18374
|
+
async function listenServer(server, port) {
|
|
18375
|
+
await new Promise((resolve, reject) => {
|
|
18376
|
+
const cleanup = () => {
|
|
18377
|
+
server.off("error", onError);
|
|
18378
|
+
server.off("listening", onListening);
|
|
18379
|
+
};
|
|
18380
|
+
const onError = (error) => {
|
|
18381
|
+
cleanup();
|
|
18382
|
+
reject(error);
|
|
18383
|
+
};
|
|
18384
|
+
const onListening = () => {
|
|
18385
|
+
cleanup();
|
|
18386
|
+
resolve();
|
|
18387
|
+
};
|
|
18388
|
+
server.once("error", onError);
|
|
18389
|
+
server.once("listening", onListening);
|
|
18390
|
+
server.listen(port);
|
|
18391
|
+
});
|
|
18392
|
+
}
|
|
18393
|
+
|
|
18394
|
+
// src/daemon/process.ts
|
|
18395
|
+
async function startDaemonProcess(paths = resolveRuntimePaths()) {
|
|
18396
|
+
const config = await loadConfig(paths);
|
|
18397
|
+
let status = await getDaemonStatus(paths);
|
|
18398
|
+
if (status.running) {
|
|
18399
|
+
const probe = await probeLocalLinkService({ port: config.port, timeoutMs: 500 });
|
|
18400
|
+
if (probe.reachable) {
|
|
18401
|
+
return status;
|
|
18402
|
+
}
|
|
18403
|
+
await stopDaemonProcess(paths);
|
|
18404
|
+
status = await getDaemonStatus(paths);
|
|
18405
|
+
if (status.running) {
|
|
18406
|
+
return status;
|
|
18407
|
+
}
|
|
18408
|
+
}
|
|
18409
|
+
await mkdir16(paths.logsDir, { recursive: true, mode: 448 });
|
|
18410
|
+
await mkdir16(paths.runDir, { recursive: true, mode: 448 });
|
|
18411
|
+
const scriptPath = currentCliScriptPath();
|
|
18412
|
+
const child = spawn4(process.execPath, [scriptPath, "daemon-supervisor"], {
|
|
18413
|
+
detached: true,
|
|
18414
|
+
stdio: "ignore",
|
|
18415
|
+
env: process.env
|
|
18416
|
+
});
|
|
18417
|
+
child.unref();
|
|
18418
|
+
for (let index = 0; index < 12; index += 1) {
|
|
18419
|
+
await wait(250);
|
|
18420
|
+
const next = await getDaemonStatus(paths);
|
|
18421
|
+
if (next.running && (await probeLocalLinkService({ port: config.port, timeoutMs: 500 })).reachable) {
|
|
18422
|
+
return next;
|
|
18423
|
+
}
|
|
18424
|
+
}
|
|
18425
|
+
return await getDaemonStatus(paths);
|
|
18426
|
+
}
|
|
18427
|
+
async function runDaemonSupervisor(paths = resolveRuntimePaths()) {
|
|
18428
|
+
await mkdir16(paths.logsDir, { recursive: true, mode: 448 });
|
|
17788
18429
|
const log = createRotatingTextLogWriter({
|
|
17789
18430
|
paths,
|
|
17790
18431
|
fileName: path21.basename(daemonLogFile(paths))
|
|
@@ -18447,7 +19088,7 @@ async function postJson(fetcher, url, token, body) {
|
|
|
18447
19088
|
});
|
|
18448
19089
|
const payload = await response.json().catch(() => null);
|
|
18449
19090
|
if (!response.ok) {
|
|
18450
|
-
const message =
|
|
19091
|
+
const message = readErrorMessage4(payload) ?? `Relay request failed with HTTP ${response.status}`;
|
|
18451
19092
|
throw new Error(message);
|
|
18452
19093
|
}
|
|
18453
19094
|
if (!payload) {
|
|
@@ -18455,7 +19096,7 @@ async function postJson(fetcher, url, token, body) {
|
|
|
18455
19096
|
}
|
|
18456
19097
|
return payload;
|
|
18457
19098
|
}
|
|
18458
|
-
function
|
|
19099
|
+
function readErrorMessage4(payload) {
|
|
18459
19100
|
if (typeof payload !== "object" || payload === null) {
|
|
18460
19101
|
return null;
|
|
18461
19102
|
}
|
|
@@ -18467,175 +19108,17 @@ function readErrorMessage3(payload) {
|
|
|
18467
19108
|
return typeof message === "string" ? message : null;
|
|
18468
19109
|
}
|
|
18469
19110
|
|
|
18470
|
-
// src/topology/network.ts
|
|
18471
|
-
import os5 from "os";
|
|
18472
|
-
var VIRTUAL_INTERFACE_NAME_PATTERN = /(docker|veth|vmnet|vmenet|vbox|virtualbox|vmware|tailscale|zerotier|wireguard|utun|virbr|hyper-v|vethernet|loopback|\blo\b|^lo\d*$|^br-|^bridge\d+$|^zt|^tun|^tap|awdl|llw|anpi|gif|stf|ipsec|ppp)/iu;
|
|
18473
|
-
var MAX_LAN_IPS = 4;
|
|
18474
|
-
var MAX_PUBLIC_IPV4S = 2;
|
|
18475
|
-
var MAX_PUBLIC_IPV6S = 2;
|
|
18476
|
-
async function discoverRouteCandidates(options) {
|
|
18477
|
-
const lanIps = discoverLanIps();
|
|
18478
|
-
const publicIps = options.relayBootstrapToken ? await observePublicRoute(options).catch(() => ({ publicIpv4s: [], publicIpv6s: [] })) : { publicIpv4s: [], publicIpv6s: [] };
|
|
18479
|
-
const publicIpv4s = unique(publicIps.publicIpv4s.filter(isUsablePublicIpv4)).slice(0, MAX_PUBLIC_IPV4S);
|
|
18480
|
-
const publicIpv6s = unique(publicIps.publicIpv6s.filter(isUsablePublicIpv6)).slice(0, MAX_PUBLIC_IPV6S);
|
|
18481
|
-
const preferredUrls = [
|
|
18482
|
-
...lanIps.map((ip) => buildDirectUrl(ip, options.port)),
|
|
18483
|
-
...publicIpv4s.map((ip) => buildDirectUrl(ip, options.port)),
|
|
18484
|
-
...publicIpv6s.map((ip) => buildDirectUrl(ip, options.port)),
|
|
18485
|
-
`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/links/${options.linkId}`
|
|
18486
|
-
];
|
|
18487
|
-
return {
|
|
18488
|
-
lanIps,
|
|
18489
|
-
publicIpv4s,
|
|
18490
|
-
publicIpv6s,
|
|
18491
|
-
preferredUrls
|
|
18492
|
-
};
|
|
18493
|
-
}
|
|
18494
|
-
function discoverLanIps() {
|
|
18495
|
-
return discoverLanIpsFromInterfaces(os5.networkInterfaces());
|
|
18496
|
-
}
|
|
18497
|
-
function discoverLanIpsFromInterfaces(interfaces) {
|
|
18498
|
-
const result = /* @__PURE__ */ new Set();
|
|
18499
|
-
const candidates = [];
|
|
18500
|
-
for (const [name, items] of Object.entries(interfaces)) {
|
|
18501
|
-
if (shouldIgnoreInterface(name)) {
|
|
18502
|
-
continue;
|
|
18503
|
-
}
|
|
18504
|
-
for (const item of items ?? []) {
|
|
18505
|
-
if (!item.internal && item.address && item.family === "IPv4" && isUsableLanIpv4(item.address, item.netmask)) {
|
|
18506
|
-
candidates.push({ name, address: item.address });
|
|
18507
|
-
}
|
|
18508
|
-
}
|
|
18509
|
-
}
|
|
18510
|
-
for (const candidate of candidates.sort(compareLanCandidate)) {
|
|
18511
|
-
result.add(candidate.address);
|
|
18512
|
-
}
|
|
18513
|
-
return [...result].slice(0, MAX_LAN_IPS);
|
|
18514
|
-
}
|
|
18515
|
-
async function observePublicRoute(options) {
|
|
18516
|
-
const fetcher = options.fetchImpl ?? fetch;
|
|
18517
|
-
const response = await fetcher(`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/public-route/observe`, {
|
|
18518
|
-
method: "POST",
|
|
18519
|
-
headers: {
|
|
18520
|
-
authorization: `Bearer ${options.relayBootstrapToken}`,
|
|
18521
|
-
"content-type": "application/json"
|
|
18522
|
-
},
|
|
18523
|
-
body: JSON.stringify({
|
|
18524
|
-
install_id: options.installId,
|
|
18525
|
-
link_id: options.linkId,
|
|
18526
|
-
public_key_pem: options.publicKeyPem
|
|
18527
|
-
})
|
|
18528
|
-
});
|
|
18529
|
-
const payload = await response.json().catch(() => null);
|
|
18530
|
-
const record = typeof payload?.record === "object" && payload.record !== null ? payload.record : null;
|
|
18531
|
-
const observed = typeof payload?.observed === "object" && payload.observed !== null ? payload.observed : null;
|
|
18532
|
-
const values = [
|
|
18533
|
-
readIpRecord(record?.ipv4),
|
|
18534
|
-
readIpRecord(record?.ipv6),
|
|
18535
|
-
typeof observed?.ip === "string" ? observed.ip : null
|
|
18536
|
-
].filter((value) => Boolean(value));
|
|
18537
|
-
return {
|
|
18538
|
-
publicIpv4s: unique(values.filter(isUsablePublicIpv4)),
|
|
18539
|
-
publicIpv6s: unique(values.filter(isUsablePublicIpv6))
|
|
18540
|
-
};
|
|
18541
|
-
}
|
|
18542
|
-
function readIpRecord(value) {
|
|
18543
|
-
if (typeof value !== "object" || value === null) {
|
|
18544
|
-
return null;
|
|
18545
|
-
}
|
|
18546
|
-
const ip = value.ip;
|
|
18547
|
-
return typeof ip === "string" && ip.trim() ? ip.trim() : null;
|
|
18548
|
-
}
|
|
18549
|
-
function buildDirectUrl(ip, port) {
|
|
18550
|
-
return `http://${ip.includes(":") ? `[${ip}]` : ip}:${port}`;
|
|
18551
|
-
}
|
|
18552
|
-
function shouldIgnoreInterface(name) {
|
|
18553
|
-
return !name.trim() || VIRTUAL_INTERFACE_NAME_PATTERN.test(name);
|
|
18554
|
-
}
|
|
18555
|
-
function compareLanCandidate(left, right) {
|
|
18556
|
-
const priority = interfacePriority(left.name) - interfacePriority(right.name);
|
|
18557
|
-
return priority || left.name.localeCompare(right.name) || left.address.localeCompare(right.address);
|
|
18558
|
-
}
|
|
18559
|
-
function interfacePriority(name) {
|
|
18560
|
-
if (/^(en|eth|wlan|wi-fi|wifi)/iu.test(name)) {
|
|
18561
|
-
return 0;
|
|
18562
|
-
}
|
|
18563
|
-
return 1;
|
|
18564
|
-
}
|
|
18565
|
-
function isUsableLanIpv4(address, netmask) {
|
|
18566
|
-
return isPrivateIpv4(address) && !isNetworkOrBroadcastIpv4Address(address, netmask);
|
|
18567
|
-
}
|
|
18568
|
-
function isUsablePublicIpv4(address) {
|
|
18569
|
-
return isValidIpv4(address) && !isSpecialIpv4(address);
|
|
18570
|
-
}
|
|
18571
|
-
function isUsablePublicIpv6(address) {
|
|
18572
|
-
const normalized = address.toLowerCase();
|
|
18573
|
-
return normalized.includes(":") && !normalized.startsWith("fe80:") && !normalized.startsWith("fc") && !normalized.startsWith("fd") && !normalized.startsWith("ff") && normalized !== "::" && normalized !== "::1";
|
|
18574
|
-
}
|
|
18575
|
-
function isPrivateIpv4(address) {
|
|
18576
|
-
const parts = parseIpv4Segments(address);
|
|
18577
|
-
if (!parts) {
|
|
18578
|
-
return false;
|
|
18579
|
-
}
|
|
18580
|
-
const [first, second] = parts;
|
|
18581
|
-
return first === 10 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168;
|
|
18582
|
-
}
|
|
18583
|
-
function isSpecialIpv4(address) {
|
|
18584
|
-
const parts = parseIpv4Segments(address);
|
|
18585
|
-
if (!parts) {
|
|
18586
|
-
return true;
|
|
18587
|
-
}
|
|
18588
|
-
const [first, second, third, fourth] = parts;
|
|
18589
|
-
return first === 0 || first === 10 || first === 127 || first >= 224 || first === 100 && second >= 64 && second <= 127 || first === 169 && second === 254 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168 || first === 192 && second === 0 && third === 2 || first === 198 && (second === 18 || second === 19) || first === 198 && second === 51 && third === 100 || first === 203 && second === 0 && third === 113 || first === 255 && second === 255 && third === 255 && fourth === 255;
|
|
18590
|
-
}
|
|
18591
|
-
function isNetworkOrBroadcastIpv4Address(address, netmask) {
|
|
18592
|
-
const addressParts = parseIpv4Segments(address);
|
|
18593
|
-
const netmaskParts = netmask ? parseIpv4Segments(netmask) : null;
|
|
18594
|
-
if (!addressParts) {
|
|
18595
|
-
return true;
|
|
18596
|
-
}
|
|
18597
|
-
if (!netmaskParts) {
|
|
18598
|
-
const last = addressParts[3];
|
|
18599
|
-
return last === 0 || last === 255;
|
|
18600
|
-
}
|
|
18601
|
-
const addressInt = ipv4SegmentsToInt(addressParts);
|
|
18602
|
-
const netmaskInt = ipv4SegmentsToInt(netmaskParts);
|
|
18603
|
-
const hostMask = ~netmaskInt >>> 0;
|
|
18604
|
-
if (hostMask === 0) {
|
|
18605
|
-
return false;
|
|
18606
|
-
}
|
|
18607
|
-
const networkInt = addressInt & netmaskInt;
|
|
18608
|
-
const broadcastInt = (networkInt | hostMask) >>> 0;
|
|
18609
|
-
return addressInt === networkInt || addressInt === broadcastInt;
|
|
18610
|
-
}
|
|
18611
|
-
function isValidIpv4(address) {
|
|
18612
|
-
return Boolean(parseIpv4Segments(address));
|
|
18613
|
-
}
|
|
18614
|
-
function parseIpv4Segments(address) {
|
|
18615
|
-
if (!/^\d{1,3}(?:\.\d{1,3}){3}$/u.test(address)) {
|
|
18616
|
-
return null;
|
|
18617
|
-
}
|
|
18618
|
-
const parts = address.split(".").map((part) => Number.parseInt(part, 10));
|
|
18619
|
-
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
18620
|
-
return null;
|
|
18621
|
-
}
|
|
18622
|
-
return parts;
|
|
18623
|
-
}
|
|
18624
|
-
function ipv4SegmentsToInt(parts) {
|
|
18625
|
-
return (parts[0] << 24 >>> 0 | parts[1] << 16 | parts[2] << 8 | parts[3]) >>> 0;
|
|
18626
|
-
}
|
|
18627
|
-
function unique(values) {
|
|
18628
|
-
return [...new Set(values)];
|
|
18629
|
-
}
|
|
18630
|
-
|
|
18631
19111
|
// src/pairing/pairing.ts
|
|
18632
19112
|
async function preparePairing(paths = resolveRuntimePaths()) {
|
|
18633
19113
|
const config = await loadConfig(paths);
|
|
18634
19114
|
const identity = await ensureIdentity(paths);
|
|
19115
|
+
const systemInfo = readLinkSystemInfo();
|
|
18635
19116
|
const created = await postServerJson(config.serverBaseUrl, "/api/v1/link-pairings", {
|
|
18636
19117
|
install_id: identity.install_id,
|
|
18637
19118
|
link_id: identity.link_id ?? void 0,
|
|
18638
|
-
display_name: defaultDisplayName
|
|
19119
|
+
display_name: systemInfo.defaultDisplayName,
|
|
19120
|
+
platform: systemInfo.platform,
|
|
19121
|
+
hostname: systemInfo.hostname ?? void 0,
|
|
18639
19122
|
public_key_pem: identity.public_key_pem
|
|
18640
19123
|
});
|
|
18641
19124
|
const relayBaseUrl = created.relayBaseUrl || config.relayBaseUrl;
|
|
@@ -18650,6 +19133,7 @@ async function preparePairing(paths = resolveRuntimePaths()) {
|
|
|
18650
19133
|
port: config.port,
|
|
18651
19134
|
relayBaseUrl,
|
|
18652
19135
|
relayBootstrapToken: created.relayBootstrapToken,
|
|
19136
|
+
configuredLanHost: config.lanHost,
|
|
18653
19137
|
linkId: assigned.linkId,
|
|
18654
19138
|
installId: updatedIdentity.install_id,
|
|
18655
19139
|
publicKeyPem: updatedIdentity.public_key_pem
|
|
@@ -18658,17 +19142,20 @@ async function preparePairing(paths = resolveRuntimePaths()) {
|
|
|
18658
19142
|
install_id: updatedIdentity.install_id,
|
|
18659
19143
|
link_id: assigned.linkId,
|
|
18660
19144
|
link_version: LINK_VERSION,
|
|
18661
|
-
display_name: defaultDisplayName
|
|
19145
|
+
display_name: systemInfo.defaultDisplayName,
|
|
19146
|
+
platform: systemInfo.platform,
|
|
19147
|
+
hostname: systemInfo.hostname ?? void 0,
|
|
18662
19148
|
lan_ips: routes.lanIps,
|
|
18663
19149
|
public_ipv4s: routes.publicIpv4s,
|
|
18664
19150
|
public_ipv6s: routes.publicIpv6s,
|
|
18665
|
-
preferred_urls: routes.preferredUrls
|
|
19151
|
+
preferred_urls: routes.preferredUrls,
|
|
19152
|
+
environment: routes.environment
|
|
18666
19153
|
});
|
|
18667
19154
|
const qrPayload = {
|
|
18668
19155
|
kind: "hermes_link_pairing",
|
|
18669
19156
|
version: 1,
|
|
18670
19157
|
link_id: assigned.linkId,
|
|
18671
|
-
display_name: defaultDisplayName
|
|
19158
|
+
display_name: systemInfo.defaultDisplayName,
|
|
18672
19159
|
session_id: created.sessionId,
|
|
18673
19160
|
code: created.code,
|
|
18674
19161
|
preferred_urls: qrPreferredUrls(routes)
|
|
@@ -18679,7 +19166,7 @@ async function preparePairing(paths = resolveRuntimePaths()) {
|
|
|
18679
19166
|
session_id: created.sessionId,
|
|
18680
19167
|
code: created.code,
|
|
18681
19168
|
link_id: assigned.linkId,
|
|
18682
|
-
display_name: defaultDisplayName
|
|
19169
|
+
display_name: systemInfo.defaultDisplayName,
|
|
18683
19170
|
local_api_url: `http://127.0.0.1:${config.port}`,
|
|
18684
19171
|
server_base_url: config.serverBaseUrl,
|
|
18685
19172
|
relay_base_url: relayBaseUrl,
|
|
@@ -18695,7 +19182,7 @@ async function preparePairing(paths = resolveRuntimePaths()) {
|
|
|
18695
19182
|
pairingToken: created.pairingToken,
|
|
18696
19183
|
relayBootstrapToken: created.relayBootstrapToken,
|
|
18697
19184
|
relayBaseUrl,
|
|
18698
|
-
displayName: defaultDisplayName
|
|
19185
|
+
displayName: systemInfo.defaultDisplayName,
|
|
18699
19186
|
linkId: assigned.linkId,
|
|
18700
19187
|
routes,
|
|
18701
19188
|
qrPayload,
|
|
@@ -18777,7 +19264,7 @@ async function claimPairing(input) {
|
|
|
18777
19264
|
return {
|
|
18778
19265
|
link: {
|
|
18779
19266
|
link_id: identity.link_id,
|
|
18780
|
-
display_name:
|
|
19267
|
+
display_name: readLinkSystemInfo().defaultDisplayName
|
|
18781
19268
|
},
|
|
18782
19269
|
device: formatDevice(session.device),
|
|
18783
19270
|
access_token: {
|
|
@@ -18823,12 +19310,12 @@ async function patchServerJson(serverBaseUrl, path24, token, body) {
|
|
|
18823
19310
|
async function readJsonResponse2(response) {
|
|
18824
19311
|
const payload = await response.json().catch(() => null);
|
|
18825
19312
|
if (!response.ok || !payload) {
|
|
18826
|
-
const message =
|
|
19313
|
+
const message = readErrorMessage5(payload) ?? `HermesPilot Server request failed with HTTP ${response.status}`;
|
|
18827
19314
|
throw new LinkHttpError(response.status, "server_request_failed", message);
|
|
18828
19315
|
}
|
|
18829
19316
|
return payload;
|
|
18830
19317
|
}
|
|
18831
|
-
function
|
|
19318
|
+
function readErrorMessage5(payload) {
|
|
18832
19319
|
if (typeof payload !== "object" || payload === null) {
|
|
18833
19320
|
return null;
|
|
18834
19321
|
}
|
|
@@ -18839,9 +19326,6 @@ function readErrorMessage4(payload) {
|
|
|
18839
19326
|
const message = error.message;
|
|
18840
19327
|
return typeof message === "string" ? message : null;
|
|
18841
19328
|
}
|
|
18842
|
-
function defaultDisplayName() {
|
|
18843
|
-
return `Hermes Link ${process.platform}`;
|
|
18844
|
-
}
|
|
18845
19329
|
function pairingClaimPath(sessionId, paths) {
|
|
18846
19330
|
return path23.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
|
|
18847
19331
|
}
|
|
@@ -18876,7 +19360,8 @@ function registerSystemRoutes(router, options) {
|
|
|
18876
19360
|
relayBaseUrl: config.relayBaseUrl,
|
|
18877
19361
|
linkId: identity.link_id,
|
|
18878
19362
|
installId: identity.install_id,
|
|
18879
|
-
publicKeyPem: identity.public_key_pem
|
|
19363
|
+
publicKeyPem: identity.public_key_pem,
|
|
19364
|
+
configuredLanHost: config.lanHost
|
|
18880
19365
|
}) : null;
|
|
18881
19366
|
ctx.set("cache-control", "no-store");
|
|
18882
19367
|
ctx.body = {
|
|
@@ -19952,14 +20437,18 @@ export {
|
|
|
19952
20437
|
getLinkLogFile,
|
|
19953
20438
|
ensureHermesApiServerAvailable,
|
|
19954
20439
|
loadConfig,
|
|
20440
|
+
saveConfig,
|
|
20441
|
+
normalizeLanHost,
|
|
19955
20442
|
loadIdentity,
|
|
19956
20443
|
ensureIdentity,
|
|
19957
20444
|
getIdentityStatus,
|
|
19958
20445
|
hasActiveDevices,
|
|
20446
|
+
detectRuntimeEnvironment,
|
|
19959
20447
|
preparePairing,
|
|
19960
20448
|
readPairingClaim,
|
|
19961
20449
|
clearPairingClaim,
|
|
19962
20450
|
createApp,
|
|
20451
|
+
reportLinkStatusToServer,
|
|
19963
20452
|
startLinkService,
|
|
19964
20453
|
startDaemonProcess,
|
|
19965
20454
|
runDaemonSupervisor,
|