@hermespilot/link 0.2.6 → 0.2.8
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-72RM6Y4H.js → chunk-W4U6XJ4W.js} +875 -374
- package/dist/cli/index.js +150 -3
- 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.8";
|
|
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";
|
|
@@ -7938,6 +7972,7 @@ function upsertAgentEventProjection(events, next) {
|
|
|
7938
7972
|
...next,
|
|
7939
7973
|
id: previous.id,
|
|
7940
7974
|
title: isGenericToolTitle(next.title) ? previous.title : next.title,
|
|
7975
|
+
created_at: earliestTimestamp(previous.created_at, next.created_at),
|
|
7941
7976
|
subtitle: nextSubtitleIsFallback ? previous.subtitle ?? next.subtitle : next.subtitle ?? previous.subtitle,
|
|
7942
7977
|
detail: next.detail ?? previous.detail,
|
|
7943
7978
|
completed_at: next.completed_at ?? previous.completed_at
|
|
@@ -7946,6 +7981,17 @@ function upsertAgentEventProjection(events, next) {
|
|
|
7946
7981
|
copy[index] = merged;
|
|
7947
7982
|
return copy;
|
|
7948
7983
|
}
|
|
7984
|
+
function earliestTimestamp(left, right) {
|
|
7985
|
+
const leftTime = Date.parse(left);
|
|
7986
|
+
const rightTime = Date.parse(right);
|
|
7987
|
+
if (Number.isNaN(leftTime)) {
|
|
7988
|
+
return right;
|
|
7989
|
+
}
|
|
7990
|
+
if (Number.isNaN(rightTime)) {
|
|
7991
|
+
return left;
|
|
7992
|
+
}
|
|
7993
|
+
return leftTime <= rightTime ? left : right;
|
|
7994
|
+
}
|
|
7949
7995
|
function isGenericToolTitle(value) {
|
|
7950
7996
|
const normalized = value.trim().toLowerCase().replace(/[\s_-]+/gu, "");
|
|
7951
7997
|
return normalized === "tool" || normalized === "toolcall" || value === "\u5DE5\u5177\u8C03\u7528";
|
|
@@ -11697,7 +11743,10 @@ async function saveAssignedLinkId(linkId, paths = resolveRuntimePaths()) {
|
|
|
11697
11743
|
return next;
|
|
11698
11744
|
}
|
|
11699
11745
|
function signRelayNonce(identity, nonce) {
|
|
11700
|
-
|
|
11746
|
+
return signIdentityPayload(identity, nonce);
|
|
11747
|
+
}
|
|
11748
|
+
function signIdentityPayload(identity, payload) {
|
|
11749
|
+
const signature = sign(null, Buffer.from(payload, "utf8"), identity.private_key_pem);
|
|
11701
11750
|
return signature.toString("base64url");
|
|
11702
11751
|
}
|
|
11703
11752
|
function getIdentityStatus(identity) {
|
|
@@ -12435,6 +12484,8 @@ function registerConversationRoutes(router, options) {
|
|
|
12435
12484
|
});
|
|
12436
12485
|
router.get("/api/v1/conversations/events", async (ctx) => {
|
|
12437
12486
|
await authenticateRequest(ctx, paths);
|
|
12487
|
+
const mode = readQueryString(ctx.query.mode);
|
|
12488
|
+
const notificationOnly = mode === "notifications";
|
|
12438
12489
|
ctx.respond = false;
|
|
12439
12490
|
const response = ctx.res;
|
|
12440
12491
|
response.statusCode = 200;
|
|
@@ -12442,6 +12493,9 @@ function registerConversationRoutes(router, options) {
|
|
|
12442
12493
|
response.setHeader("cache-control", "no-store");
|
|
12443
12494
|
response.setHeader("connection", "keep-alive");
|
|
12444
12495
|
const unsubscribe = conversations.subscribeAll((event) => {
|
|
12496
|
+
if (notificationOnly && !isConversationNotificationEvent(event)) {
|
|
12497
|
+
return;
|
|
12498
|
+
}
|
|
12445
12499
|
writeSseEvent(response, event);
|
|
12446
12500
|
});
|
|
12447
12501
|
const cleanup = () => {
|
|
@@ -12679,6 +12733,16 @@ function registerConversationRoutes(router, options) {
|
|
|
12679
12733
|
}
|
|
12680
12734
|
);
|
|
12681
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
|
+
}
|
|
12682
12746
|
|
|
12683
12747
|
// src/http/middleware/error-handler.ts
|
|
12684
12748
|
function createHttpErrorMiddleware(logger) {
|
|
@@ -17561,218 +17625,807 @@ async function handleFrame(socket, raw, localPort, abortControllers) {
|
|
|
17561
17625
|
}
|
|
17562
17626
|
}
|
|
17563
17627
|
|
|
17564
|
-
// src/
|
|
17565
|
-
|
|
17566
|
-
|
|
17567
|
-
|
|
17568
|
-
|
|
17569
|
-
|
|
17570
|
-
|
|
17571
|
-
|
|
17572
|
-
|
|
17573
|
-
await syncHermesLinkCronDeliveries(
|
|
17574
|
-
options.paths,
|
|
17575
|
-
options.conversations,
|
|
17576
|
-
options.logger
|
|
17577
|
-
);
|
|
17578
|
-
} catch (error) {
|
|
17579
|
-
void options.logger.warn("cron_link_delivery_sync_failed", {
|
|
17580
|
-
error: error instanceof Error ? error.message : String(error)
|
|
17581
|
-
});
|
|
17582
|
-
} finally {
|
|
17583
|
-
running = false;
|
|
17584
|
-
}
|
|
17585
|
-
};
|
|
17586
|
-
const timer = setInterval(() => {
|
|
17587
|
-
void syncCronDeliveries();
|
|
17588
|
-
}, options.intervalMs ?? 3e4);
|
|
17589
|
-
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 });
|
|
17590
17637
|
return {
|
|
17591
|
-
|
|
17592
|
-
|
|
17593
|
-
|
|
17638
|
+
platform,
|
|
17639
|
+
hostname,
|
|
17640
|
+
osLabel,
|
|
17641
|
+
defaultDisplayName
|
|
17594
17642
|
};
|
|
17595
17643
|
}
|
|
17596
|
-
|
|
17597
|
-
|
|
17598
|
-
|
|
17599
|
-
|
|
17600
|
-
|
|
17601
|
-
const [identity, config] = await Promise.all([loadIdentity(paths), loadConfig(paths)]);
|
|
17602
|
-
await logger.info("service_starting", {
|
|
17603
|
-
port: config.port,
|
|
17604
|
-
mode: identity?.link_id ? "paired" : "local-only"
|
|
17605
|
-
});
|
|
17606
|
-
const migration = await migrateLinkDatabase(paths);
|
|
17607
|
-
if (migration.appliedVersions.length > 0) {
|
|
17608
|
-
await logger.info("database_migrated", {
|
|
17609
|
-
database_file: migration.databaseFile,
|
|
17610
|
-
applied_versions: migration.appliedVersions,
|
|
17611
|
-
current_version: migration.currentVersion
|
|
17612
|
-
});
|
|
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);
|
|
17613
17649
|
}
|
|
17614
|
-
|
|
17615
|
-
|
|
17616
|
-
|
|
17617
|
-
|
|
17618
|
-
|
|
17619
|
-
|
|
17620
|
-
|
|
17621
|
-
|
|
17622
|
-
|
|
17623
|
-
|
|
17624
|
-
await listenServer(server, config.port);
|
|
17625
|
-
} catch (error) {
|
|
17626
|
-
await logger.error("service_start_failed", {
|
|
17627
|
-
port: config.port,
|
|
17628
|
-
error: error instanceof Error ? error.message : String(error)
|
|
17629
|
-
});
|
|
17630
|
-
await logger.flush();
|
|
17631
|
-
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]));
|
|
17632
17660
|
}
|
|
17633
|
-
|
|
17634
|
-
|
|
17635
|
-
|
|
17636
|
-
|
|
17637
|
-
|
|
17638
|
-
|
|
17639
|
-
|
|
17640
|
-
|
|
17641
|
-
paths,
|
|
17642
|
-
conversations,
|
|
17643
|
-
logger
|
|
17644
|
-
});
|
|
17645
|
-
let relay = null;
|
|
17646
|
-
if (identity?.link_id) {
|
|
17647
|
-
relay = connectRelayControl({
|
|
17648
|
-
relayBaseUrl: config.relayBaseUrl,
|
|
17649
|
-
linkId: identity.link_id,
|
|
17650
|
-
localPort: config.port,
|
|
17651
|
-
maxReconnectAttempts: options.relayMaxReconnectAttempts ?? 5,
|
|
17652
|
-
backoffBaseMs: 1e3,
|
|
17653
|
-
backoffMaxMs: 3e4,
|
|
17654
|
-
onStatus: (status) => {
|
|
17655
|
-
void logger.info("relay_status", status);
|
|
17656
|
-
}
|
|
17657
|
-
});
|
|
17658
|
-
} else {
|
|
17659
|
-
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
|
+
}
|
|
17660
17669
|
}
|
|
17661
|
-
|
|
17662
|
-
|
|
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";
|
|
17663
17676
|
}
|
|
17664
|
-
|
|
17665
|
-
|
|
17666
|
-
|
|
17667
|
-
|
|
17668
|
-
|
|
17669
|
-
|
|
17670
|
-
|
|
17671
|
-
|
|
17672
|
-
|
|
17673
|
-
|
|
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 {
|
|
17674
17690
|
}
|
|
17675
|
-
}
|
|
17691
|
+
}
|
|
17692
|
+
return null;
|
|
17676
17693
|
}
|
|
17677
|
-
function
|
|
17678
|
-
|
|
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
|
+
}
|
|
17679
17705
|
}
|
|
17680
|
-
|
|
17681
|
-
|
|
17682
|
-
|
|
17683
|
-
|
|
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;
|
|
17684
17713
|
}
|
|
17685
|
-
|
|
17686
|
-
|
|
17687
|
-
|
|
17688
|
-
|
|
17689
|
-
|
|
17690
|
-
|
|
17691
|
-
if (settled) {
|
|
17692
|
-
return;
|
|
17693
|
-
}
|
|
17694
|
-
settled = true;
|
|
17695
|
-
clearTimeout(forceCloseTimer);
|
|
17696
|
-
clearTimeout(timeoutTimer);
|
|
17697
|
-
if (error) {
|
|
17698
|
-
reject(error);
|
|
17699
|
-
return;
|
|
17700
|
-
}
|
|
17701
|
-
resolve();
|
|
17702
|
-
};
|
|
17703
|
-
forceCloseTimer = setTimeout(() => {
|
|
17704
|
-
server.closeIdleConnections?.();
|
|
17705
|
-
server.closeAllConnections?.();
|
|
17706
|
-
}, 250);
|
|
17707
|
-
timeoutTimer = setTimeout(() => {
|
|
17708
|
-
server.closeAllConnections?.();
|
|
17709
|
-
settle();
|
|
17710
|
-
}, 5e3);
|
|
17711
|
-
server.close((error) => {
|
|
17712
|
-
if (error) {
|
|
17713
|
-
settle(error);
|
|
17714
|
-
return;
|
|
17715
|
-
}
|
|
17716
|
-
settle();
|
|
17717
|
-
});
|
|
17718
|
-
server.closeIdleConnections?.();
|
|
17719
|
-
});
|
|
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;
|
|
17720
17720
|
}
|
|
17721
|
-
|
|
17722
|
-
|
|
17723
|
-
|
|
17724
|
-
|
|
17725
|
-
|
|
17726
|
-
|
|
17727
|
-
const onError = (error) => {
|
|
17728
|
-
cleanup();
|
|
17729
|
-
reject(error);
|
|
17730
|
-
};
|
|
17731
|
-
const onListening = () => {
|
|
17732
|
-
cleanup();
|
|
17733
|
-
resolve();
|
|
17734
|
-
};
|
|
17735
|
-
server.once("error", onError);
|
|
17736
|
-
server.once("listening", onListening);
|
|
17737
|
-
server.listen(port);
|
|
17738
|
-
});
|
|
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;
|
|
17739
17727
|
}
|
|
17740
17728
|
|
|
17741
|
-
// src/
|
|
17742
|
-
|
|
17743
|
-
|
|
17744
|
-
|
|
17745
|
-
|
|
17746
|
-
|
|
17747
|
-
|
|
17748
|
-
|
|
17749
|
-
|
|
17750
|
-
|
|
17751
|
-
|
|
17752
|
-
|
|
17753
|
-
|
|
17754
|
-
}
|
|
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>`."
|
|
17741
|
+
};
|
|
17755
17742
|
}
|
|
17756
|
-
|
|
17757
|
-
|
|
17758
|
-
|
|
17759
|
-
|
|
17760
|
-
|
|
17761
|
-
|
|
17762
|
-
env: process.env
|
|
17763
|
-
});
|
|
17764
|
-
child.unref();
|
|
17765
|
-
for (let index = 0; index < 12; index += 1) {
|
|
17766
|
-
await wait(250);
|
|
17767
|
-
const next = await getDaemonStatus(paths);
|
|
17768
|
-
if (next.running && (await probeLocalLinkService({ port: config.port, timeoutMs: 500 })).reachable) {
|
|
17769
|
-
return next;
|
|
17770
|
-
}
|
|
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>`."
|
|
17748
|
+
};
|
|
17771
17749
|
}
|
|
17772
|
-
return
|
|
17750
|
+
return {
|
|
17751
|
+
kind: "native",
|
|
17752
|
+
lanAutoDiscoveryUsable: true,
|
|
17753
|
+
warning: null
|
|
17754
|
+
};
|
|
17773
17755
|
}
|
|
17774
|
-
|
|
17775
|
-
|
|
17756
|
+
function isWsl(env) {
|
|
17757
|
+
if (process.platform !== "linux") {
|
|
17758
|
+
return false;
|
|
17759
|
+
}
|
|
17760
|
+
if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) {
|
|
17761
|
+
return true;
|
|
17762
|
+
}
|
|
17763
|
+
const release = os6.release().toLowerCase();
|
|
17764
|
+
return release.includes("microsoft") || release.includes("wsl");
|
|
17765
|
+
}
|
|
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 });
|
|
17776
18429
|
const log = createRotatingTextLogWriter({
|
|
17777
18430
|
paths,
|
|
17778
18431
|
fileName: path21.basename(daemonLogFile(paths))
|
|
@@ -18435,7 +19088,7 @@ async function postJson(fetcher, url, token, body) {
|
|
|
18435
19088
|
});
|
|
18436
19089
|
const payload = await response.json().catch(() => null);
|
|
18437
19090
|
if (!response.ok) {
|
|
18438
|
-
const message =
|
|
19091
|
+
const message = readErrorMessage4(payload) ?? `Relay request failed with HTTP ${response.status}`;
|
|
18439
19092
|
throw new Error(message);
|
|
18440
19093
|
}
|
|
18441
19094
|
if (!payload) {
|
|
@@ -18443,7 +19096,7 @@ async function postJson(fetcher, url, token, body) {
|
|
|
18443
19096
|
}
|
|
18444
19097
|
return payload;
|
|
18445
19098
|
}
|
|
18446
|
-
function
|
|
19099
|
+
function readErrorMessage4(payload) {
|
|
18447
19100
|
if (typeof payload !== "object" || payload === null) {
|
|
18448
19101
|
return null;
|
|
18449
19102
|
}
|
|
@@ -18455,175 +19108,17 @@ function readErrorMessage3(payload) {
|
|
|
18455
19108
|
return typeof message === "string" ? message : null;
|
|
18456
19109
|
}
|
|
18457
19110
|
|
|
18458
|
-
// src/topology/network.ts
|
|
18459
|
-
import os5 from "os";
|
|
18460
|
-
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;
|
|
18461
|
-
var MAX_LAN_IPS = 4;
|
|
18462
|
-
var MAX_PUBLIC_IPV4S = 2;
|
|
18463
|
-
var MAX_PUBLIC_IPV6S = 2;
|
|
18464
|
-
async function discoverRouteCandidates(options) {
|
|
18465
|
-
const lanIps = discoverLanIps();
|
|
18466
|
-
const publicIps = options.relayBootstrapToken ? await observePublicRoute(options).catch(() => ({ publicIpv4s: [], publicIpv6s: [] })) : { publicIpv4s: [], publicIpv6s: [] };
|
|
18467
|
-
const publicIpv4s = unique(publicIps.publicIpv4s.filter(isUsablePublicIpv4)).slice(0, MAX_PUBLIC_IPV4S);
|
|
18468
|
-
const publicIpv6s = unique(publicIps.publicIpv6s.filter(isUsablePublicIpv6)).slice(0, MAX_PUBLIC_IPV6S);
|
|
18469
|
-
const preferredUrls = [
|
|
18470
|
-
...lanIps.map((ip) => buildDirectUrl(ip, options.port)),
|
|
18471
|
-
...publicIpv4s.map((ip) => buildDirectUrl(ip, options.port)),
|
|
18472
|
-
...publicIpv6s.map((ip) => buildDirectUrl(ip, options.port)),
|
|
18473
|
-
`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/links/${options.linkId}`
|
|
18474
|
-
];
|
|
18475
|
-
return {
|
|
18476
|
-
lanIps,
|
|
18477
|
-
publicIpv4s,
|
|
18478
|
-
publicIpv6s,
|
|
18479
|
-
preferredUrls
|
|
18480
|
-
};
|
|
18481
|
-
}
|
|
18482
|
-
function discoverLanIps() {
|
|
18483
|
-
return discoverLanIpsFromInterfaces(os5.networkInterfaces());
|
|
18484
|
-
}
|
|
18485
|
-
function discoverLanIpsFromInterfaces(interfaces) {
|
|
18486
|
-
const result = /* @__PURE__ */ new Set();
|
|
18487
|
-
const candidates = [];
|
|
18488
|
-
for (const [name, items] of Object.entries(interfaces)) {
|
|
18489
|
-
if (shouldIgnoreInterface(name)) {
|
|
18490
|
-
continue;
|
|
18491
|
-
}
|
|
18492
|
-
for (const item of items ?? []) {
|
|
18493
|
-
if (!item.internal && item.address && item.family === "IPv4" && isUsableLanIpv4(item.address, item.netmask)) {
|
|
18494
|
-
candidates.push({ name, address: item.address });
|
|
18495
|
-
}
|
|
18496
|
-
}
|
|
18497
|
-
}
|
|
18498
|
-
for (const candidate of candidates.sort(compareLanCandidate)) {
|
|
18499
|
-
result.add(candidate.address);
|
|
18500
|
-
}
|
|
18501
|
-
return [...result].slice(0, MAX_LAN_IPS);
|
|
18502
|
-
}
|
|
18503
|
-
async function observePublicRoute(options) {
|
|
18504
|
-
const fetcher = options.fetchImpl ?? fetch;
|
|
18505
|
-
const response = await fetcher(`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/public-route/observe`, {
|
|
18506
|
-
method: "POST",
|
|
18507
|
-
headers: {
|
|
18508
|
-
authorization: `Bearer ${options.relayBootstrapToken}`,
|
|
18509
|
-
"content-type": "application/json"
|
|
18510
|
-
},
|
|
18511
|
-
body: JSON.stringify({
|
|
18512
|
-
install_id: options.installId,
|
|
18513
|
-
link_id: options.linkId,
|
|
18514
|
-
public_key_pem: options.publicKeyPem
|
|
18515
|
-
})
|
|
18516
|
-
});
|
|
18517
|
-
const payload = await response.json().catch(() => null);
|
|
18518
|
-
const record = typeof payload?.record === "object" && payload.record !== null ? payload.record : null;
|
|
18519
|
-
const observed = typeof payload?.observed === "object" && payload.observed !== null ? payload.observed : null;
|
|
18520
|
-
const values = [
|
|
18521
|
-
readIpRecord(record?.ipv4),
|
|
18522
|
-
readIpRecord(record?.ipv6),
|
|
18523
|
-
typeof observed?.ip === "string" ? observed.ip : null
|
|
18524
|
-
].filter((value) => Boolean(value));
|
|
18525
|
-
return {
|
|
18526
|
-
publicIpv4s: unique(values.filter(isUsablePublicIpv4)),
|
|
18527
|
-
publicIpv6s: unique(values.filter(isUsablePublicIpv6))
|
|
18528
|
-
};
|
|
18529
|
-
}
|
|
18530
|
-
function readIpRecord(value) {
|
|
18531
|
-
if (typeof value !== "object" || value === null) {
|
|
18532
|
-
return null;
|
|
18533
|
-
}
|
|
18534
|
-
const ip = value.ip;
|
|
18535
|
-
return typeof ip === "string" && ip.trim() ? ip.trim() : null;
|
|
18536
|
-
}
|
|
18537
|
-
function buildDirectUrl(ip, port) {
|
|
18538
|
-
return `http://${ip.includes(":") ? `[${ip}]` : ip}:${port}`;
|
|
18539
|
-
}
|
|
18540
|
-
function shouldIgnoreInterface(name) {
|
|
18541
|
-
return !name.trim() || VIRTUAL_INTERFACE_NAME_PATTERN.test(name);
|
|
18542
|
-
}
|
|
18543
|
-
function compareLanCandidate(left, right) {
|
|
18544
|
-
const priority = interfacePriority(left.name) - interfacePriority(right.name);
|
|
18545
|
-
return priority || left.name.localeCompare(right.name) || left.address.localeCompare(right.address);
|
|
18546
|
-
}
|
|
18547
|
-
function interfacePriority(name) {
|
|
18548
|
-
if (/^(en|eth|wlan|wi-fi|wifi)/iu.test(name)) {
|
|
18549
|
-
return 0;
|
|
18550
|
-
}
|
|
18551
|
-
return 1;
|
|
18552
|
-
}
|
|
18553
|
-
function isUsableLanIpv4(address, netmask) {
|
|
18554
|
-
return isPrivateIpv4(address) && !isNetworkOrBroadcastIpv4Address(address, netmask);
|
|
18555
|
-
}
|
|
18556
|
-
function isUsablePublicIpv4(address) {
|
|
18557
|
-
return isValidIpv4(address) && !isSpecialIpv4(address);
|
|
18558
|
-
}
|
|
18559
|
-
function isUsablePublicIpv6(address) {
|
|
18560
|
-
const normalized = address.toLowerCase();
|
|
18561
|
-
return normalized.includes(":") && !normalized.startsWith("fe80:") && !normalized.startsWith("fc") && !normalized.startsWith("fd") && !normalized.startsWith("ff") && normalized !== "::" && normalized !== "::1";
|
|
18562
|
-
}
|
|
18563
|
-
function isPrivateIpv4(address) {
|
|
18564
|
-
const parts = parseIpv4Segments(address);
|
|
18565
|
-
if (!parts) {
|
|
18566
|
-
return false;
|
|
18567
|
-
}
|
|
18568
|
-
const [first, second] = parts;
|
|
18569
|
-
return first === 10 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168;
|
|
18570
|
-
}
|
|
18571
|
-
function isSpecialIpv4(address) {
|
|
18572
|
-
const parts = parseIpv4Segments(address);
|
|
18573
|
-
if (!parts) {
|
|
18574
|
-
return true;
|
|
18575
|
-
}
|
|
18576
|
-
const [first, second, third, fourth] = parts;
|
|
18577
|
-
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;
|
|
18578
|
-
}
|
|
18579
|
-
function isNetworkOrBroadcastIpv4Address(address, netmask) {
|
|
18580
|
-
const addressParts = parseIpv4Segments(address);
|
|
18581
|
-
const netmaskParts = netmask ? parseIpv4Segments(netmask) : null;
|
|
18582
|
-
if (!addressParts) {
|
|
18583
|
-
return true;
|
|
18584
|
-
}
|
|
18585
|
-
if (!netmaskParts) {
|
|
18586
|
-
const last = addressParts[3];
|
|
18587
|
-
return last === 0 || last === 255;
|
|
18588
|
-
}
|
|
18589
|
-
const addressInt = ipv4SegmentsToInt(addressParts);
|
|
18590
|
-
const netmaskInt = ipv4SegmentsToInt(netmaskParts);
|
|
18591
|
-
const hostMask = ~netmaskInt >>> 0;
|
|
18592
|
-
if (hostMask === 0) {
|
|
18593
|
-
return false;
|
|
18594
|
-
}
|
|
18595
|
-
const networkInt = addressInt & netmaskInt;
|
|
18596
|
-
const broadcastInt = (networkInt | hostMask) >>> 0;
|
|
18597
|
-
return addressInt === networkInt || addressInt === broadcastInt;
|
|
18598
|
-
}
|
|
18599
|
-
function isValidIpv4(address) {
|
|
18600
|
-
return Boolean(parseIpv4Segments(address));
|
|
18601
|
-
}
|
|
18602
|
-
function parseIpv4Segments(address) {
|
|
18603
|
-
if (!/^\d{1,3}(?:\.\d{1,3}){3}$/u.test(address)) {
|
|
18604
|
-
return null;
|
|
18605
|
-
}
|
|
18606
|
-
const parts = address.split(".").map((part) => Number.parseInt(part, 10));
|
|
18607
|
-
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
18608
|
-
return null;
|
|
18609
|
-
}
|
|
18610
|
-
return parts;
|
|
18611
|
-
}
|
|
18612
|
-
function ipv4SegmentsToInt(parts) {
|
|
18613
|
-
return (parts[0] << 24 >>> 0 | parts[1] << 16 | parts[2] << 8 | parts[3]) >>> 0;
|
|
18614
|
-
}
|
|
18615
|
-
function unique(values) {
|
|
18616
|
-
return [...new Set(values)];
|
|
18617
|
-
}
|
|
18618
|
-
|
|
18619
19111
|
// src/pairing/pairing.ts
|
|
18620
19112
|
async function preparePairing(paths = resolveRuntimePaths()) {
|
|
18621
19113
|
const config = await loadConfig(paths);
|
|
18622
19114
|
const identity = await ensureIdentity(paths);
|
|
19115
|
+
const systemInfo = readLinkSystemInfo();
|
|
18623
19116
|
const created = await postServerJson(config.serverBaseUrl, "/api/v1/link-pairings", {
|
|
18624
19117
|
install_id: identity.install_id,
|
|
18625
19118
|
link_id: identity.link_id ?? void 0,
|
|
18626
|
-
display_name: defaultDisplayName
|
|
19119
|
+
display_name: systemInfo.defaultDisplayName,
|
|
19120
|
+
platform: systemInfo.platform,
|
|
19121
|
+
hostname: systemInfo.hostname ?? void 0,
|
|
18627
19122
|
public_key_pem: identity.public_key_pem
|
|
18628
19123
|
});
|
|
18629
19124
|
const relayBaseUrl = created.relayBaseUrl || config.relayBaseUrl;
|
|
@@ -18638,6 +19133,7 @@ async function preparePairing(paths = resolveRuntimePaths()) {
|
|
|
18638
19133
|
port: config.port,
|
|
18639
19134
|
relayBaseUrl,
|
|
18640
19135
|
relayBootstrapToken: created.relayBootstrapToken,
|
|
19136
|
+
configuredLanHost: config.lanHost,
|
|
18641
19137
|
linkId: assigned.linkId,
|
|
18642
19138
|
installId: updatedIdentity.install_id,
|
|
18643
19139
|
publicKeyPem: updatedIdentity.public_key_pem
|
|
@@ -18646,17 +19142,20 @@ async function preparePairing(paths = resolveRuntimePaths()) {
|
|
|
18646
19142
|
install_id: updatedIdentity.install_id,
|
|
18647
19143
|
link_id: assigned.linkId,
|
|
18648
19144
|
link_version: LINK_VERSION,
|
|
18649
|
-
display_name: defaultDisplayName
|
|
19145
|
+
display_name: systemInfo.defaultDisplayName,
|
|
19146
|
+
platform: systemInfo.platform,
|
|
19147
|
+
hostname: systemInfo.hostname ?? void 0,
|
|
18650
19148
|
lan_ips: routes.lanIps,
|
|
18651
19149
|
public_ipv4s: routes.publicIpv4s,
|
|
18652
19150
|
public_ipv6s: routes.publicIpv6s,
|
|
18653
|
-
preferred_urls: routes.preferredUrls
|
|
19151
|
+
preferred_urls: routes.preferredUrls,
|
|
19152
|
+
environment: routes.environment
|
|
18654
19153
|
});
|
|
18655
19154
|
const qrPayload = {
|
|
18656
19155
|
kind: "hermes_link_pairing",
|
|
18657
19156
|
version: 1,
|
|
18658
19157
|
link_id: assigned.linkId,
|
|
18659
|
-
display_name: defaultDisplayName
|
|
19158
|
+
display_name: systemInfo.defaultDisplayName,
|
|
18660
19159
|
session_id: created.sessionId,
|
|
18661
19160
|
code: created.code,
|
|
18662
19161
|
preferred_urls: qrPreferredUrls(routes)
|
|
@@ -18667,7 +19166,7 @@ async function preparePairing(paths = resolveRuntimePaths()) {
|
|
|
18667
19166
|
session_id: created.sessionId,
|
|
18668
19167
|
code: created.code,
|
|
18669
19168
|
link_id: assigned.linkId,
|
|
18670
|
-
display_name: defaultDisplayName
|
|
19169
|
+
display_name: systemInfo.defaultDisplayName,
|
|
18671
19170
|
local_api_url: `http://127.0.0.1:${config.port}`,
|
|
18672
19171
|
server_base_url: config.serverBaseUrl,
|
|
18673
19172
|
relay_base_url: relayBaseUrl,
|
|
@@ -18683,7 +19182,7 @@ async function preparePairing(paths = resolveRuntimePaths()) {
|
|
|
18683
19182
|
pairingToken: created.pairingToken,
|
|
18684
19183
|
relayBootstrapToken: created.relayBootstrapToken,
|
|
18685
19184
|
relayBaseUrl,
|
|
18686
|
-
displayName: defaultDisplayName
|
|
19185
|
+
displayName: systemInfo.defaultDisplayName,
|
|
18687
19186
|
linkId: assigned.linkId,
|
|
18688
19187
|
routes,
|
|
18689
19188
|
qrPayload,
|
|
@@ -18765,7 +19264,7 @@ async function claimPairing(input) {
|
|
|
18765
19264
|
return {
|
|
18766
19265
|
link: {
|
|
18767
19266
|
link_id: identity.link_id,
|
|
18768
|
-
display_name:
|
|
19267
|
+
display_name: readLinkSystemInfo().defaultDisplayName
|
|
18769
19268
|
},
|
|
18770
19269
|
device: formatDevice(session.device),
|
|
18771
19270
|
access_token: {
|
|
@@ -18811,12 +19310,12 @@ async function patchServerJson(serverBaseUrl, path24, token, body) {
|
|
|
18811
19310
|
async function readJsonResponse2(response) {
|
|
18812
19311
|
const payload = await response.json().catch(() => null);
|
|
18813
19312
|
if (!response.ok || !payload) {
|
|
18814
|
-
const message =
|
|
19313
|
+
const message = readErrorMessage5(payload) ?? `HermesPilot Server request failed with HTTP ${response.status}`;
|
|
18815
19314
|
throw new LinkHttpError(response.status, "server_request_failed", message);
|
|
18816
19315
|
}
|
|
18817
19316
|
return payload;
|
|
18818
19317
|
}
|
|
18819
|
-
function
|
|
19318
|
+
function readErrorMessage5(payload) {
|
|
18820
19319
|
if (typeof payload !== "object" || payload === null) {
|
|
18821
19320
|
return null;
|
|
18822
19321
|
}
|
|
@@ -18827,9 +19326,6 @@ function readErrorMessage4(payload) {
|
|
|
18827
19326
|
const message = error.message;
|
|
18828
19327
|
return typeof message === "string" ? message : null;
|
|
18829
19328
|
}
|
|
18830
|
-
function defaultDisplayName() {
|
|
18831
|
-
return `Hermes Link ${process.platform}`;
|
|
18832
|
-
}
|
|
18833
19329
|
function pairingClaimPath(sessionId, paths) {
|
|
18834
19330
|
return path23.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
|
|
18835
19331
|
}
|
|
@@ -18864,7 +19360,8 @@ function registerSystemRoutes(router, options) {
|
|
|
18864
19360
|
relayBaseUrl: config.relayBaseUrl,
|
|
18865
19361
|
linkId: identity.link_id,
|
|
18866
19362
|
installId: identity.install_id,
|
|
18867
|
-
publicKeyPem: identity.public_key_pem
|
|
19363
|
+
publicKeyPem: identity.public_key_pem,
|
|
19364
|
+
configuredLanHost: config.lanHost
|
|
18868
19365
|
}) : null;
|
|
18869
19366
|
ctx.set("cache-control", "no-store");
|
|
18870
19367
|
ctx.body = {
|
|
@@ -19940,14 +20437,18 @@ export {
|
|
|
19940
20437
|
getLinkLogFile,
|
|
19941
20438
|
ensureHermesApiServerAvailable,
|
|
19942
20439
|
loadConfig,
|
|
20440
|
+
saveConfig,
|
|
20441
|
+
normalizeLanHost,
|
|
19943
20442
|
loadIdentity,
|
|
19944
20443
|
ensureIdentity,
|
|
19945
20444
|
getIdentityStatus,
|
|
19946
20445
|
hasActiveDevices,
|
|
20446
|
+
detectRuntimeEnvironment,
|
|
19947
20447
|
preparePairing,
|
|
19948
20448
|
readPairingClaim,
|
|
19949
20449
|
clearPairingClaim,
|
|
19950
20450
|
createApp,
|
|
20451
|
+
reportLinkStatusToServer,
|
|
19951
20452
|
startLinkService,
|
|
19952
20453
|
startDaemonProcess,
|
|
19953
20454
|
runDaemonSupervisor,
|