herdr-remote 0.2.18 → 0.2.19
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/bin/herdr-remote.js +3 -225
- package/dist/agent-status.js +130 -0
- package/dist/binary-plist.js +157 -0
- package/dist/cli.js +255 -0
- package/dist/config.js +270 -0
- package/dist/connector/agent-watch.js +110 -0
- package/dist/connector/font-server.js +165 -0
- package/dist/connector/host-connector.js +430 -0
- package/dist/connector/lock.js +66 -0
- package/dist/connector/main.js +18 -0
- package/dist/connector/sessions.js +366 -0
- package/dist/connector/update-report.js +67 -0
- package/dist/connector/wire.js +42 -0
- package/{src → dist}/exit-codes.js +1 -4
- package/dist/font-subset.js +110 -0
- package/dist/herdr-api.js +246 -0
- package/dist/herdr-command.js +223 -0
- package/dist/herdr-plugin.js +116 -0
- package/dist/herdr-server.js +189 -0
- package/dist/i18n/en.js +223 -0
- package/dist/i18n/index.js +48 -0
- package/dist/i18n/zh.js +222 -0
- package/dist/keepalive/detached.js +78 -0
- package/dist/keepalive/environment.js +44 -0
- package/dist/keepalive/index.js +110 -0
- package/dist/keepalive/launchd.js +117 -0
- package/dist/keepalive/systemd.js +108 -0
- package/dist/keepalive/types.js +1 -0
- package/dist/lib/process.js +12 -0
- package/dist/lifecycle.js +59 -0
- package/dist/net-interfaces.js +73 -0
- package/dist/pasted-files.js +115 -0
- package/dist/paths.js +62 -0
- package/dist/pty-session.js +96 -0
- package/dist/relay-client.js +106 -0
- package/dist/relay-urls.js +99 -0
- package/dist/runtime.js +106 -0
- package/dist/service.js +260 -0
- package/dist/settings-model.js +298 -0
- package/dist/socket-discovery.js +48 -0
- package/dist/supervisor.js +319 -0
- package/dist/terminal-font/deps.js +73 -0
- package/dist/terminal-font/files.js +354 -0
- package/dist/terminal-font/formats.js +335 -0
- package/dist/terminal-font/index.js +187 -0
- package/dist/terminal-font/readers.js +316 -0
- package/dist/terminal-font/terminals.js +128 -0
- package/dist/terminal-palette.js +339 -0
- package/dist/tui/App.js +195 -0
- package/dist/tui/api.js +79 -0
- package/dist/tui/components/TextField.js +50 -0
- package/dist/tui/components/common.js +80 -0
- package/dist/tui/index.js +36 -0
- package/dist/tui/mouse/index.js +164 -0
- package/dist/tui/mouse/protocol.js +96 -0
- package/dist/tui/mouse/source.js +74 -0
- package/dist/tui/screens/About.js +166 -0
- package/dist/tui/screens/Herdr.js +74 -0
- package/dist/tui/screens/Keepalive.js +84 -0
- package/dist/tui/screens/Overview.js +36 -0
- package/dist/tui/screens/Pair.js +74 -0
- package/dist/tui/screens/Relay.js +242 -0
- package/dist/tui/screens/Services.js +63 -0
- package/dist/tui/screens/Wizard.js +235 -0
- package/dist/tui/terminal.js +39 -0
- package/dist/tui/theme.js +33 -0
- package/dist/updater.js +382 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +8 -8
- package/dist/tui.mjs +0 -5775
- package/src/agent-status.js +0 -139
- package/src/binary-plist.js +0 -146
- package/src/config.js +0 -454
- package/src/font-subset.js +0 -110
- package/src/herdr-api.js +0 -244
- package/src/herdr-command.js +0 -231
- package/src/herdr-plugin.js +0 -130
- package/src/herdr-server.js +0 -201
- package/src/host-connector.js +0 -1069
- package/src/i18n/en.js +0 -240
- package/src/i18n/index.js +0 -56
- package/src/i18n/zh.js +0 -239
- package/src/keepalive.js +0 -460
- package/src/lifecycle.js +0 -62
- package/src/net-interfaces.js +0 -76
- package/src/pasted-files.js +0 -176
- package/src/pty-session.js +0 -92
- package/src/service.js +0 -530
- package/src/settings-model.js +0 -323
- package/src/socket-discovery.js +0 -60
- package/src/state.js +0 -47
- package/src/supervisor.js +0 -250
- package/src/terminal-font.js +0 -1204
- package/src/terminal-palette.js +0 -325
- package/src/updater.js +0 -394
package/src/config.js
DELETED
|
@@ -1,454 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const os = require('node:os');
|
|
5
|
-
const path = require('node:path');
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Locate the package directory by walking up to our own package.json.
|
|
9
|
-
*
|
|
10
|
-
* `path.resolve(__dirname, '..')` was wrong as soon as this module started
|
|
11
|
-
* being bundled into dist/tui.js, because the bundle sits at a different depth
|
|
12
|
-
* than src/. Everything that matters — the plugin manifest, the host connector
|
|
13
|
-
* entry point, the systemd unit's ExecStart — is resolved from this value, so
|
|
14
|
-
* it has to be independent of where the code happens to be loaded from.
|
|
15
|
-
*/
|
|
16
|
-
function findPackageRoot(start) {
|
|
17
|
-
let directory = start;
|
|
18
|
-
for (let depth = 0; depth < 8; depth += 1) {
|
|
19
|
-
const candidate = path.join(directory, 'package.json');
|
|
20
|
-
try {
|
|
21
|
-
if (JSON.parse(fs.readFileSync(candidate, 'utf8')).name === 'herdr-remote') return directory;
|
|
22
|
-
} catch {
|
|
23
|
-
// Keep climbing: a missing or unrelated package.json is expected.
|
|
24
|
-
}
|
|
25
|
-
const parent = path.dirname(directory);
|
|
26
|
-
if (parent === directory) break;
|
|
27
|
-
directory = parent;
|
|
28
|
-
}
|
|
29
|
-
return path.resolve(start, '..');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const PACKAGE_ROOT = findPackageRoot(__dirname);
|
|
33
|
-
|
|
34
|
-
// Access modes
|
|
35
|
-
// local — a relay runs on this machine, reachable only from this machine.
|
|
36
|
-
// lan — a relay runs on this machine, bound to every interface so other
|
|
37
|
-
// devices on the LAN (or on a Tailscale/WireGuard overlay) reach it.
|
|
38
|
-
// remote — no local relay; the host connector dials an operator-run relay,
|
|
39
|
-
// which is the only way in from outside the local network.
|
|
40
|
-
const ACCESS_MODES = ['local', 'lan', 'remote'];
|
|
41
|
-
|
|
42
|
-
// The relay this project runs for people who do not want to host one. Offered
|
|
43
|
-
// as a one-keystroke choice during setup; it is never selected automatically,
|
|
44
|
-
// because routing a terminal through someone else's server has to be a
|
|
45
|
-
// deliberate decision. It is a plain public relay: no join password, and each
|
|
46
|
-
// workstation stays reachable only through its own host token.
|
|
47
|
-
const OFFICIAL_RELAY_URL = 'wss://herdr-remote.564616.xyz';
|
|
48
|
-
const LANGUAGES = ['auto', 'zh', 'en'];
|
|
49
|
-
const KEEPALIVE_MANAGERS = ['auto', 'systemd', 'launchd', 'supervisor', 'none'];
|
|
50
|
-
|
|
51
|
-
const DEFAULTS = {
|
|
52
|
-
ui: {
|
|
53
|
-
language: 'auto',
|
|
54
|
-
},
|
|
55
|
-
relay: {
|
|
56
|
-
mode: 'local',
|
|
57
|
-
port: 8787,
|
|
58
|
-
// Address advertised in pairing URLs when mode is "lan". Empty means "pick
|
|
59
|
-
// the first non-internal IPv4 automatically".
|
|
60
|
-
lanHost: '',
|
|
61
|
-
// Manual override for the URL browsers open. Empty means "derive it".
|
|
62
|
-
publicUrl: '',
|
|
63
|
-
// Operator-run relay, e.g. wss://herdr.example.com (mode "remote" only).
|
|
64
|
-
remoteUrl: '',
|
|
65
|
-
// Kept in step with the relay package's own default: the CLI starts a
|
|
66
|
-
// relay of its own, and a smaller ceiling here would reject an upload the
|
|
67
|
-
// hosted relay accepts.
|
|
68
|
-
maxPayloadBytes: 5 * 1024 * 1024,
|
|
69
|
-
maxClientsPerHost: 16,
|
|
70
|
-
maxHosts: 1024,
|
|
71
|
-
maxPendingHandshakes: 1024,
|
|
72
|
-
maxBufferedBytesPerClient: 4 * 1024 * 1024,
|
|
73
|
-
hostReconnectGraceMs: 30 * 1000,
|
|
74
|
-
allowedOrigins: [],
|
|
75
|
-
},
|
|
76
|
-
herdr: {
|
|
77
|
-
socketPath: null,
|
|
78
|
-
args: [],
|
|
79
|
-
cwd: os.homedir(),
|
|
80
|
-
// Start Herdr's server when herdr-remote starts (at boot, under keep-alive).
|
|
81
|
-
// Off: Herdr starts when the user runs it, or confirms it from the WebUI.
|
|
82
|
-
autoStart: false,
|
|
83
|
-
},
|
|
84
|
-
auth: {
|
|
85
|
-
pairingTtlMs: 10 * 60 * 1000,
|
|
86
|
-
deviceTtlMs: 30 * 24 * 60 * 60 * 1000,
|
|
87
|
-
maxDevices: 32,
|
|
88
|
-
},
|
|
89
|
-
cleanup: {
|
|
90
|
-
intervalMs: 60 * 1000,
|
|
91
|
-
heartbeatIntervalMs: 30 * 1000,
|
|
92
|
-
staleAfterMs: 90 * 1000,
|
|
93
|
-
},
|
|
94
|
-
keepalive: {
|
|
95
|
-
manager: 'auto',
|
|
96
|
-
},
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
function clone(value) {
|
|
100
|
-
return JSON.parse(JSON.stringify(value));
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* The canonical configuration directory.
|
|
105
|
-
*
|
|
106
|
-
* Deliberately NOT $HERDR_PLUGIN_CONFIG_DIR: Herdr sets that variable only when
|
|
107
|
-
* it launches the plugin itself, so honouring it would give "herdr-remote" run
|
|
108
|
-
* from a shell and the same tool run from a Herdr pane two different config
|
|
109
|
-
* files. One path, one config; `migrateLegacyConfig()` imports the old one.
|
|
110
|
-
*/
|
|
111
|
-
function configDir() {
|
|
112
|
-
return process.env.HERDR_REMOTE_CONFIG_DIR || path.join(os.homedir(), '.config', 'herdr-remote');
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function stateDir() {
|
|
116
|
-
return process.env.HERDR_REMOTE_STATE_DIR || path.join(os.homedir(), '.local', 'state', 'herdr-remote');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function configPath() {
|
|
120
|
-
return path.join(configDir(), 'config.json');
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function runtimeStatePath() {
|
|
124
|
-
return path.join(stateDir(), 'runtime.json');
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function legacyConfigPath() {
|
|
128
|
-
return process.env.HERDR_PLUGIN_CONFIG_DIR
|
|
129
|
-
? path.join(process.env.HERDR_PLUGIN_CONFIG_DIR, 'config.json')
|
|
130
|
-
: null;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function readJson(filePath) {
|
|
134
|
-
try {
|
|
135
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
136
|
-
} catch (error) {
|
|
137
|
-
if (error.code !== 'ENOENT') {
|
|
138
|
-
process.stderr.write(`herdr-remote: ignoring invalid JSON at ${filePath}: ${error.message}\n`);
|
|
139
|
-
}
|
|
140
|
-
return {};
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Copy a config written by an older, plugin-scoped install into the canonical
|
|
146
|
-
* location. Runs at most once: it never overwrites an existing config.
|
|
147
|
-
*/
|
|
148
|
-
function migrateLegacyConfig() {
|
|
149
|
-
const target = configPath();
|
|
150
|
-
if (fs.existsSync(target)) return { migrated: false, reason: 'config already exists' };
|
|
151
|
-
const legacy = legacyConfigPath();
|
|
152
|
-
if (!legacy || !fs.existsSync(legacy)) return { migrated: false, reason: 'no legacy config' };
|
|
153
|
-
try {
|
|
154
|
-
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
155
|
-
fs.copyFileSync(legacy, target);
|
|
156
|
-
return { migrated: true, from: legacy, to: target };
|
|
157
|
-
} catch (error) {
|
|
158
|
-
return { migrated: false, reason: error.message };
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function mergeConfig(fileConfig) {
|
|
163
|
-
const config = clone(DEFAULTS);
|
|
164
|
-
for (const section of Object.keys(config)) {
|
|
165
|
-
if (fileConfig && fileConfig[section] && typeof fileConfig[section] === 'object') {
|
|
166
|
-
Object.assign(config[section], fileConfig[section]);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
return config;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Accept configs written before access modes existed. The 0.1 schema used
|
|
174
|
-
* `relay.local` (boolean), `relay.host` (bind address) and `relay.url`.
|
|
175
|
-
*/
|
|
176
|
-
function normalizeLegacyFields(config, fileConfig) {
|
|
177
|
-
const legacy = (fileConfig && fileConfig.relay) || {};
|
|
178
|
-
// The absence of an explicit `mode` — not an invalid one — is what marks a
|
|
179
|
-
// file as pre-0.2, since merging defaults always leaves a valid mode behind.
|
|
180
|
-
const hasExplicitMode = ACCESS_MODES.includes(legacy.mode);
|
|
181
|
-
if (!hasExplicitMode) {
|
|
182
|
-
if (legacy.local === false && typeof legacy.url === 'string' && legacy.url) {
|
|
183
|
-
config.relay.mode = 'remote';
|
|
184
|
-
if (!config.relay.remoteUrl) config.relay.remoteUrl = legacy.url;
|
|
185
|
-
} else if (typeof legacy.host === 'string' && legacy.host && !isLoopbackHost(legacy.host)) {
|
|
186
|
-
config.relay.mode = 'lan';
|
|
187
|
-
} else {
|
|
188
|
-
config.relay.mode = 'local';
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
if (config.relay.mode === 'lan' && !config.relay.lanHost && typeof legacy.host === 'string' && !isLoopbackHost(legacy.host) && legacy.host !== '0.0.0.0') {
|
|
192
|
-
config.relay.lanHost = legacy.host;
|
|
193
|
-
}
|
|
194
|
-
// Superseded keys copied in by the merge would otherwise reappear in the
|
|
195
|
-
// in-memory config and confuse anything reading it.
|
|
196
|
-
delete config.relay.local;
|
|
197
|
-
delete config.relay.host;
|
|
198
|
-
delete config.relay.url;
|
|
199
|
-
// `publicUrl` used to be mandatory and defaulted to the loopback URL; treat
|
|
200
|
-
// that default as "no override" so derivation can take over.
|
|
201
|
-
if (typeof config.relay.publicUrl === 'string' && /^https?:\/\/(127\.0\.0\.1|localhost)(:\d+)?\/?$/.test(config.relay.publicUrl)) {
|
|
202
|
-
config.relay.publicUrl = '';
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function parseInteger(value, fallback, min, max) {
|
|
207
|
-
const numeric = Number(value);
|
|
208
|
-
if (!Number.isInteger(numeric) || numeric < min || numeric > max) return fallback;
|
|
209
|
-
return numeric;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function isLoopbackHost(value) {
|
|
213
|
-
return value === '127.0.0.1' || value === 'localhost' || value === '::1' || value === '[::1]';
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function isUnspecifiedAddress(value) {
|
|
217
|
-
const address = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
218
|
-
return address === '0.0.0.0' || address === '::' || address === '[::]';
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/** An unspecified host is valid for a server bind, but not for a browser URL. */
|
|
222
|
-
function isUnspecifiedHost(value) {
|
|
223
|
-
try {
|
|
224
|
-
const hostname = new URL(value).hostname.toLowerCase();
|
|
225
|
-
return hostname === '0.0.0.0' || hostname === '::' || hostname === '[::]';
|
|
226
|
-
} catch {
|
|
227
|
-
return false;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function normalizeUrl(value) {
|
|
232
|
-
return typeof value === 'string' ? value.trim().replace(/\/+$/, '') : '';
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function validate(config) {
|
|
236
|
-
if (!ACCESS_MODES.includes(config.relay.mode)) config.relay.mode = DEFAULTS.relay.mode;
|
|
237
|
-
if (!LANGUAGES.includes(config.ui.language)) config.ui.language = DEFAULTS.ui.language;
|
|
238
|
-
if (!KEEPALIVE_MANAGERS.includes(config.keepalive.manager)) config.keepalive.manager = DEFAULTS.keepalive.manager;
|
|
239
|
-
|
|
240
|
-
config.relay.port = parseInteger(config.relay.port, DEFAULTS.relay.port, 1, 65535);
|
|
241
|
-
config.relay.maxPayloadBytes = parseInteger(config.relay.maxPayloadBytes, DEFAULTS.relay.maxPayloadBytes, 4096, 16 * 1024 * 1024);
|
|
242
|
-
config.relay.maxClientsPerHost = parseInteger(config.relay.maxClientsPerHost, DEFAULTS.relay.maxClientsPerHost, 1, 256);
|
|
243
|
-
config.relay.maxHosts = parseInteger(config.relay.maxHosts, DEFAULTS.relay.maxHosts, 1, 100000);
|
|
244
|
-
config.relay.maxPendingHandshakes = parseInteger(config.relay.maxPendingHandshakes, DEFAULTS.relay.maxPendingHandshakes, 16, 100000);
|
|
245
|
-
config.relay.maxBufferedBytesPerClient = parseInteger(
|
|
246
|
-
config.relay.maxBufferedBytesPerClient,
|
|
247
|
-
DEFAULTS.relay.maxBufferedBytesPerClient,
|
|
248
|
-
64 * 1024,
|
|
249
|
-
256 * 1024 * 1024,
|
|
250
|
-
);
|
|
251
|
-
config.relay.hostReconnectGraceMs = parseInteger(
|
|
252
|
-
config.relay.hostReconnectGraceMs,
|
|
253
|
-
DEFAULTS.relay.hostReconnectGraceMs,
|
|
254
|
-
1000,
|
|
255
|
-
24 * 60 * 60 * 1000,
|
|
256
|
-
);
|
|
257
|
-
config.auth.pairingTtlMs = parseInteger(config.auth.pairingTtlMs, DEFAULTS.auth.pairingTtlMs, 30 * 1000, 24 * 60 * 60 * 1000);
|
|
258
|
-
config.auth.deviceTtlMs = parseInteger(config.auth.deviceTtlMs, DEFAULTS.auth.deviceTtlMs, 60 * 1000, 365 * 24 * 60 * 60 * 1000);
|
|
259
|
-
config.auth.maxDevices = parseInteger(config.auth.maxDevices, DEFAULTS.auth.maxDevices, 1, 10000);
|
|
260
|
-
config.cleanup.intervalMs = parseInteger(config.cleanup.intervalMs, DEFAULTS.cleanup.intervalMs, 1000, 24 * 60 * 60 * 1000);
|
|
261
|
-
config.cleanup.heartbeatIntervalMs = parseInteger(config.cleanup.heartbeatIntervalMs, DEFAULTS.cleanup.heartbeatIntervalMs, 1000, 10 * 60 * 1000);
|
|
262
|
-
config.cleanup.staleAfterMs = parseInteger(config.cleanup.staleAfterMs, DEFAULTS.cleanup.staleAfterMs, config.cleanup.heartbeatIntervalMs * 2, 24 * 60 * 60 * 1000);
|
|
263
|
-
|
|
264
|
-
config.relay.publicUrl = normalizeUrl(config.relay.publicUrl);
|
|
265
|
-
config.relay.remoteUrl = normalizeUrl(config.relay.remoteUrl);
|
|
266
|
-
config.relay.lanHost = typeof config.relay.lanHost === 'string' ? config.relay.lanHost.trim() : '';
|
|
267
|
-
// A LAN relay listens on every interface, but a loopback or wildcard value
|
|
268
|
-
// cannot be opened by another device. Clear stale values from older TUI
|
|
269
|
-
// versions so the advertised URL falls back to a real interface address.
|
|
270
|
-
if (config.relay.mode === 'lan'
|
|
271
|
-
&& (isLoopbackHost(config.relay.lanHost) || isUnspecifiedAddress(config.relay.lanHost))) {
|
|
272
|
-
config.relay.lanHost = '';
|
|
273
|
-
}
|
|
274
|
-
// 0.0.0.0 is a server-side bind wildcard, never a destination a browser can
|
|
275
|
-
// open. Treat it like an empty override so LAN mode derives the real address.
|
|
276
|
-
if (isUnspecifiedHost(config.relay.publicUrl)) config.relay.publicUrl = '';
|
|
277
|
-
if (!Array.isArray(config.relay.allowedOrigins)) config.relay.allowedOrigins = [];
|
|
278
|
-
|
|
279
|
-
if (!Array.isArray(config.herdr.args) || !config.herdr.args.every((arg) => typeof arg === 'string')) {
|
|
280
|
-
config.herdr.args = [];
|
|
281
|
-
}
|
|
282
|
-
// Herdr 0.9.0 removed --no-session; retaining it in saved configs causes
|
|
283
|
-
// session creation to fail. Filter it out if present.
|
|
284
|
-
if (config.herdr.args.includes('--no-session')) {
|
|
285
|
-
config.herdr.args = config.herdr.args.filter((arg) => arg !== '--no-session');
|
|
286
|
-
process.stderr.write('herdr-remote: ignoring removed --no-session argument (removed in Herdr 0.9.0)\n');
|
|
287
|
-
}
|
|
288
|
-
if (typeof config.herdr.socketPath !== 'string' || config.herdr.socketPath.length === 0) {
|
|
289
|
-
config.herdr.socketPath = null;
|
|
290
|
-
}
|
|
291
|
-
if (typeof config.herdr.cwd !== 'string' || config.herdr.cwd.length === 0) {
|
|
292
|
-
config.herdr.cwd = os.homedir();
|
|
293
|
-
}
|
|
294
|
-
config.herdr.autoStart = config.herdr.autoStart === true;
|
|
295
|
-
|
|
296
|
-
// A "remote" config without a relay URL cannot reach anything; fall back to
|
|
297
|
-
// local rather than silently starting a host connector that dials nowhere.
|
|
298
|
-
if (config.relay.mode === 'remote' && !config.relay.remoteUrl) {
|
|
299
|
-
config.relay.mode = 'local';
|
|
300
|
-
}
|
|
301
|
-
return config;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function applyEnvironment(config) {
|
|
305
|
-
const env = process.env;
|
|
306
|
-
if (env.HERDR_REMOTE_LANG && LANGUAGES.includes(env.HERDR_REMOTE_LANG)) config.ui.language = env.HERDR_REMOTE_LANG;
|
|
307
|
-
if (env.HERDR_REMOTE_MODE && ACCESS_MODES.includes(env.HERDR_REMOTE_MODE)) config.relay.mode = env.HERDR_REMOTE_MODE;
|
|
308
|
-
if (env.RELAY_PORT) config.relay.port = env.RELAY_PORT;
|
|
309
|
-
if (env.RELAY_PUBLIC_URL) config.relay.publicUrl = env.RELAY_PUBLIC_URL;
|
|
310
|
-
if (env.RELAY_REMOTE_URL) config.relay.remoteUrl = env.RELAY_REMOTE_URL;
|
|
311
|
-
if (env.RELAY_MAX_HOSTS) config.relay.maxHosts = env.RELAY_MAX_HOSTS;
|
|
312
|
-
if (env.RELAY_MAX_PENDING_HANDSHAKES) config.relay.maxPendingHandshakes = env.RELAY_MAX_PENDING_HANDSHAKES;
|
|
313
|
-
if (env.RELAY_MAX_BUFFERED_BYTES_PER_CLIENT) config.relay.maxBufferedBytesPerClient = env.RELAY_MAX_BUFFERED_BYTES_PER_CLIENT;
|
|
314
|
-
if (env.RELAY_HOST_RECONNECT_GRACE_MS) config.relay.hostReconnectGraceMs = env.RELAY_HOST_RECONNECT_GRACE_MS;
|
|
315
|
-
if (env.HERDR_SOCKET_PATH) config.herdr.socketPath = env.HERDR_SOCKET_PATH;
|
|
316
|
-
if (env.HERDR_CWD) config.herdr.cwd = env.HERDR_CWD;
|
|
317
|
-
if (env.HERDR_ARGS_JSON) {
|
|
318
|
-
try {
|
|
319
|
-
const args = JSON.parse(env.HERDR_ARGS_JSON);
|
|
320
|
-
if (Array.isArray(args)) config.herdr.args = args;
|
|
321
|
-
} catch (error) {
|
|
322
|
-
process.stderr.write(`herdr-remote: invalid HERDR_ARGS_JSON: ${error.message}\n`);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function loadConfig() {
|
|
328
|
-
const fileConfig = readJson(configPath());
|
|
329
|
-
const config = mergeConfig(fileConfig);
|
|
330
|
-
normalizeLegacyFields(config, fileConfig);
|
|
331
|
-
applyEnvironment(config);
|
|
332
|
-
validate(config);
|
|
333
|
-
return config;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function configExists() {
|
|
337
|
-
return fs.existsSync(configPath());
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// ---------------------------------------------------------------------------
|
|
341
|
-
// Derived values
|
|
342
|
-
// ---------------------------------------------------------------------------
|
|
343
|
-
|
|
344
|
-
/** Whether this machine runs its own relay process. */
|
|
345
|
-
function runsLocalRelay(config) {
|
|
346
|
-
return config.relay.mode !== 'remote';
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/** Address the local relay binds to for the configured mode. */
|
|
350
|
-
function bindAddress(config) {
|
|
351
|
-
return config.relay.mode === 'lan' ? '0.0.0.0' : '127.0.0.1';
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Host name to put into pairing URLs. For "lan" this is the interface address a
|
|
356
|
-
* phone can actually reach; `lanHost` is the user's pick from the TUI, and the
|
|
357
|
-
* fallback keeps things working if that interface disappeared.
|
|
358
|
-
*/
|
|
359
|
-
function advertisedHost(config, fallbackLanHost = null) {
|
|
360
|
-
if (config.relay.mode === 'lan') {
|
|
361
|
-
const configured = config.relay.lanHost;
|
|
362
|
-
if (configured && !isLoopbackHost(configured) && !isUnspecifiedAddress(configured)) return configured;
|
|
363
|
-
if (fallbackLanHost && !isLoopbackHost(fallbackLanHost) && !isUnspecifiedAddress(fallbackLanHost)) {
|
|
364
|
-
return fallbackLanHost;
|
|
365
|
-
}
|
|
366
|
-
return '127.0.0.1';
|
|
367
|
-
}
|
|
368
|
-
return '127.0.0.1';
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/** The URL a browser opens. */
|
|
372
|
-
function resolvePublicUrl(config, fallbackLanHost = null) {
|
|
373
|
-
if (config.relay.publicUrl) return config.relay.publicUrl;
|
|
374
|
-
if (config.relay.mode === 'remote') return httpOrigin(config.relay.remoteUrl);
|
|
375
|
-
return `http://${advertisedHost(config, fallbackLanHost)}:${config.relay.port}`;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/** Base HTTP origin used for relay admin calls (pairing, health). */
|
|
379
|
-
function resolveAdminOrigin(config) {
|
|
380
|
-
if (config.relay.mode === 'remote') return httpOrigin(config.relay.remoteUrl);
|
|
381
|
-
return `http://127.0.0.1:${config.relay.port}`;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/** WebSocket URL the host connector dials. */
|
|
385
|
-
function resolveHostRelayUrl(config) {
|
|
386
|
-
const base = config.relay.mode === 'remote'
|
|
387
|
-
? config.relay.remoteUrl
|
|
388
|
-
: `ws://127.0.0.1:${config.relay.port}`;
|
|
389
|
-
return hostWebSocketUrl(base);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function httpOrigin(value) {
|
|
393
|
-
try {
|
|
394
|
-
const url = new URL(value);
|
|
395
|
-
if (url.protocol === 'ws:') url.protocol = 'http:';
|
|
396
|
-
if (url.protocol === 'wss:') url.protocol = 'https:';
|
|
397
|
-
const pathname = url.pathname.replace(/\/+$/, '');
|
|
398
|
-
return `${url.origin}${pathname}`;
|
|
399
|
-
} catch {
|
|
400
|
-
return normalizeUrl(value);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
function hostWebSocketUrl(base) {
|
|
405
|
-
const url = new URL(base);
|
|
406
|
-
if (url.protocol === 'http:') url.protocol = 'ws:';
|
|
407
|
-
if (url.protocol === 'https:') url.protocol = 'wss:';
|
|
408
|
-
const pathname = url.pathname.replace(/\/+$/, '');
|
|
409
|
-
if (!pathname || pathname === '/') {
|
|
410
|
-
url.pathname = '/ws/host';
|
|
411
|
-
} else if (!pathname.endsWith('/ws/host')) {
|
|
412
|
-
url.pathname = `${pathname}/ws/host`;
|
|
413
|
-
}
|
|
414
|
-
return url.toString();
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function clientWebSocketUrl(locationLike) {
|
|
418
|
-
const url = new URL(locationLike);
|
|
419
|
-
if (url.protocol === 'http:') url.protocol = 'ws:';
|
|
420
|
-
if (url.protocol === 'https:') url.protocol = 'wss:';
|
|
421
|
-
const pathname = url.pathname.replace(/\/+$/, '');
|
|
422
|
-
url.pathname = !pathname || pathname === '/' ? '/ws/client' : `${pathname}/ws/client`;
|
|
423
|
-
return url.toString();
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
module.exports = {
|
|
427
|
-
PACKAGE_ROOT,
|
|
428
|
-
DEFAULTS,
|
|
429
|
-
ACCESS_MODES,
|
|
430
|
-
OFFICIAL_RELAY_URL,
|
|
431
|
-
LANGUAGES,
|
|
432
|
-
KEEPALIVE_MANAGERS,
|
|
433
|
-
configDir,
|
|
434
|
-
configPath,
|
|
435
|
-
configExists,
|
|
436
|
-
stateDir,
|
|
437
|
-
runtimeStatePath,
|
|
438
|
-
legacyConfigPath,
|
|
439
|
-
migrateLegacyConfig,
|
|
440
|
-
loadConfig,
|
|
441
|
-
validate,
|
|
442
|
-
runsLocalRelay,
|
|
443
|
-
bindAddress,
|
|
444
|
-
advertisedHost,
|
|
445
|
-
resolvePublicUrl,
|
|
446
|
-
resolveAdminOrigin,
|
|
447
|
-
resolveHostRelayUrl,
|
|
448
|
-
hostWebSocketUrl,
|
|
449
|
-
clientWebSocketUrl,
|
|
450
|
-
httpOrigin,
|
|
451
|
-
isLoopbackHost,
|
|
452
|
-
isUnspecifiedAddress,
|
|
453
|
-
isUnspecifiedHost,
|
|
454
|
-
};
|
package/src/font-subset.js
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Cuts the characters a browser needs out of a large font.
|
|
5
|
-
*
|
|
6
|
-
* A CJK face is 16–20 MB — too much to send a phone whole — but a terminal
|
|
7
|
-
* only ever shows a few thousand distinct characters. HarfBuzz's subsetter
|
|
8
|
-
* (the same code Google Fonts slices its CJK families with) produces a small,
|
|
9
|
-
* valid font holding exactly the requested characters: 3,755 common Hanzi in
|
|
10
|
-
* about 1.7 MB and 50 ms, a handful of rare ones in a few KB and milliseconds.
|
|
11
|
-
*
|
|
12
|
-
* The WebAssembly build has no imports and runs synchronously. A loaded face
|
|
13
|
-
* costs its file size twice over (the file and the wasm heap), so the whole
|
|
14
|
-
* instance is dropped after a quiet minute; wasm memory never shrinks, only
|
|
15
|
-
* a new instance gives it back.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
const fs = require('node:fs');
|
|
19
|
-
|
|
20
|
-
/** How long a loaded font may sit unused before its memory is released. */
|
|
21
|
-
const IDLE_RELEASE_MS = 60_000;
|
|
22
|
-
/** Never cut more than this many characters in one go. */
|
|
23
|
-
const MAX_CODEPOINTS = 16_384;
|
|
24
|
-
const HB_MEMORY_MODE_READONLY = 1;
|
|
25
|
-
|
|
26
|
-
let wasmModule = null;
|
|
27
|
-
function loadModule() {
|
|
28
|
-
if (!wasmModule) {
|
|
29
|
-
const file = require.resolve('harfbuzzjs/dist/harfbuzz-subset.wasm');
|
|
30
|
-
wasmModule = new WebAssembly.Module(fs.readFileSync(file));
|
|
31
|
-
}
|
|
32
|
-
return wasmModule;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
class FontSubsetter {
|
|
36
|
-
constructor({ idleMs = IDLE_RELEASE_MS } = {}) {
|
|
37
|
-
this.idleMs = idleMs;
|
|
38
|
-
this.instance = null;
|
|
39
|
-
/** Loaded faces by source key: `{ pointer, face }` in the current instance. */
|
|
40
|
-
this.faces = new Map();
|
|
41
|
-
this.idleTimer = null;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
exports() {
|
|
45
|
-
if (!this.instance) {
|
|
46
|
-
this.instance = new WebAssembly.Instance(loadModule(), {}).exports;
|
|
47
|
-
// An Emscripten reactor: static constructors run here, before any call.
|
|
48
|
-
this.instance._initialize();
|
|
49
|
-
}
|
|
50
|
-
return this.instance;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
face(source) {
|
|
54
|
-
const key = `${source.path}\0${source.index || 0}`;
|
|
55
|
-
const loaded = this.faces.get(key);
|
|
56
|
-
if (loaded) return loaded.face;
|
|
57
|
-
const hb = this.exports();
|
|
58
|
-
const data = fs.readFileSync(source.path);
|
|
59
|
-
const pointer = hb.malloc(data.length);
|
|
60
|
-
if (!pointer) throw new Error('out of memory loading font');
|
|
61
|
-
new Uint8Array(hb.memory.buffer).set(data, pointer);
|
|
62
|
-
const blob = hb.hb_blob_create(pointer, data.length, HB_MEMORY_MODE_READONLY, 0, 0);
|
|
63
|
-
const face = hb.hb_face_create(blob, source.index || 0);
|
|
64
|
-
hb.hb_blob_destroy(blob);
|
|
65
|
-
this.faces.set(key, { pointer, face });
|
|
66
|
-
return face;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** A standalone font with just `codepoints` from `source` (`{ path, index }`). */
|
|
70
|
-
subset(source, codepoints) {
|
|
71
|
-
const hb = this.exports();
|
|
72
|
-
const face = this.face(source);
|
|
73
|
-
const input = hb.hb_subset_input_create_or_fail();
|
|
74
|
-
if (!input) throw new Error('font subsetting is unavailable');
|
|
75
|
-
let result = 0;
|
|
76
|
-
let subsetFace = 0;
|
|
77
|
-
try {
|
|
78
|
-
const set = hb.hb_subset_input_unicode_set(input);
|
|
79
|
-
for (const codepoint of codepoints.slice(0, MAX_CODEPOINTS)) hb.hb_set_add(set, codepoint);
|
|
80
|
-
subsetFace = hb.hb_subset_or_fail(face, input);
|
|
81
|
-
if (!subsetFace) throw new Error('the font could not be subset');
|
|
82
|
-
result = hb.hb_face_reference_blob(subsetFace);
|
|
83
|
-
const length = hb.hb_blob_get_length(result);
|
|
84
|
-
const pointer = hb.hb_blob_get_data(result, 0);
|
|
85
|
-
// Copied out: the heap may grow (and move) on the next call.
|
|
86
|
-
return Buffer.from(new Uint8Array(hb.memory.buffer, pointer, length));
|
|
87
|
-
} finally {
|
|
88
|
-
if (result) hb.hb_blob_destroy(result);
|
|
89
|
-
if (subsetFace) hb.hb_face_destroy(subsetFace);
|
|
90
|
-
hb.hb_subset_input_destroy(input);
|
|
91
|
-
this.scheduleRelease();
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
scheduleRelease() {
|
|
96
|
-
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
97
|
-
this.idleTimer = setTimeout(() => this.release(), this.idleMs);
|
|
98
|
-
this.idleTimer.unref?.();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Drop every loaded face and the instance holding them. */
|
|
102
|
-
release() {
|
|
103
|
-
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
104
|
-
this.idleTimer = null;
|
|
105
|
-
this.faces.clear();
|
|
106
|
-
this.instance = null;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
module.exports = { FontSubsetter, MAX_CODEPOINTS };
|