@kortix/agent-tunnel 0.13.41 → 0.13.42
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/README.md +120 -11
- package/dist/agent-cli.js +1144 -435
- package/dist/client-cli.js +1 -3
- package/package.json +1 -1
- package/src/agent/access.test.ts +164 -0
- package/src/agent/access.ts +176 -0
- package/src/agent/agent.test.ts +641 -0
- package/src/agent/agent.ts +333 -10
- package/src/agent/capabilities/security.test.ts +20 -1
- package/src/agent/cli-device-auth.test.ts +114 -0
- package/src/agent/cli-reauth.test.ts +108 -8
- package/src/agent/cli.ts +201 -57
- package/src/agent/config.test.ts +5 -0
- package/src/agent/config.ts +17 -2
- package/src/agent/credential-probe.ts +3 -3
- package/src/agent/credential-store.ts +2 -2
- package/src/agent/device-auth.test.ts +57 -0
- package/src/agent/device-auth.ts +45 -3
- package/src/agent/security/path-validator.ts +3 -1
- package/src/agent/service-control.ts +5 -2
- package/src/agent/service-drivers.test.ts +164 -0
- package/src/agent/service-drivers.ts +205 -56
- package/src/agent/service-lifecycle.test.ts +32 -18
- package/src/agent/service-paths.ts +42 -10
- package/src/agent/service.test.ts +126 -5
- package/src/agent/service.ts +99 -12
- package/src/agent/state-file.test.ts +32 -0
- package/src/agent/state-file.ts +58 -0
- package/src/client/tunnel-client.ts +6 -3
- package/src/server/ws-handler.test.ts +31 -1
- package/src/server/ws-handler.ts +3 -3
- package/src/shared/types.ts +12 -0
package/dist/agent-cli.js
CHANGED
|
@@ -2971,19 +2971,33 @@ if (typeof globalThis.WebSocket === "undefined") {
|
|
|
2971
2971
|
}
|
|
2972
2972
|
|
|
2973
2973
|
// src/agent/cli.ts
|
|
2974
|
-
import { existsSync as
|
|
2974
|
+
import { existsSync as existsSync7, readFileSync as readFileSync9, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "fs";
|
|
2975
2975
|
import { hostname as hostname4 } from "os";
|
|
2976
|
-
import { join as
|
|
2976
|
+
import { join as join12 } from "path";
|
|
2977
2977
|
|
|
2978
2978
|
// src/agent/agent.ts
|
|
2979
|
-
import {
|
|
2979
|
+
import { spawn as spawn3 } from "child_process";
|
|
2980
|
+
import { hostname as hostname2, platform as platform2, arch, release } from "os";
|
|
2980
2981
|
|
|
2981
2982
|
// src/agent/config.ts
|
|
2982
|
-
import { chmodSync, existsSync, lstatSync, readFileSync } from "fs";
|
|
2983
|
-
import { join } from "path";
|
|
2984
|
-
import { homedir } from "os";
|
|
2983
|
+
import { chmodSync, existsSync, lstatSync, readFileSync as readFileSync2 } from "fs";
|
|
2984
|
+
import { basename, dirname, join as join2 } from "path";
|
|
2985
|
+
import { homedir as homedir2 } from "os";
|
|
2985
2986
|
|
|
2986
2987
|
// src/shared/types.ts
|
|
2988
|
+
var TunnelErrorCode = {
|
|
2989
|
+
PERMISSION_DENIED: -32000,
|
|
2990
|
+
CAPABILITY_NOT_REGISTERED: -32001,
|
|
2991
|
+
TIMEOUT: -32002,
|
|
2992
|
+
LOCAL_ERROR: -32003,
|
|
2993
|
+
NOT_CONNECTED: -32004,
|
|
2994
|
+
EXPIRED: -32005,
|
|
2995
|
+
RATE_LIMITED: -32006,
|
|
2996
|
+
AUTH_FAILED: -32007,
|
|
2997
|
+
ACCESS_PENDING: -32010,
|
|
2998
|
+
ACCESS_DENIED: -32011,
|
|
2999
|
+
ACCESS_OFF: -32012
|
|
3000
|
+
};
|
|
2987
3001
|
var TunnelMethods = {
|
|
2988
3002
|
"fs.read": "filesystem",
|
|
2989
3003
|
"fs.write": "filesystem",
|
|
@@ -3273,14 +3287,77 @@ function desktopFeatureForMethod(method, args = {}) {
|
|
|
3273
3287
|
return DESKTOP_METHOD_FEATURES[method];
|
|
3274
3288
|
}
|
|
3275
3289
|
|
|
3290
|
+
// src/agent/service-paths.ts
|
|
3291
|
+
import { createHash } from "crypto";
|
|
3292
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
3293
|
+
import { homedir } from "os";
|
|
3294
|
+
import { join, resolve } from "path";
|
|
3295
|
+
var SERVICE_LABEL = "ai.kortix.agent-tunnel";
|
|
3296
|
+
function defaultAgentTunnelHome() {
|
|
3297
|
+
return join(homedir(), ".agent-tunnel");
|
|
3298
|
+
}
|
|
3299
|
+
function agentTunnelHome(env = process.env) {
|
|
3300
|
+
const override = env.AGENT_TUNNEL_HOME?.trim();
|
|
3301
|
+
return override ? resolve(override) : defaultAgentTunnelHome();
|
|
3302
|
+
}
|
|
3303
|
+
function serviceLabelFor(home) {
|
|
3304
|
+
const dir = resolve(home);
|
|
3305
|
+
if (dir === defaultAgentTunnelHome())
|
|
3306
|
+
return SERVICE_LABEL;
|
|
3307
|
+
return `${SERVICE_LABEL}.${createHash("sha256").update(dir).digest("hex").slice(0, 8)}`;
|
|
3308
|
+
}
|
|
3309
|
+
function serviceHomeEnv(home = agentTunnelHome()) {
|
|
3310
|
+
return resolve(home) === defaultAgentTunnelHome() ? {} : { AGENT_TUNNEL_HOME: resolve(home) };
|
|
3311
|
+
}
|
|
3312
|
+
var DEFAULT_INSTALL_BACKGROUND_SERVICE = true;
|
|
3313
|
+
var MAX_SERVICE_LOG_BYTES = 5 * 1024 * 1024;
|
|
3314
|
+
var RETAINED_LOG_LINES = 500;
|
|
3315
|
+
function getServicePaths(configDir = agentTunnelHome()) {
|
|
3316
|
+
const home = homedir();
|
|
3317
|
+
const label = serviceLabelFor(configDir);
|
|
3318
|
+
const binDir = join(configDir, "bin");
|
|
3319
|
+
return {
|
|
3320
|
+
label,
|
|
3321
|
+
configDir,
|
|
3322
|
+
logDir: join(configDir, "logs"),
|
|
3323
|
+
binDir,
|
|
3324
|
+
vendoredRunner: join(binDir, "agent-cli.js"),
|
|
3325
|
+
launchdPlist: join(home, "Library", "LaunchAgents", `${label}.plist`),
|
|
3326
|
+
systemdUnit: join(home, ".config", "systemd", "user", `${label}.service`),
|
|
3327
|
+
windowsScript: join(configDir, "agent-tunnel-service.ps1")
|
|
3328
|
+
};
|
|
3329
|
+
}
|
|
3330
|
+
function serviceLogFiles(paths = getServicePaths()) {
|
|
3331
|
+
return [
|
|
3332
|
+
join(paths.logDir, "agent-tunnel.out.log"),
|
|
3333
|
+
join(paths.logDir, "agent-tunnel.err.log")
|
|
3334
|
+
];
|
|
3335
|
+
}
|
|
3336
|
+
function rotateServiceLogs(paths = getServicePaths(), maxBytes = MAX_SERVICE_LOG_BYTES) {
|
|
3337
|
+
const rotated = [];
|
|
3338
|
+
for (const file of serviceLogFiles(paths)) {
|
|
3339
|
+
try {
|
|
3340
|
+
const contents = readFileSync(file, "utf8");
|
|
3341
|
+
if (Buffer.byteLength(contents, "utf8") <= maxBytes)
|
|
3342
|
+
continue;
|
|
3343
|
+
const kept = contents.split(/\r?\n/).slice(-RETAINED_LOG_LINES).join(`
|
|
3344
|
+
`);
|
|
3345
|
+
writeFileSync(file, `[agent-tunnel] earlier entries trimmed
|
|
3346
|
+
${kept}`, { mode: 384 });
|
|
3347
|
+
rotated.push(file);
|
|
3348
|
+
} catch {}
|
|
3349
|
+
}
|
|
3350
|
+
return rotated;
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3276
3353
|
// src/agent/config.ts
|
|
3277
|
-
var CONFIG_DIR =
|
|
3278
|
-
var CONFIG_FILE =
|
|
3354
|
+
var CONFIG_DIR = agentTunnelHome();
|
|
3355
|
+
var CONFIG_FILE = join2(CONFIG_DIR, "config.json");
|
|
3279
3356
|
var DEFAULTS = {
|
|
3280
3357
|
apiUrl: "http://localhost:8080",
|
|
3281
3358
|
wsPath: "/ws",
|
|
3282
3359
|
maxFileSize: 10 * 1024 * 1024,
|
|
3283
|
-
allowedPaths: [
|
|
3360
|
+
allowedPaths: [homedir2()],
|
|
3284
3361
|
allowedCommands: [],
|
|
3285
3362
|
blockedCommands: [],
|
|
3286
3363
|
blockedPaths: [
|
|
@@ -3293,7 +3370,7 @@ var DEFAULTS = {
|
|
|
3293
3370
|
"/sys",
|
|
3294
3371
|
"/dev"
|
|
3295
3372
|
],
|
|
3296
|
-
workingDir:
|
|
3373
|
+
workingDir: homedir2(),
|
|
3297
3374
|
shellTimeout: 30000,
|
|
3298
3375
|
shellMaxTimeout: 120000,
|
|
3299
3376
|
shellMaxOutputSize: 1024 * 1024,
|
|
@@ -3420,6 +3497,12 @@ function buildTunnelWsUrl(config) {
|
|
|
3420
3497
|
});
|
|
3421
3498
|
return `${base}${wsPath}?${params.toString()}`;
|
|
3422
3499
|
}
|
|
3500
|
+
function protectedAgentPaths(home = CONFIG_DIR) {
|
|
3501
|
+
const paths = new Set([home, defaultAgentTunnelHome()]);
|
|
3502
|
+
if (basename(dirname(home)) === "agent-tunnel")
|
|
3503
|
+
paths.add(dirname(home));
|
|
3504
|
+
return [...paths];
|
|
3505
|
+
}
|
|
3423
3506
|
function loadConfig(overrides = {}) {
|
|
3424
3507
|
let fileConfig = {};
|
|
3425
3508
|
if (existsSync(CONFIG_FILE)) {
|
|
@@ -3428,7 +3511,7 @@ function loadConfig(overrides = {}) {
|
|
|
3428
3511
|
assertPrivateOwnedPath(CONFIG_DIR, "directory", 448);
|
|
3429
3512
|
assertPrivateOwnedPath(CONFIG_FILE, "file", 384);
|
|
3430
3513
|
try {
|
|
3431
|
-
const parsed = JSON.parse(
|
|
3514
|
+
const parsed = JSON.parse(readFileSync2(CONFIG_FILE, "utf-8"));
|
|
3432
3515
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3433
3516
|
throw new Error("root value must be an object");
|
|
3434
3517
|
}
|
|
@@ -3457,19 +3540,327 @@ function loadConfig(overrides = {}) {
|
|
|
3457
3540
|
merged.apiUrl = normalizeApiUrl(merged.apiUrl);
|
|
3458
3541
|
merged.wsPath = absoluteWsPath(merged.wsPath);
|
|
3459
3542
|
validateConfigValues(merged);
|
|
3543
|
+
merged.blockedPaths = [...merged.blockedPaths, ...protectedAgentPaths()];
|
|
3460
3544
|
return merged;
|
|
3461
3545
|
}
|
|
3462
3546
|
|
|
3547
|
+
// src/agent/access.ts
|
|
3548
|
+
import { spawn } from "child_process";
|
|
3549
|
+
import { randomUUID } from "crypto";
|
|
3550
|
+
import { chmodSync as chmodSync2, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, renameSync, rmSync, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
3551
|
+
import { isAbsolute, join as join3 } from "path";
|
|
3552
|
+
var ACCESS_HOLD_MS = 20000;
|
|
3553
|
+
var DESKTOP_APP_HEARTBEAT_MS = 30000;
|
|
3554
|
+
var MAX_GRANT_MS = 24 * 3600000;
|
|
3555
|
+
var MODES = ["ask", "always", "off"];
|
|
3556
|
+
var COMPAT = { mode: "always", grantedUntil: null, deniedUntil: null, keepAwake: false };
|
|
3557
|
+
var accessFilePath = (home = agentTunnelHome()) => join3(home, "access.json");
|
|
3558
|
+
var accessRequestPath = (home = agentTunnelHome()) => join3(home, "access-request.json");
|
|
3559
|
+
var desktopAppPath = (home = agentTunnelHome()) => join3(home, "desktop-app.json");
|
|
3560
|
+
var isoOrNull = (value) => typeof value === "string" && !Number.isNaN(Date.parse(value)) ? value : null;
|
|
3561
|
+
function readAccess(home = agentTunnelHome()) {
|
|
3562
|
+
let raw;
|
|
3563
|
+
try {
|
|
3564
|
+
raw = readFileSync3(accessFilePath(home), "utf8");
|
|
3565
|
+
} catch {
|
|
3566
|
+
return { ...COMPAT };
|
|
3567
|
+
}
|
|
3568
|
+
try {
|
|
3569
|
+
const parsed = JSON.parse(raw);
|
|
3570
|
+
return {
|
|
3571
|
+
mode: MODES.includes(parsed.mode) ? parsed.mode : "ask",
|
|
3572
|
+
grantedUntil: isoOrNull(parsed.grantedUntil),
|
|
3573
|
+
deniedUntil: isoOrNull(parsed.deniedUntil),
|
|
3574
|
+
keepAwake: parsed.keepAwake === true
|
|
3575
|
+
};
|
|
3576
|
+
} catch {
|
|
3577
|
+
return { mode: "ask", grantedUntil: null, deniedUntil: null, keepAwake: false };
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
function writePrivateJson(file, home, value) {
|
|
3581
|
+
mkdirSync(home, { recursive: true, mode: 448 });
|
|
3582
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
3583
|
+
writeFileSync2(tmp, `${JSON.stringify(value, null, 2)}
|
|
3584
|
+
`, { mode: 384 });
|
|
3585
|
+
try {
|
|
3586
|
+
chmodSync2(tmp, 384);
|
|
3587
|
+
} catch {}
|
|
3588
|
+
renameSync(tmp, file);
|
|
3589
|
+
}
|
|
3590
|
+
function writeAccess(state, home = agentTunnelHome()) {
|
|
3591
|
+
writePrivateJson(accessFilePath(home), home, state);
|
|
3592
|
+
}
|
|
3593
|
+
function decideAccess(state, now = Date.now()) {
|
|
3594
|
+
if (state.mode === "always")
|
|
3595
|
+
return "run";
|
|
3596
|
+
if (state.mode === "off")
|
|
3597
|
+
return "off";
|
|
3598
|
+
const denied = state.deniedUntil ? Date.parse(state.deniedUntil) : 0;
|
|
3599
|
+
if (denied > now)
|
|
3600
|
+
return "denied";
|
|
3601
|
+
const granted = state.grantedUntil ? Date.parse(state.grantedUntil) : 0;
|
|
3602
|
+
return granted > now && granted - now <= MAX_GRANT_MS ? "run" : "ask";
|
|
3603
|
+
}
|
|
3604
|
+
function writeAccessRequest(input, home = agentTunnelHome()) {
|
|
3605
|
+
const request = { id: randomUUID(), requestedAt: new Date().toISOString(), ...input };
|
|
3606
|
+
writePrivateJson(accessRequestPath(home), home, request);
|
|
3607
|
+
return request;
|
|
3608
|
+
}
|
|
3609
|
+
function readAccessRequest(home = agentTunnelHome()) {
|
|
3610
|
+
try {
|
|
3611
|
+
const parsed = JSON.parse(readFileSync3(accessRequestPath(home), "utf8"));
|
|
3612
|
+
return typeof parsed?.id === "string" ? parsed : null;
|
|
3613
|
+
} catch {
|
|
3614
|
+
return null;
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
function clearAccessRequest(id, home = agentTunnelHome()) {
|
|
3618
|
+
if (readAccessRequest(home)?.id === id)
|
|
3619
|
+
rmSync(accessRequestPath(home), { force: true });
|
|
3620
|
+
}
|
|
3621
|
+
function isAlive(pid) {
|
|
3622
|
+
if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0)
|
|
3623
|
+
return false;
|
|
3624
|
+
try {
|
|
3625
|
+
process.kill(pid, 0);
|
|
3626
|
+
return true;
|
|
3627
|
+
} catch (error) {
|
|
3628
|
+
return error.code === "EPERM";
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
function wakeDesktopApp(home = agentTunnelHome(), now = Date.now()) {
|
|
3632
|
+
let app;
|
|
3633
|
+
let touchedAt = 0;
|
|
3634
|
+
try {
|
|
3635
|
+
app = JSON.parse(readFileSync3(desktopAppPath(home), "utf8"));
|
|
3636
|
+
touchedAt = statSync(desktopAppPath(home)).mtimeMs;
|
|
3637
|
+
} catch {
|
|
3638
|
+
return "no-app";
|
|
3639
|
+
}
|
|
3640
|
+
if (isAlive(app.pid) && now - touchedAt < DESKTOP_APP_HEARTBEAT_MS)
|
|
3641
|
+
return "running";
|
|
3642
|
+
if (typeof app.command !== "string" || !isAbsolute(app.command) || !existsSync2(app.command))
|
|
3643
|
+
return "no-app";
|
|
3644
|
+
const args = Array.isArray(app.args) ? app.args.filter((arg) => typeof arg === "string") : [];
|
|
3645
|
+
const recorded = app.env && typeof app.env === "object" ? app.env : {};
|
|
3646
|
+
const env = { ...process.env };
|
|
3647
|
+
delete env.ELECTRON_RUN_AS_NODE;
|
|
3648
|
+
delete env.AGENT_TUNNEL_HOME;
|
|
3649
|
+
for (const [key, value] of Object.entries(recorded))
|
|
3650
|
+
if (typeof value === "string")
|
|
3651
|
+
env[key] = value;
|
|
3652
|
+
try {
|
|
3653
|
+
const child = spawn(app.command, args, { detached: true, stdio: "ignore", env });
|
|
3654
|
+
child.on("error", () => {});
|
|
3655
|
+
child.unref();
|
|
3656
|
+
return "launched";
|
|
3657
|
+
} catch {
|
|
3658
|
+
return "no-app";
|
|
3659
|
+
}
|
|
3660
|
+
}
|
|
3661
|
+
function keepAwakeCommand(platform, pid) {
|
|
3662
|
+
if (platform === "darwin")
|
|
3663
|
+
return { command: "caffeinate", args: ["-s", "-w", String(pid)] };
|
|
3664
|
+
if (platform === "linux") {
|
|
3665
|
+
return {
|
|
3666
|
+
command: "systemd-inhibit",
|
|
3667
|
+
args: ["--what=sleep", "--who=Kortix", "--why=Keep this computer reachable", "--mode=block", "tail", `--pid=${pid}`, "-f", "/dev/null"]
|
|
3668
|
+
};
|
|
3669
|
+
}
|
|
3670
|
+
return null;
|
|
3671
|
+
}
|
|
3672
|
+
|
|
3673
|
+
// src/agent/device-auth.ts
|
|
3674
|
+
import { execFileSync, spawn as spawn2 } from "child_process";
|
|
3675
|
+
import { hostname, platform } from "os";
|
|
3676
|
+
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3677
|
+
var TUNNEL_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
3678
|
+
var SETUP_TOKEN_PATTERN = /^kortix_tnl_[A-Za-z0-9_-]{32,64}$/;
|
|
3679
|
+
var DEVICE_CODE_PATTERN = /^[A-Z]{4}-[0-9]{4}$/;
|
|
3680
|
+
var DEVICE_SECRET_PATTERN = /^[A-Za-z0-9]{32}$/;
|
|
3681
|
+
var MAX_CHALLENGE_LIFETIME_MS = 10 * 60000;
|
|
3682
|
+
|
|
3683
|
+
class InvalidDeviceAuthResponseError extends Error {
|
|
3684
|
+
constructor(message) {
|
|
3685
|
+
super(message);
|
|
3686
|
+
this.name = "InvalidDeviceAuthResponseError";
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
function invalid(what) {
|
|
3690
|
+
throw new InvalidDeviceAuthResponseError(`Authorization server returned an invalid ${what}`);
|
|
3691
|
+
}
|
|
3692
|
+
function isJsonRecord(value) {
|
|
3693
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3694
|
+
}
|
|
3695
|
+
function normalizeBrowserUrl(value) {
|
|
3696
|
+
try {
|
|
3697
|
+
const url = new URL(value);
|
|
3698
|
+
return url.protocol === "https:" || url.protocol === "http:" ? url.toString() : null;
|
|
3699
|
+
} catch {
|
|
3700
|
+
return null;
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
function isLoopback(url) {
|
|
3704
|
+
return ["localhost", "127.0.0.1", "[::1]", "::1"].includes(url.hostname);
|
|
3705
|
+
}
|
|
3706
|
+
function assertSafeVerificationUrl(value) {
|
|
3707
|
+
if (value.length > 2048)
|
|
3708
|
+
invalid("verification URL");
|
|
3709
|
+
const browserUrl = normalizeBrowserUrl(value);
|
|
3710
|
+
if (!browserUrl)
|
|
3711
|
+
invalid("verification URL");
|
|
3712
|
+
const url = new URL(browserUrl);
|
|
3713
|
+
if (url.username || url.password || url.protocol !== "https:" && !isLoopback(url)) {
|
|
3714
|
+
throw new InvalidDeviceAuthResponseError("Authorization server returned an unsafe verification URL");
|
|
3715
|
+
}
|
|
3716
|
+
return browserUrl;
|
|
3717
|
+
}
|
|
3718
|
+
function parseDeviceAuthChallenge(value) {
|
|
3719
|
+
if (!isJsonRecord(value))
|
|
3720
|
+
invalid("challenge");
|
|
3721
|
+
const { deviceCode, deviceSecret, verificationUrl, expiresAt, pollIntervalMs } = value;
|
|
3722
|
+
if (typeof deviceCode !== "string" || !DEVICE_CODE_PATTERN.test(deviceCode))
|
|
3723
|
+
invalid("device code");
|
|
3724
|
+
if (typeof deviceSecret !== "string" || !DEVICE_SECRET_PATTERN.test(deviceSecret)) {
|
|
3725
|
+
invalid("device secret");
|
|
3726
|
+
}
|
|
3727
|
+
if (typeof verificationUrl !== "string")
|
|
3728
|
+
invalid("verification URL");
|
|
3729
|
+
if (typeof expiresAt !== "string")
|
|
3730
|
+
invalid("expiration");
|
|
3731
|
+
const expiresAtMs = Date.parse(expiresAt);
|
|
3732
|
+
const now = Date.now();
|
|
3733
|
+
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= now || expiresAtMs > now + MAX_CHALLENGE_LIFETIME_MS) {
|
|
3734
|
+
invalid("expiration");
|
|
3735
|
+
}
|
|
3736
|
+
if (!Number.isSafeInteger(pollIntervalMs) || pollIntervalMs < 250 || pollIntervalMs > 1e4) {
|
|
3737
|
+
invalid("poll interval");
|
|
3738
|
+
}
|
|
3739
|
+
return {
|
|
3740
|
+
deviceCode,
|
|
3741
|
+
deviceSecret,
|
|
3742
|
+
verificationUrl: assertSafeVerificationUrl(verificationUrl),
|
|
3743
|
+
expiresAt,
|
|
3744
|
+
pollIntervalMs
|
|
3745
|
+
};
|
|
3746
|
+
}
|
|
3747
|
+
function parseApprovedCredentials(value) {
|
|
3748
|
+
const { tunnelId, token } = value;
|
|
3749
|
+
if (typeof tunnelId !== "string" || !TUNNEL_ID_PATTERN.test(tunnelId))
|
|
3750
|
+
invalid("tunnel ID");
|
|
3751
|
+
if (typeof token !== "string" || !SETUP_TOKEN_PATTERN.test(token))
|
|
3752
|
+
invalid("setup token");
|
|
3753
|
+
return { tunnelId, token };
|
|
3754
|
+
}
|
|
3755
|
+
function parseApprovedCapabilities(value) {
|
|
3756
|
+
if (!Array.isArray(value))
|
|
3757
|
+
return [];
|
|
3758
|
+
return [...new Set(value)].filter((item) => isTunnelCapability(item));
|
|
3759
|
+
}
|
|
3760
|
+
function parseDeviceAuthStatus(value) {
|
|
3761
|
+
if (!isJsonRecord(value) || typeof value.status !== "string")
|
|
3762
|
+
invalid("status response");
|
|
3763
|
+
switch (value.status) {
|
|
3764
|
+
case "approved": {
|
|
3765
|
+
if (!value.tunnelId || !value.token)
|
|
3766
|
+
return { status: "approved-without-token" };
|
|
3767
|
+
return {
|
|
3768
|
+
status: "approved",
|
|
3769
|
+
...parseApprovedCredentials(value),
|
|
3770
|
+
capabilities: parseApprovedCapabilities(value.capabilities)
|
|
3771
|
+
};
|
|
3772
|
+
}
|
|
3773
|
+
case "denied":
|
|
3774
|
+
return { status: "denied" };
|
|
3775
|
+
case "expired":
|
|
3776
|
+
return { status: "expired" };
|
|
3777
|
+
default:
|
|
3778
|
+
return null;
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3781
|
+
function machineDisplayName({
|
|
3782
|
+
os = platform(),
|
|
3783
|
+
host = hostname(),
|
|
3784
|
+
env = process.env,
|
|
3785
|
+
run = (command, args) => execFileSync(command, args, { encoding: "utf8", timeout: 1000, stdio: ["ignore", "pipe", "ignore"] })
|
|
3786
|
+
} = {}) {
|
|
3787
|
+
const fallback = host.replace(/\.local$/i, "") || host;
|
|
3788
|
+
try {
|
|
3789
|
+
const name = os === "darwin" ? run("scutil", ["--get", "ComputerName"]) : os === "win32" ? env.COMPUTERNAME : "";
|
|
3790
|
+
return name?.trim() || fallback;
|
|
3791
|
+
} catch {
|
|
3792
|
+
return fallback;
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
async function requestDeviceAuthorization(apiUrl, options = {}) {
|
|
3796
|
+
const response = await fetch(`${apiUrl}/device-auth`, {
|
|
3797
|
+
method: "POST",
|
|
3798
|
+
headers: { "Content-Type": "application/json" },
|
|
3799
|
+
body: JSON.stringify({
|
|
3800
|
+
machineHostname: machineDisplayName(),
|
|
3801
|
+
...options.projectId ? { project_id: options.projectId } : {}
|
|
3802
|
+
})
|
|
3803
|
+
});
|
|
3804
|
+
if (!response.ok) {
|
|
3805
|
+
const body = await response.text().catch(() => "");
|
|
3806
|
+
throw new InvalidDeviceAuthResponseError(`Failed to create device auth request: ${response.status} ${body.slice(0, 200)}`);
|
|
3807
|
+
}
|
|
3808
|
+
return parseDeviceAuthChallenge(await response.json());
|
|
3809
|
+
}
|
|
3810
|
+
async function awaitDeviceAuthorization(apiUrl, challenge, options = {}) {
|
|
3811
|
+
const wait = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
3812
|
+
const expiresAtMs = Date.parse(challenge.expiresAt);
|
|
3813
|
+
for (;; ) {
|
|
3814
|
+
const secondsRemaining = Math.max(0, Math.floor((expiresAtMs - Date.now()) / 1000));
|
|
3815
|
+
if (secondsRemaining <= 0)
|
|
3816
|
+
return { status: "expired" };
|
|
3817
|
+
options.onWaiting?.(secondsRemaining);
|
|
3818
|
+
let payload;
|
|
3819
|
+
try {
|
|
3820
|
+
const response = await fetch(`${apiUrl}/device-auth/${challenge.deviceCode}/status`, {
|
|
3821
|
+
headers: { Authorization: `Bearer ${challenge.deviceSecret}` }
|
|
3822
|
+
});
|
|
3823
|
+
if (!response.ok) {
|
|
3824
|
+
await wait(challenge.pollIntervalMs);
|
|
3825
|
+
continue;
|
|
3826
|
+
}
|
|
3827
|
+
payload = await response.json();
|
|
3828
|
+
} catch {
|
|
3829
|
+
await wait(challenge.pollIntervalMs);
|
|
3830
|
+
continue;
|
|
3831
|
+
}
|
|
3832
|
+
const outcome = parseDeviceAuthStatus(payload);
|
|
3833
|
+
if (outcome)
|
|
3834
|
+
return outcome;
|
|
3835
|
+
await wait(challenge.pollIntervalMs);
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3838
|
+
function openBrowser(url) {
|
|
3839
|
+
if (process.env.KORTIX_AGENT_TUNNEL_NO_BROWSER === "1")
|
|
3840
|
+
return;
|
|
3841
|
+
const safeUrl = normalizeBrowserUrl(url);
|
|
3842
|
+
if (!safeUrl)
|
|
3843
|
+
return;
|
|
3844
|
+
const opener = {
|
|
3845
|
+
darwin: ["open", [safeUrl]],
|
|
3846
|
+
win32: ["rundll32.exe", ["url.dll,FileProtocolHandler", safeUrl]]
|
|
3847
|
+
};
|
|
3848
|
+
const [command, args] = opener[platform()] ?? ["xdg-open", [safeUrl]];
|
|
3849
|
+
try {
|
|
3850
|
+
spawn2(command, args, { detached: true, stdio: "ignore" }).unref();
|
|
3851
|
+
} catch {}
|
|
3852
|
+
}
|
|
3853
|
+
|
|
3463
3854
|
// src/agent/version.ts
|
|
3464
|
-
import { readFileSync as
|
|
3465
|
-
import { dirname, join as
|
|
3855
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
3856
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
3466
3857
|
import { fileURLToPath } from "url";
|
|
3467
3858
|
var PACKAGE_NAME = "@kortix/agent-tunnel";
|
|
3468
3859
|
var MAX_LOOKUP_DEPTH = 8;
|
|
3469
3860
|
var cached = null;
|
|
3470
3861
|
function moduleDirectory() {
|
|
3471
3862
|
try {
|
|
3472
|
-
return
|
|
3863
|
+
return dirname2(fileURLToPath(import.meta.url));
|
|
3473
3864
|
} catch {
|
|
3474
3865
|
return process.cwd();
|
|
3475
3866
|
}
|
|
@@ -3480,14 +3871,14 @@ function agentTunnelVersion() {
|
|
|
3480
3871
|
let directory = moduleDirectory();
|
|
3481
3872
|
for (let depth = 0;depth < MAX_LOOKUP_DEPTH; depth++) {
|
|
3482
3873
|
try {
|
|
3483
|
-
const manifest = JSON.parse(
|
|
3874
|
+
const manifest = JSON.parse(readFileSync4(join4(directory, "package.json"), "utf8"));
|
|
3484
3875
|
if (manifest?.name === PACKAGE_NAME && typeof manifest.version === "string") {
|
|
3485
3876
|
const version = manifest.version;
|
|
3486
3877
|
cached = version;
|
|
3487
3878
|
return version;
|
|
3488
3879
|
}
|
|
3489
3880
|
} catch {}
|
|
3490
|
-
const parent =
|
|
3881
|
+
const parent = dirname2(directory);
|
|
3491
3882
|
if (parent === directory)
|
|
3492
3883
|
break;
|
|
3493
3884
|
directory = parent;
|
|
@@ -3588,7 +3979,7 @@ class PermissionGuard {
|
|
|
3588
3979
|
}
|
|
3589
3980
|
|
|
3590
3981
|
// src/shared/crypto.ts
|
|
3591
|
-
import { createHash, createHmac, timingSafeEqual, randomInt } from "crypto";
|
|
3982
|
+
import { createHash as createHash2, createHmac, timingSafeEqual, randomInt } from "crypto";
|
|
3592
3983
|
function signMessage(signingKey, payload, nonce) {
|
|
3593
3984
|
return createHmac("sha256", signingKey).update(`${nonce}:${payload}`).digest("hex");
|
|
3594
3985
|
}
|
|
@@ -3608,6 +3999,12 @@ function verifyMessageSignature(signingKey, payload, nonce, signature) {
|
|
|
3608
3999
|
// src/agent/agent.ts
|
|
3609
4000
|
var AGENT_VERSION = agentTunnelVersion();
|
|
3610
4001
|
var AUTH_REJECTED_CLOSE_CODES = [4001, 4003];
|
|
4002
|
+
var RELAY_FAULT_4001_REASONS = new Set(["auth timeout", "authentication error", "authentication response failed"]);
|
|
4003
|
+
function isCredentialRejection(code, reason = "") {
|
|
4004
|
+
if (!AUTH_REJECTED_CLOSE_CODES.includes(code))
|
|
4005
|
+
return false;
|
|
4006
|
+
return !(code === 4001 && RELAY_FAULT_4001_REASONS.has(reason));
|
|
4007
|
+
}
|
|
3611
4008
|
var AGENT_REPLACED_CLOSE_CODE = 4004;
|
|
3612
4009
|
var MAX_RPC_MESSAGE_SIZE = 5 * 1024 * 1024;
|
|
3613
4010
|
function log(icon, msg) {
|
|
@@ -3616,36 +4013,84 @@ function log(icon, msg) {
|
|
|
3616
4013
|
process.stdout.write(` ${safeIcon} ${c.dim}${safeMsg}${c.reset}
|
|
3617
4014
|
`);
|
|
3618
4015
|
}
|
|
4016
|
+
var LIVENESS_TIMEOUT_MS = 75000;
|
|
4017
|
+
var WATCHDOG_INTERVAL_MS = 5000;
|
|
4018
|
+
var CLOCK_JUMP_MS = 30000;
|
|
4019
|
+
var REJECTED_RETRY_MS = 5 * 60000;
|
|
4020
|
+
var STANDBY_RETRY_MS = 60000;
|
|
4021
|
+
var MAX_STANDBY_RETRY_MS = 30 * 60000;
|
|
4022
|
+
var CONNECT_DEADLINE_MS = 20000;
|
|
4023
|
+
var KEEP_AWAKE_MIN_LIFETIME_MS = 5000;
|
|
4024
|
+
var REJECTED_LOG_EVERY_MS = 3600000;
|
|
4025
|
+
var BASE_RECONNECT_DELAY_MS = 1000;
|
|
4026
|
+
var MAX_RECONNECT_DELAY_MS = 30000;
|
|
4027
|
+
function reconnectDelay(attempt, random = Math.random()) {
|
|
4028
|
+
const raw = Math.min(BASE_RECONNECT_DELAY_MS * 2 ** Math.min(attempt - 1, 30), MAX_RECONNECT_DELAY_MS);
|
|
4029
|
+
return Math.round(Math.min(raw * (0.8 + 0.4 * random), MAX_RECONNECT_DELAY_MS));
|
|
4030
|
+
}
|
|
4031
|
+
var ACCESS_ERRORS = {
|
|
4032
|
+
pending: {
|
|
4033
|
+
code: TunnelErrorCode.ACCESS_PENDING,
|
|
4034
|
+
message: "computer_access_pending: The owner has not allowed access yet. Ask them to approve the prompt on their computer, then try again."
|
|
4035
|
+
},
|
|
4036
|
+
denied: {
|
|
4037
|
+
code: TunnelErrorCode.ACCESS_DENIED,
|
|
4038
|
+
message: "computer_access_denied: The owner denied access on their computer. Try again later."
|
|
4039
|
+
},
|
|
4040
|
+
off: {
|
|
4041
|
+
code: TunnelErrorCode.ACCESS_OFF,
|
|
4042
|
+
message: "computer_access_off: The owner turned off access to this computer."
|
|
4043
|
+
}
|
|
4044
|
+
};
|
|
3619
4045
|
|
|
3620
4046
|
class TunnelAgent {
|
|
3621
4047
|
hooks;
|
|
4048
|
+
options;
|
|
4049
|
+
home;
|
|
4050
|
+
now;
|
|
3622
4051
|
ws = null;
|
|
3623
4052
|
registry;
|
|
3624
4053
|
permissionGuard;
|
|
3625
4054
|
config;
|
|
3626
4055
|
reconnectAttempts = 0;
|
|
3627
|
-
|
|
3628
|
-
|
|
4056
|
+
closeCurrentSocket = null;
|
|
4057
|
+
watchdog = null;
|
|
4058
|
+
lastTick = 0;
|
|
4059
|
+
lastPingAt = 0;
|
|
4060
|
+
connectStartedAt = 0;
|
|
4061
|
+
standbyAttempts = 0;
|
|
4062
|
+
lastRejectedLogAt = 0;
|
|
4063
|
+
sentAccessKey = null;
|
|
4064
|
+
keepAwake = null;
|
|
4065
|
+
keepAwakeFailed = false;
|
|
3629
4066
|
reconnectTimer = null;
|
|
3630
4067
|
stableConnectionTimer = null;
|
|
3631
4068
|
isShuttingDown = false;
|
|
4069
|
+
status = null;
|
|
3632
4070
|
uptime = 0;
|
|
4071
|
+
displayName;
|
|
3633
4072
|
uptimeInterval = null;
|
|
3634
4073
|
signingKey = null;
|
|
3635
4074
|
lastNonce = 0;
|
|
3636
4075
|
responseNonce = 0;
|
|
3637
|
-
constructor(config, registry, hooks = {}) {
|
|
4076
|
+
constructor(config, registry, hooks = {}, options = {}) {
|
|
3638
4077
|
this.config = config;
|
|
3639
4078
|
this.registry = registry;
|
|
3640
4079
|
this.permissionGuard = new PermissionGuard;
|
|
3641
4080
|
this.hooks = hooks;
|
|
4081
|
+
this.options = options;
|
|
4082
|
+
this.home = options.home ?? agentTunnelHome();
|
|
4083
|
+
this.now = options.now ?? Date.now;
|
|
3642
4084
|
}
|
|
3643
4085
|
connect() {
|
|
3644
4086
|
if (this.ws) {
|
|
3645
4087
|
this.ws.close();
|
|
3646
4088
|
}
|
|
4089
|
+
this.startWatchdog();
|
|
3647
4090
|
const wsUrl = this.buildWsUrl();
|
|
3648
4091
|
log(`${c.cyan}◆${c.reset}`, `Connecting…`);
|
|
4092
|
+
this.connectStartedAt = this.now();
|
|
4093
|
+
this.setStatus("connecting");
|
|
3649
4094
|
try {
|
|
3650
4095
|
this.ws = new WebSocket(new URL(wsUrl));
|
|
3651
4096
|
this.setupWsHandlers();
|
|
@@ -3664,6 +4109,11 @@ class TunnelAgent {
|
|
|
3664
4109
|
clearInterval(this.uptimeInterval);
|
|
3665
4110
|
this.uptimeInterval = null;
|
|
3666
4111
|
}
|
|
4112
|
+
if (this.watchdog) {
|
|
4113
|
+
clearInterval(this.watchdog);
|
|
4114
|
+
this.watchdog = null;
|
|
4115
|
+
}
|
|
4116
|
+
this.syncKeepAwake(false);
|
|
3667
4117
|
if (this.ws) {
|
|
3668
4118
|
try {
|
|
3669
4119
|
this.ws.close(1000, "client shutdown");
|
|
@@ -3672,6 +4122,15 @@ class TunnelAgent {
|
|
|
3672
4122
|
}
|
|
3673
4123
|
this.permissionGuard.clear();
|
|
3674
4124
|
log(`${c.gray}○${c.reset}`, `Disconnected`);
|
|
4125
|
+
this.setStatus("offline");
|
|
4126
|
+
}
|
|
4127
|
+
setStatus(status) {
|
|
4128
|
+
if (this.status === status)
|
|
4129
|
+
return;
|
|
4130
|
+
this.status = status;
|
|
4131
|
+
try {
|
|
4132
|
+
this.hooks.onStatus?.(status, this.config);
|
|
4133
|
+
} catch {}
|
|
3675
4134
|
}
|
|
3676
4135
|
isConnected() {
|
|
3677
4136
|
return this.ws?.readyState === WebSocket.OPEN;
|
|
@@ -3691,13 +4150,19 @@ class TunnelAgent {
|
|
|
3691
4150
|
type: "auth",
|
|
3692
4151
|
token: trustedCredential(this.config.token, "token"),
|
|
3693
4152
|
capabilities: this.registry.getCapabilityNames(),
|
|
3694
|
-
agentVersion: AGENT_VERSION
|
|
4153
|
+
agentVersion: AGENT_VERSION,
|
|
4154
|
+
reportsAccess: true
|
|
3695
4155
|
});
|
|
3696
4156
|
});
|
|
3697
4157
|
this.ws.addEventListener("message", (event) => {
|
|
3698
4158
|
this.handleMessage(event.data);
|
|
3699
4159
|
});
|
|
3700
|
-
|
|
4160
|
+
let closeHandled = false;
|
|
4161
|
+
const onClose = (event) => {
|
|
4162
|
+
if (closeHandled)
|
|
4163
|
+
return;
|
|
4164
|
+
closeHandled = true;
|
|
4165
|
+
this.setStatus("offline");
|
|
3701
4166
|
if (this.uptimeInterval) {
|
|
3702
4167
|
clearInterval(this.uptimeInterval);
|
|
3703
4168
|
this.uptimeInterval = null;
|
|
@@ -3707,7 +4172,23 @@ class TunnelAgent {
|
|
|
3707
4172
|
this.stableConnectionTimer = null;
|
|
3708
4173
|
}
|
|
3709
4174
|
if (!this.isShuttingDown) {
|
|
3710
|
-
if (event.code
|
|
4175
|
+
if (this.options.persistent && isCredentialRejection(event.code, event.reason)) {
|
|
4176
|
+
this.setStatus("rejected");
|
|
4177
|
+
if (this.now() - this.lastRejectedLogAt >= REJECTED_LOG_EVERY_MS) {
|
|
4178
|
+
this.lastRejectedLogAt = this.now();
|
|
4179
|
+
log(`${c.red}✗${c.reset}`, `Credential rejected — waiting for this computer to be paired again (retrying every 5 min)`);
|
|
4180
|
+
}
|
|
4181
|
+
this.retryAfter(this.options.rejectedRetryMs ?? REJECTED_RETRY_MS);
|
|
4182
|
+
return;
|
|
4183
|
+
}
|
|
4184
|
+
if (this.options.persistent && event.code === AGENT_REPLACED_CLOSE_CODE) {
|
|
4185
|
+
this.setStatus("standby");
|
|
4186
|
+
const wait = Math.min((this.options.standbyRetryMs ?? STANDBY_RETRY_MS) * 2 ** this.standbyAttempts++, MAX_STANDBY_RETRY_MS);
|
|
4187
|
+
log(`${c.yellow}○${c.reset}`, `Another Agent Tunnel process holds this computer — standing by, next try in ${Math.round(wait / 1000)}s`);
|
|
4188
|
+
this.retryAfter(wait);
|
|
4189
|
+
return;
|
|
4190
|
+
}
|
|
4191
|
+
if (event.code === 4001 && isCredentialRejection(event.code, event.reason)) {
|
|
3711
4192
|
this.isShuttingDown = true;
|
|
3712
4193
|
log(`${c.red}✗${c.reset}`, `Credential rejected — run \`agent-tunnel connect --reauth\` to pair again`);
|
|
3713
4194
|
this.hooks.onTerminalClose?.({ code: event.code, reason: "credential-rejected" });
|
|
@@ -3728,9 +4209,12 @@ class TunnelAgent {
|
|
|
3728
4209
|
log(`${c.yellow}○${c.reset}`, `Disconnected ${c.gray}(code: ${event.code})${c.reset}`);
|
|
3729
4210
|
this.scheduleReconnect();
|
|
3730
4211
|
}
|
|
3731
|
-
}
|
|
4212
|
+
};
|
|
4213
|
+
this.ws.addEventListener("close", onClose);
|
|
4214
|
+
this.closeCurrentSocket = onClose;
|
|
3732
4215
|
this.ws.addEventListener("error", () => {
|
|
3733
4216
|
log(`${c.red}✗${c.reset}`, `WebSocket error`);
|
|
4217
|
+
setTimeout(() => onClose({ code: 1006 }), 1000);
|
|
3734
4218
|
});
|
|
3735
4219
|
}
|
|
3736
4220
|
async handleMessage(raw) {
|
|
@@ -3756,6 +4240,11 @@ class TunnelAgent {
|
|
|
3756
4240
|
} else {
|
|
3757
4241
|
log(`${c.green}●${c.reset}`, `Connected ${c.reset}${c.gray}(${capabilityNames.join(", ")})${c.reset}`);
|
|
3758
4242
|
}
|
|
4243
|
+
this.lastPingAt = this.now();
|
|
4244
|
+
this.sentAccessKey = null;
|
|
4245
|
+
this.standbyAttempts = 0;
|
|
4246
|
+
this.setStatus("online");
|
|
4247
|
+
this.reportAccess();
|
|
3759
4248
|
if (this.stableConnectionTimer)
|
|
3760
4249
|
clearTimeout(this.stableConnectionTimer);
|
|
3761
4250
|
this.stableConnectionTimer = setTimeout(() => {
|
|
@@ -3775,6 +4264,7 @@ class TunnelAgent {
|
|
|
3775
4264
|
return;
|
|
3776
4265
|
}
|
|
3777
4266
|
if ("method" in msg && msg.method === "tunnel.ping") {
|
|
4267
|
+
this.lastPingAt = this.now();
|
|
3778
4268
|
this.sendPong();
|
|
3779
4269
|
return;
|
|
3780
4270
|
}
|
|
@@ -3842,6 +4332,11 @@ class TunnelAgent {
|
|
|
3842
4332
|
this.sendSignedError(id, -32001, `Capability not registered for method: ${method}`);
|
|
3843
4333
|
return;
|
|
3844
4334
|
}
|
|
4335
|
+
const refusal = await this.awaitAccess(method);
|
|
4336
|
+
if (refusal) {
|
|
4337
|
+
this.sendSignedError(id, refusal.code, refusal.message);
|
|
4338
|
+
return;
|
|
4339
|
+
}
|
|
3845
4340
|
try {
|
|
3846
4341
|
const result = await handler({
|
|
3847
4342
|
...params,
|
|
@@ -3900,22 +4395,166 @@ class TunnelAgent {
|
|
|
3900
4395
|
uptime: this.uptime,
|
|
3901
4396
|
capabilities: this.registry.getCapabilityNames(),
|
|
3902
4397
|
machineInfo: {
|
|
3903
|
-
hostname:
|
|
3904
|
-
|
|
4398
|
+
hostname: hostname2(),
|
|
4399
|
+
displayName: this.displayName ??= machineDisplayName(),
|
|
4400
|
+
platform: platform2(),
|
|
3905
4401
|
arch: arch(),
|
|
3906
4402
|
osVersion: release(),
|
|
3907
|
-
agentVersion: AGENT_VERSION
|
|
4403
|
+
agentVersion: AGENT_VERSION,
|
|
4404
|
+
homeDir: this.config.workingDir,
|
|
4405
|
+
allowedPaths: this.config.allowedPaths
|
|
3908
4406
|
}
|
|
3909
4407
|
}
|
|
3910
4408
|
});
|
|
3911
4409
|
}
|
|
4410
|
+
async awaitAccess(method) {
|
|
4411
|
+
const first = decideAccess(readAccess(this.home));
|
|
4412
|
+
if (first === "run")
|
|
4413
|
+
return null;
|
|
4414
|
+
if (first !== "ask")
|
|
4415
|
+
return ACCESS_ERRORS[first];
|
|
4416
|
+
const hold = this.options.accessHoldMs ?? ACCESS_HOLD_MS;
|
|
4417
|
+
let requestId = null;
|
|
4418
|
+
try {
|
|
4419
|
+
const pending = readAccessRequest(this.home);
|
|
4420
|
+
if (pending && Date.now() - Date.parse(pending.requestedAt) < hold) {
|
|
4421
|
+
requestId = pending.id;
|
|
4422
|
+
} else {
|
|
4423
|
+
requestId = writeAccessRequest({ capability: capabilityForMethod(method) ?? method, method }, this.home).id;
|
|
4424
|
+
wakeDesktopApp(this.home);
|
|
4425
|
+
}
|
|
4426
|
+
} catch {}
|
|
4427
|
+
const deadline = Date.now() + hold;
|
|
4428
|
+
while (Date.now() < deadline) {
|
|
4429
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(100, hold / 4)));
|
|
4430
|
+
const decision = decideAccess(readAccess(this.home));
|
|
4431
|
+
if (decision === "ask")
|
|
4432
|
+
continue;
|
|
4433
|
+
if (requestId)
|
|
4434
|
+
clearAccessRequest(requestId, this.home);
|
|
4435
|
+
return decision === "run" ? null : ACCESS_ERRORS[decision];
|
|
4436
|
+
}
|
|
4437
|
+
return ACCESS_ERRORS.pending;
|
|
4438
|
+
}
|
|
4439
|
+
reportAccess() {
|
|
4440
|
+
const access = readAccess(this.home);
|
|
4441
|
+
const { mode } = access;
|
|
4442
|
+
const grantedUntil = access.grantedUntil && Date.parse(access.grantedUntil) > this.now() ? access.grantedUntil : null;
|
|
4443
|
+
const key = `${mode}|${grantedUntil}`;
|
|
4444
|
+
if (key === this.sentAccessKey || this.status !== "online")
|
|
4445
|
+
return;
|
|
4446
|
+
this.sentAccessKey = key;
|
|
4447
|
+
this.sendSigned({ jsonrpc: "2.0", method: "tunnel.access.state", params: { mode, grantedUntil } });
|
|
4448
|
+
}
|
|
4449
|
+
syncKeepAwake(want) {
|
|
4450
|
+
if (!want) {
|
|
4451
|
+
this.keepAwake?.kill();
|
|
4452
|
+
this.keepAwake = null;
|
|
4453
|
+
this.keepAwakeFailed = false;
|
|
4454
|
+
return;
|
|
4455
|
+
}
|
|
4456
|
+
if (this.keepAwake || this.keepAwakeFailed)
|
|
4457
|
+
return;
|
|
4458
|
+
const blocker = keepAwakeCommand(platform2(), process.pid);
|
|
4459
|
+
if (!blocker)
|
|
4460
|
+
return;
|
|
4461
|
+
const child = spawn3(blocker.command, blocker.args, { stdio: "ignore" });
|
|
4462
|
+
child.on("error", () => {
|
|
4463
|
+
this.keepAwakeFailed = true;
|
|
4464
|
+
log(`${c.yellow}!${c.reset}`, `Keep awake is unavailable: ${blocker.command} could not start`);
|
|
4465
|
+
});
|
|
4466
|
+
const startedAt = this.now();
|
|
4467
|
+
child.on("exit", (code) => {
|
|
4468
|
+
if (this.keepAwake !== child)
|
|
4469
|
+
return;
|
|
4470
|
+
this.keepAwake = null;
|
|
4471
|
+
if (code !== 0 || this.now() - startedAt < KEEP_AWAKE_MIN_LIFETIME_MS) {
|
|
4472
|
+
this.keepAwakeFailed = true;
|
|
4473
|
+
log(`${c.yellow}!${c.reset}`, `Keep awake is unavailable: ${blocker.command} exited (code ${code})`);
|
|
4474
|
+
}
|
|
4475
|
+
});
|
|
4476
|
+
this.keepAwake = child;
|
|
4477
|
+
}
|
|
4478
|
+
startWatchdog() {
|
|
4479
|
+
if (this.watchdog)
|
|
4480
|
+
return;
|
|
4481
|
+
const interval = this.options.watchdogIntervalMs ?? WATCHDOG_INTERVAL_MS;
|
|
4482
|
+
this.lastTick = this.now();
|
|
4483
|
+
this.watchdog = setInterval(() => {
|
|
4484
|
+
const now = this.now();
|
|
4485
|
+
const slept = now - this.lastTick > interval + CLOCK_JUMP_MS;
|
|
4486
|
+
this.lastTick = now;
|
|
4487
|
+
if (slept) {
|
|
4488
|
+
this.reconnectAttempts = 0;
|
|
4489
|
+
this.forceReconnect("clock jump");
|
|
4490
|
+
} else if (this.status === "online" && now - this.lastPingAt > (this.options.livenessTimeoutMs ?? LIVENESS_TIMEOUT_MS)) {
|
|
4491
|
+
this.forceReconnect("liveness timeout");
|
|
4492
|
+
} else if (this.status === "connecting" && this.ws && now - this.connectStartedAt > (this.options.connectDeadlineMs ?? CONNECT_DEADLINE_MS)) {
|
|
4493
|
+
this.forceReconnect("handshake timeout");
|
|
4494
|
+
} else if (this.status === "rejected" && this.credentialChanged()) {
|
|
4495
|
+
this.forceReconnect("new credential");
|
|
4496
|
+
}
|
|
4497
|
+
try {
|
|
4498
|
+
const access = readAccess(this.home);
|
|
4499
|
+
this.syncKeepAwake(access.keepAwake);
|
|
4500
|
+
this.reportAccess();
|
|
4501
|
+
} catch {}
|
|
4502
|
+
}, interval);
|
|
4503
|
+
this.watchdog.unref?.();
|
|
4504
|
+
}
|
|
4505
|
+
credentialChanged() {
|
|
4506
|
+
try {
|
|
4507
|
+
const next = this.options.reloadConfig?.();
|
|
4508
|
+
if (!next || next.token === this.config.token && next.tunnelId === this.config.tunnelId)
|
|
4509
|
+
return false;
|
|
4510
|
+
this.config = next;
|
|
4511
|
+
return true;
|
|
4512
|
+
} catch {
|
|
4513
|
+
return false;
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
forceReconnect(reason) {
|
|
4517
|
+
if (this.isShuttingDown)
|
|
4518
|
+
return;
|
|
4519
|
+
if (!this.ws) {
|
|
4520
|
+
if (this.reconnectTimer) {
|
|
4521
|
+
clearTimeout(this.reconnectTimer);
|
|
4522
|
+
this.reconnectTimer = null;
|
|
4523
|
+
this.connect();
|
|
4524
|
+
}
|
|
4525
|
+
return;
|
|
4526
|
+
}
|
|
4527
|
+
log(`${c.yellow}○${c.reset}`, `Reconnecting (${reason})`);
|
|
4528
|
+
try {
|
|
4529
|
+
this.ws.close(4000, reason);
|
|
4530
|
+
} catch {}
|
|
4531
|
+
this.ws = null;
|
|
4532
|
+
this.closeCurrentSocket?.({ code: 4000, reason });
|
|
4533
|
+
}
|
|
4534
|
+
retryAfter(ms) {
|
|
4535
|
+
if (this.reconnectTimer)
|
|
4536
|
+
clearTimeout(this.reconnectTimer);
|
|
4537
|
+
this.ws = null;
|
|
4538
|
+
this.reconnectTimer = setTimeout(() => {
|
|
4539
|
+
this.reconnectTimer = null;
|
|
4540
|
+
try {
|
|
4541
|
+
if (this.options.reloadConfig)
|
|
4542
|
+
this.config = this.options.reloadConfig();
|
|
4543
|
+
} catch {}
|
|
4544
|
+
this.connect();
|
|
4545
|
+
}, ms);
|
|
4546
|
+
}
|
|
3912
4547
|
scheduleReconnect() {
|
|
3913
4548
|
if (this.isShuttingDown)
|
|
3914
4549
|
return;
|
|
3915
4550
|
this.reconnectAttempts++;
|
|
3916
|
-
const delay =
|
|
4551
|
+
const delay = reconnectDelay(this.reconnectAttempts);
|
|
4552
|
+
this.ws = null;
|
|
3917
4553
|
log(`${c.cyan}◆${c.reset}`, `Reconnecting in ${c.reset}${c.white}${(delay / 1000).toFixed(1)}s${c.dim} (attempt ${this.reconnectAttempts})`);
|
|
4554
|
+
if (this.reconnectTimer)
|
|
4555
|
+
clearTimeout(this.reconnectTimer);
|
|
3918
4556
|
this.reconnectTimer = setTimeout(() => {
|
|
4557
|
+
this.reconnectTimer = null;
|
|
3919
4558
|
this.connect();
|
|
3920
4559
|
}, delay);
|
|
3921
4560
|
}
|
|
@@ -3925,7 +4564,7 @@ class TunnelAgent {
|
|
|
3925
4564
|
}
|
|
3926
4565
|
|
|
3927
4566
|
// src/agent/banner.ts
|
|
3928
|
-
import { arch as arch2, hostname as
|
|
4567
|
+
import { arch as arch2, hostname as hostname3, platform as platform3 } from "os";
|
|
3929
4568
|
var BOX_WIDTH = 60;
|
|
3930
4569
|
var BAR_WIDTH = 50;
|
|
3931
4570
|
var BAR_FRAMES = 14;
|
|
@@ -3975,7 +4614,7 @@ async function printStartupBanner({
|
|
|
3975
4614
|
blank();
|
|
3976
4615
|
row(` ${c.dim}tunnel${c.reset} ${c.white}${truncate(tunnelId, 40)}${c.reset}`);
|
|
3977
4616
|
row(` ${c.dim}relay${c.reset} ${c.white}${truncate(apiUrl, 40)}${c.reset}`);
|
|
3978
|
-
row(` ${c.dim}machine${c.reset} ${c.white}${truncate(
|
|
4617
|
+
row(` ${c.dim}machine${c.reset} ${c.white}${truncate(hostname3(), 28)}${c.reset} ${c.dim}(${platform3()} ${arch2()})${c.reset}`);
|
|
3979
4618
|
row(` ${c.dim}access${c.reset} ${capabilityRow}`);
|
|
3980
4619
|
blank();
|
|
3981
4620
|
console.log(` ${c.gray}╰${"─".repeat(BOX_WIDTH - brand.length - 3)} ${c.dim}created by ${c.cyan}kortix${c.reset} ${c.gray}─╯${c.reset}`);
|
|
@@ -4008,10 +4647,10 @@ class CapabilityRegistry {
|
|
|
4008
4647
|
}
|
|
4009
4648
|
|
|
4010
4649
|
// src/agent/capabilities/desktop/cua-driver.ts
|
|
4011
|
-
import { spawn } from "child_process";
|
|
4012
|
-
import { existsSync as
|
|
4013
|
-
import { homedir as
|
|
4014
|
-
import { join as
|
|
4650
|
+
import { spawn as spawn4 } from "child_process";
|
|
4651
|
+
import { existsSync as existsSync3, realpathSync, statSync as statSync2 } from "fs";
|
|
4652
|
+
import { homedir as homedir3, platform as platform4 } from "os";
|
|
4653
|
+
import { join as join5 } from "path";
|
|
4015
4654
|
var MAX_DRIVER_OUTPUT_BYTES = 5 * 1024 * 1024;
|
|
4016
4655
|
var DRIVER_ENV_KEYS = [
|
|
4017
4656
|
"PATH",
|
|
@@ -4030,7 +4669,7 @@ var DRIVER_ENV_KEYS = [
|
|
|
4030
4669
|
function candidateBins() {
|
|
4031
4670
|
const candidates = [
|
|
4032
4671
|
process.env.CUA_DRIVER_BIN,
|
|
4033
|
-
|
|
4672
|
+
join5(homedir3(), ".local", "bin", process.platform === "win32" ? "cua-driver.exe" : "cua-driver"),
|
|
4034
4673
|
"/usr/local/bin/cua-driver",
|
|
4035
4674
|
"/opt/homebrew/bin/cua-driver"
|
|
4036
4675
|
];
|
|
@@ -4038,10 +4677,10 @@ function candidateBins() {
|
|
|
4038
4677
|
}
|
|
4039
4678
|
function findCuaDriverBinary() {
|
|
4040
4679
|
for (const candidate of candidateBins()) {
|
|
4041
|
-
if (!
|
|
4680
|
+
if (!existsSync3(candidate))
|
|
4042
4681
|
continue;
|
|
4043
4682
|
const resolved = realpathSync(candidate);
|
|
4044
|
-
const stats =
|
|
4683
|
+
const stats = statSync2(resolved);
|
|
4045
4684
|
if (!stats.isFile())
|
|
4046
4685
|
throw new Error(`cua-driver is not a regular file: ${candidate}`);
|
|
4047
4686
|
if (process.platform !== "win32") {
|
|
@@ -4070,7 +4709,7 @@ function driverEnvironment() {
|
|
|
4070
4709
|
}
|
|
4071
4710
|
function execFile(cmd, args, timeoutMs = 30000) {
|
|
4072
4711
|
return new Promise((resolve, reject) => {
|
|
4073
|
-
const proc =
|
|
4712
|
+
const proc = spawn4(cmd, args, {
|
|
4074
4713
|
stdio: ["ignore", "pipe", "pipe"],
|
|
4075
4714
|
env: driverEnvironment()
|
|
4076
4715
|
});
|
|
@@ -4153,7 +4792,7 @@ class CuaDriver {
|
|
|
4153
4792
|
binary = null;
|
|
4154
4793
|
daemonReady = false;
|
|
4155
4794
|
async ensureInstalled() {
|
|
4156
|
-
if (this.binary &&
|
|
4795
|
+
if (this.binary && existsSync3(this.binary))
|
|
4157
4796
|
return this.binary;
|
|
4158
4797
|
const found = findCuaDriverBinary();
|
|
4159
4798
|
if (found) {
|
|
@@ -4211,15 +4850,15 @@ class CuaDriver {
|
|
|
4211
4850
|
}
|
|
4212
4851
|
async startDaemon() {
|
|
4213
4852
|
const bin = await this.ensureInstalled();
|
|
4214
|
-
if (
|
|
4215
|
-
const child =
|
|
4853
|
+
if (platform4() === "darwin") {
|
|
4854
|
+
const child = spawn4("open", ["-n", "-g", "-a", "CuaDriver", "--args", "serve"], {
|
|
4216
4855
|
detached: true,
|
|
4217
4856
|
stdio: "ignore",
|
|
4218
4857
|
env: driverEnvironment()
|
|
4219
4858
|
});
|
|
4220
4859
|
child.unref();
|
|
4221
4860
|
} else {
|
|
4222
|
-
const child =
|
|
4861
|
+
const child = spawn4(bin, ["serve"], {
|
|
4223
4862
|
detached: true,
|
|
4224
4863
|
stdio: "ignore",
|
|
4225
4864
|
env: driverEnvironment()
|
|
@@ -4366,7 +5005,7 @@ function createDesktopCapability() {
|
|
|
4366
5005
|
|
|
4367
5006
|
// src/agent/capabilities/filesystem.ts
|
|
4368
5007
|
import { open, writeFile, readdir, stat, unlink, mkdir } from "fs/promises";
|
|
4369
|
-
import { createHash as
|
|
5008
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
4370
5009
|
|
|
4371
5010
|
// src/shared/filesystem-validation.ts
|
|
4372
5011
|
function validateFilesystemParams(method, params) {
|
|
@@ -4399,13 +5038,13 @@ function validateFilesystemParams(method, params) {
|
|
|
4399
5038
|
}
|
|
4400
5039
|
|
|
4401
5040
|
// src/agent/capabilities/filesystem.ts
|
|
4402
|
-
import { join as
|
|
5041
|
+
import { join as join7, dirname as dirname4 } from "path";
|
|
4403
5042
|
|
|
4404
5043
|
// src/agent/security/path-validator.ts
|
|
4405
|
-
import { dirname as
|
|
5044
|
+
import { dirname as dirname3, basename as basename2, join as join6, resolve as resolve2, normalize, relative, isAbsolute as isAbsolute2 } from "path";
|
|
4406
5045
|
import { realpathSync as realpathSync2 } from "fs";
|
|
4407
5046
|
function resolveExistingRoot(path) {
|
|
4408
|
-
const normalized = normalize(
|
|
5047
|
+
const normalized = normalize(resolve2(path));
|
|
4409
5048
|
try {
|
|
4410
5049
|
return resolvePathForValidation(normalized);
|
|
4411
5050
|
} catch {
|
|
@@ -4413,7 +5052,7 @@ function resolveExistingRoot(path) {
|
|
|
4413
5052
|
}
|
|
4414
5053
|
}
|
|
4415
5054
|
function resolvePathForValidation(path) {
|
|
4416
|
-
const normalized = normalize(
|
|
5055
|
+
const normalized = normalize(resolve2(path));
|
|
4417
5056
|
try {
|
|
4418
5057
|
return realpathSync2(normalized);
|
|
4419
5058
|
} catch (err) {
|
|
@@ -4421,10 +5060,10 @@ function resolvePathForValidation(path) {
|
|
|
4421
5060
|
if (code !== "ENOENT") {
|
|
4422
5061
|
throw new Error(`Access denied: cannot resolve path "${path}" (${code})`);
|
|
4423
5062
|
}
|
|
4424
|
-
const parent =
|
|
5063
|
+
const parent = dirname3(normalized);
|
|
4425
5064
|
if (parent === normalized)
|
|
4426
5065
|
return normalized;
|
|
4427
|
-
return
|
|
5066
|
+
return join6(resolvePathForValidation(parent), basename2(normalized));
|
|
4428
5067
|
}
|
|
4429
5068
|
}
|
|
4430
5069
|
function assertAllowedResolvedPath(originalPath, resolved, allowedPaths, blockedPaths = []) {
|
|
@@ -4440,13 +5079,13 @@ function assertAllowedResolvedPath(originalPath, resolved, allowedPaths, blocked
|
|
|
4440
5079
|
return isPathInside(resolved, normalizedAllowed);
|
|
4441
5080
|
});
|
|
4442
5081
|
if (!withinAllowed) {
|
|
4443
|
-
throw new Error(`Access denied: path "${originalPath}" is outside allowed directories`);
|
|
5082
|
+
throw new Error(`Access denied: path "${originalPath}" is outside allowed directories (${allowedPaths.join(", ")})`);
|
|
4444
5083
|
}
|
|
4445
5084
|
}
|
|
4446
5085
|
}
|
|
4447
5086
|
function isPathInside(target, root) {
|
|
4448
5087
|
const child = relative(root, target);
|
|
4449
|
-
return child === "" || !child.startsWith("..") && !
|
|
5088
|
+
return child === "" || !child.startsWith("..") && !isAbsolute2(child);
|
|
4450
5089
|
}
|
|
4451
5090
|
function validatePath(path, allowedPaths, blockedPaths = []) {
|
|
4452
5091
|
if (!path) {
|
|
@@ -4553,18 +5192,18 @@ function createFilesystemCapability(config) {
|
|
|
4553
5192
|
throw new Error(`Content exceeds max size (${contentBytes} > ${maxFileSize})`);
|
|
4554
5193
|
}
|
|
4555
5194
|
const bytes = Buffer.from(content, encoding);
|
|
4556
|
-
const expectedHash =
|
|
5195
|
+
const expectedHash = createHash3("sha256").update(bytes).digest("hex");
|
|
4557
5196
|
if (params.sha256 !== undefined && params.sha256.toLowerCase() !== expectedHash) {
|
|
4558
5197
|
throw new Error("SHA-256 mismatch: content differs from the source; destination was not modified");
|
|
4559
5198
|
}
|
|
4560
|
-
await mkdir(
|
|
5199
|
+
await mkdir(dirname4(path), { recursive: true });
|
|
4561
5200
|
validateFilesystemPath(path, config, params, true);
|
|
4562
5201
|
await writeFile(path, bytes);
|
|
4563
5202
|
validateFilesystemPath(path, config, params);
|
|
4564
5203
|
const handle = await open(path, "r");
|
|
4565
5204
|
try {
|
|
4566
5205
|
const stats = await handle.stat();
|
|
4567
|
-
const persistedHash =
|
|
5206
|
+
const persistedHash = createHash3("sha256").update(await handle.readFile()).digest("hex");
|
|
4568
5207
|
if (persistedHash !== expectedHash)
|
|
4569
5208
|
throw new Error("SHA-256 mismatch after write; destination verification failed");
|
|
4570
5209
|
return {
|
|
@@ -4584,7 +5223,7 @@ function createFilesystemCapability(config) {
|
|
|
4584
5223
|
const entries = await readdir(path, { withFileTypes: true });
|
|
4585
5224
|
const result = entries.map((entry) => ({
|
|
4586
5225
|
name: entry.name,
|
|
4587
|
-
path:
|
|
5226
|
+
path: join7(path, entry.name),
|
|
4588
5227
|
isDirectory: entry.isDirectory(),
|
|
4589
5228
|
isFile: entry.isFile(),
|
|
4590
5229
|
isSymlink: entry.isSymbolicLink()
|
|
@@ -4597,7 +5236,7 @@ function createFilesystemCapability(config) {
|
|
|
4597
5236
|
for (const sub of subEntries) {
|
|
4598
5237
|
result.push({
|
|
4599
5238
|
name: sub.name,
|
|
4600
|
-
path:
|
|
5239
|
+
path: join7(dir.path, sub.name),
|
|
4601
5240
|
isDirectory: sub.isDirectory(),
|
|
4602
5241
|
isFile: sub.isFile(),
|
|
4603
5242
|
isSymlink: sub.isSymbolicLink()
|
|
@@ -4638,7 +5277,7 @@ function createFilesystemCapability(config) {
|
|
|
4638
5277
|
}
|
|
4639
5278
|
|
|
4640
5279
|
// src/agent/capabilities/shell.ts
|
|
4641
|
-
import { spawn as
|
|
5280
|
+
import { spawn as spawn5 } from "child_process";
|
|
4642
5281
|
|
|
4643
5282
|
// src/agent/security/command-validator.ts
|
|
4644
5283
|
var SHELL_METACHAR_REGEX = /[;&|`$(){}[\]<>!#~]/;
|
|
@@ -4703,7 +5342,7 @@ function createShellCapability(config) {
|
|
|
4703
5342
|
}
|
|
4704
5343
|
}
|
|
4705
5344
|
return new Promise((resolve, reject) => {
|
|
4706
|
-
const proc =
|
|
5345
|
+
const proc = spawn5(executable, args, {
|
|
4707
5346
|
cwd,
|
|
4708
5347
|
shell: false,
|
|
4709
5348
|
timeout,
|
|
@@ -4777,33 +5416,32 @@ function createEnabledCapabilityRegistry(config, findDesktopDriver = findCuaDriv
|
|
|
4777
5416
|
}
|
|
4778
5417
|
|
|
4779
5418
|
// src/agent/credential-store.ts
|
|
4780
|
-
import { chmodSync as
|
|
4781
|
-
import {
|
|
4782
|
-
|
|
4783
|
-
var
|
|
4784
|
-
var CONFIG_FILE2 = join6(CONFIG_DIR2, "config.json");
|
|
5419
|
+
import { chmodSync as chmodSync3, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
5420
|
+
import { join as join8 } from "path";
|
|
5421
|
+
var CONFIG_DIR2 = agentTunnelHome();
|
|
5422
|
+
var CONFIG_FILE2 = join8(CONFIG_DIR2, "config.json");
|
|
4785
5423
|
var PAIRING_FIELDS = ["token", "tunnelId", "enabledCapabilities"];
|
|
4786
5424
|
function readConfigFile() {
|
|
4787
5425
|
try {
|
|
4788
|
-
const parsed = JSON.parse(
|
|
5426
|
+
const parsed = JSON.parse(readFileSync5(CONFIG_FILE2, "utf-8"));
|
|
4789
5427
|
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
4790
5428
|
} catch {
|
|
4791
5429
|
return {};
|
|
4792
5430
|
}
|
|
4793
5431
|
}
|
|
4794
5432
|
function writeConfigFileAtomic(next) {
|
|
4795
|
-
|
|
5433
|
+
mkdirSync2(CONFIG_DIR2, { recursive: true, mode: 448 });
|
|
4796
5434
|
try {
|
|
4797
|
-
|
|
5435
|
+
chmodSync3(CONFIG_DIR2, 448);
|
|
4798
5436
|
} catch {}
|
|
4799
|
-
const tmpFile =
|
|
4800
|
-
|
|
5437
|
+
const tmpFile = join8(CONFIG_DIR2, `config.${process.pid}.${Date.now()}.tmp`);
|
|
5438
|
+
writeFileSync3(tmpFile, JSON.stringify(next, null, 2), { mode: 384, flag: "wx" });
|
|
4801
5439
|
try {
|
|
4802
|
-
|
|
5440
|
+
chmodSync3(tmpFile, 384);
|
|
4803
5441
|
} catch {}
|
|
4804
|
-
|
|
5442
|
+
renameSync2(tmpFile, CONFIG_FILE2);
|
|
4805
5443
|
try {
|
|
4806
|
-
|
|
5444
|
+
chmodSync3(CONFIG_FILE2, 384);
|
|
4807
5445
|
} catch {}
|
|
4808
5446
|
}
|
|
4809
5447
|
function saveCredentials(tunnelId, token, apiUrl, enabledCapabilities) {
|
|
@@ -4816,13 +5454,13 @@ function saveCredentials(tunnelId, token, apiUrl, enabledCapabilities) {
|
|
|
4816
5454
|
});
|
|
4817
5455
|
}
|
|
4818
5456
|
function clearSavedCredentials() {
|
|
4819
|
-
if (!
|
|
5457
|
+
if (!existsSync4(CONFIG_FILE2))
|
|
4820
5458
|
return false;
|
|
4821
5459
|
let existing;
|
|
4822
5460
|
try {
|
|
4823
|
-
existing = JSON.parse(
|
|
5461
|
+
existing = JSON.parse(readFileSync5(CONFIG_FILE2, "utf-8"));
|
|
4824
5462
|
} catch {
|
|
4825
|
-
|
|
5463
|
+
rmSync2(CONFIG_FILE2, { force: true });
|
|
4826
5464
|
return true;
|
|
4827
5465
|
}
|
|
4828
5466
|
const hadCredentials = PAIRING_FIELDS.some((key) => (key in existing) && existing[key] != null);
|
|
@@ -4866,13 +5504,13 @@ function probeCredentials(config, options = {}) {
|
|
|
4866
5504
|
socket.addEventListener("message", (event) => {
|
|
4867
5505
|
try {
|
|
4868
5506
|
const message = JSON.parse(String(event.data));
|
|
4869
|
-
if (
|
|
5507
|
+
if (isJsonRecord2(message) && message.type === "auth_ok")
|
|
4870
5508
|
settle("valid");
|
|
4871
5509
|
} catch {}
|
|
4872
5510
|
});
|
|
4873
5511
|
socket.addEventListener("close", (event) => {
|
|
4874
|
-
const { code } = event;
|
|
4875
|
-
if (
|
|
5512
|
+
const { code, reason } = event;
|
|
5513
|
+
if (isCredentialRejection(code, reason))
|
|
4876
5514
|
return settle("rejected");
|
|
4877
5515
|
if (code === AGENT_REPLACED_CLOSE_CODE)
|
|
4878
5516
|
return settle("valid");
|
|
@@ -4881,172 +5519,9 @@ function probeCredentials(config, options = {}) {
|
|
|
4881
5519
|
socket.addEventListener("error", () => settle("unreachable"));
|
|
4882
5520
|
});
|
|
4883
5521
|
}
|
|
4884
|
-
function isJsonRecord(value) {
|
|
4885
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4886
|
-
}
|
|
4887
|
-
|
|
4888
|
-
// src/agent/device-auth.ts
|
|
4889
|
-
import { spawn as spawn3 } from "child_process";
|
|
4890
|
-
import { hostname as hostname3, platform as platform4 } from "os";
|
|
4891
|
-
var TUNNEL_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
4892
|
-
var SETUP_TOKEN_PATTERN = /^kortix_tnl_[A-Za-z0-9_-]{32,64}$/;
|
|
4893
|
-
var DEVICE_CODE_PATTERN = /^[A-Z]{4}-[0-9]{4}$/;
|
|
4894
|
-
var DEVICE_SECRET_PATTERN = /^[A-Za-z0-9]{32}$/;
|
|
4895
|
-
var MAX_CHALLENGE_LIFETIME_MS = 10 * 60000;
|
|
4896
|
-
|
|
4897
|
-
class InvalidDeviceAuthResponseError extends Error {
|
|
4898
|
-
constructor(message) {
|
|
4899
|
-
super(message);
|
|
4900
|
-
this.name = "InvalidDeviceAuthResponseError";
|
|
4901
|
-
}
|
|
4902
|
-
}
|
|
4903
|
-
function invalid(what) {
|
|
4904
|
-
throw new InvalidDeviceAuthResponseError(`Authorization server returned an invalid ${what}`);
|
|
4905
|
-
}
|
|
4906
5522
|
function isJsonRecord2(value) {
|
|
4907
5523
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4908
5524
|
}
|
|
4909
|
-
function normalizeBrowserUrl(value) {
|
|
4910
|
-
try {
|
|
4911
|
-
const url = new URL(value);
|
|
4912
|
-
return url.protocol === "https:" || url.protocol === "http:" ? url.toString() : null;
|
|
4913
|
-
} catch {
|
|
4914
|
-
return null;
|
|
4915
|
-
}
|
|
4916
|
-
}
|
|
4917
|
-
function isLoopback(url) {
|
|
4918
|
-
return ["localhost", "127.0.0.1", "[::1]", "::1"].includes(url.hostname);
|
|
4919
|
-
}
|
|
4920
|
-
function assertSafeVerificationUrl(value) {
|
|
4921
|
-
if (value.length > 2048)
|
|
4922
|
-
invalid("verification URL");
|
|
4923
|
-
const browserUrl = normalizeBrowserUrl(value);
|
|
4924
|
-
if (!browserUrl)
|
|
4925
|
-
invalid("verification URL");
|
|
4926
|
-
const url = new URL(browserUrl);
|
|
4927
|
-
if (url.username || url.password || url.protocol !== "https:" && !isLoopback(url)) {
|
|
4928
|
-
throw new InvalidDeviceAuthResponseError("Authorization server returned an unsafe verification URL");
|
|
4929
|
-
}
|
|
4930
|
-
return browserUrl;
|
|
4931
|
-
}
|
|
4932
|
-
function parseDeviceAuthChallenge(value) {
|
|
4933
|
-
if (!isJsonRecord2(value))
|
|
4934
|
-
invalid("challenge");
|
|
4935
|
-
const { deviceCode, deviceSecret, verificationUrl, expiresAt, pollIntervalMs } = value;
|
|
4936
|
-
if (typeof deviceCode !== "string" || !DEVICE_CODE_PATTERN.test(deviceCode))
|
|
4937
|
-
invalid("device code");
|
|
4938
|
-
if (typeof deviceSecret !== "string" || !DEVICE_SECRET_PATTERN.test(deviceSecret)) {
|
|
4939
|
-
invalid("device secret");
|
|
4940
|
-
}
|
|
4941
|
-
if (typeof verificationUrl !== "string")
|
|
4942
|
-
invalid("verification URL");
|
|
4943
|
-
if (typeof expiresAt !== "string")
|
|
4944
|
-
invalid("expiration");
|
|
4945
|
-
const expiresAtMs = Date.parse(expiresAt);
|
|
4946
|
-
const now = Date.now();
|
|
4947
|
-
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= now || expiresAtMs > now + MAX_CHALLENGE_LIFETIME_MS) {
|
|
4948
|
-
invalid("expiration");
|
|
4949
|
-
}
|
|
4950
|
-
if (!Number.isSafeInteger(pollIntervalMs) || pollIntervalMs < 250 || pollIntervalMs > 1e4) {
|
|
4951
|
-
invalid("poll interval");
|
|
4952
|
-
}
|
|
4953
|
-
return {
|
|
4954
|
-
deviceCode,
|
|
4955
|
-
deviceSecret,
|
|
4956
|
-
verificationUrl: assertSafeVerificationUrl(verificationUrl),
|
|
4957
|
-
expiresAt,
|
|
4958
|
-
pollIntervalMs
|
|
4959
|
-
};
|
|
4960
|
-
}
|
|
4961
|
-
function parseApprovedCredentials(value) {
|
|
4962
|
-
const { tunnelId, token } = value;
|
|
4963
|
-
if (typeof tunnelId !== "string" || !TUNNEL_ID_PATTERN.test(tunnelId))
|
|
4964
|
-
invalid("tunnel ID");
|
|
4965
|
-
if (typeof token !== "string" || !SETUP_TOKEN_PATTERN.test(token))
|
|
4966
|
-
invalid("setup token");
|
|
4967
|
-
return { tunnelId, token };
|
|
4968
|
-
}
|
|
4969
|
-
function parseApprovedCapabilities(value) {
|
|
4970
|
-
if (!Array.isArray(value))
|
|
4971
|
-
return [];
|
|
4972
|
-
return [...new Set(value)].filter((item) => isTunnelCapability(item));
|
|
4973
|
-
}
|
|
4974
|
-
function parseDeviceAuthStatus(value) {
|
|
4975
|
-
if (!isJsonRecord2(value) || typeof value.status !== "string")
|
|
4976
|
-
invalid("status response");
|
|
4977
|
-
switch (value.status) {
|
|
4978
|
-
case "approved": {
|
|
4979
|
-
if (!value.tunnelId || !value.token)
|
|
4980
|
-
return { status: "approved-without-token" };
|
|
4981
|
-
return {
|
|
4982
|
-
status: "approved",
|
|
4983
|
-
...parseApprovedCredentials(value),
|
|
4984
|
-
capabilities: parseApprovedCapabilities(value.capabilities)
|
|
4985
|
-
};
|
|
4986
|
-
}
|
|
4987
|
-
case "denied":
|
|
4988
|
-
return { status: "denied" };
|
|
4989
|
-
case "expired":
|
|
4990
|
-
return { status: "expired" };
|
|
4991
|
-
default:
|
|
4992
|
-
return null;
|
|
4993
|
-
}
|
|
4994
|
-
}
|
|
4995
|
-
async function requestDeviceAuthorization(apiUrl) {
|
|
4996
|
-
const response = await fetch(`${apiUrl}/device-auth`, {
|
|
4997
|
-
method: "POST",
|
|
4998
|
-
headers: { "Content-Type": "application/json" },
|
|
4999
|
-
body: JSON.stringify({ machineHostname: hostname3() })
|
|
5000
|
-
});
|
|
5001
|
-
if (!response.ok) {
|
|
5002
|
-
const body = await response.text().catch(() => "");
|
|
5003
|
-
throw new InvalidDeviceAuthResponseError(`Failed to create device auth request: ${response.status} ${body.slice(0, 200)}`);
|
|
5004
|
-
}
|
|
5005
|
-
return parseDeviceAuthChallenge(await response.json());
|
|
5006
|
-
}
|
|
5007
|
-
async function awaitDeviceAuthorization(apiUrl, challenge, options = {}) {
|
|
5008
|
-
const wait = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
5009
|
-
const expiresAtMs = Date.parse(challenge.expiresAt);
|
|
5010
|
-
for (;; ) {
|
|
5011
|
-
const secondsRemaining = Math.max(0, Math.floor((expiresAtMs - Date.now()) / 1000));
|
|
5012
|
-
if (secondsRemaining <= 0)
|
|
5013
|
-
return { status: "expired" };
|
|
5014
|
-
options.onWaiting?.(secondsRemaining);
|
|
5015
|
-
let payload;
|
|
5016
|
-
try {
|
|
5017
|
-
const response = await fetch(`${apiUrl}/device-auth/${challenge.deviceCode}/status`, {
|
|
5018
|
-
headers: { Authorization: `Bearer ${challenge.deviceSecret}` }
|
|
5019
|
-
});
|
|
5020
|
-
if (!response.ok) {
|
|
5021
|
-
await wait(challenge.pollIntervalMs);
|
|
5022
|
-
continue;
|
|
5023
|
-
}
|
|
5024
|
-
payload = await response.json();
|
|
5025
|
-
} catch {
|
|
5026
|
-
await wait(challenge.pollIntervalMs);
|
|
5027
|
-
continue;
|
|
5028
|
-
}
|
|
5029
|
-
const outcome = parseDeviceAuthStatus(payload);
|
|
5030
|
-
if (outcome)
|
|
5031
|
-
return outcome;
|
|
5032
|
-
await wait(challenge.pollIntervalMs);
|
|
5033
|
-
}
|
|
5034
|
-
}
|
|
5035
|
-
function openBrowser(url) {
|
|
5036
|
-
if (process.env.KORTIX_AGENT_TUNNEL_NO_BROWSER === "1")
|
|
5037
|
-
return;
|
|
5038
|
-
const safeUrl = normalizeBrowserUrl(url);
|
|
5039
|
-
if (!safeUrl)
|
|
5040
|
-
return;
|
|
5041
|
-
const opener = {
|
|
5042
|
-
darwin: ["open", [safeUrl]],
|
|
5043
|
-
win32: ["rundll32.exe", ["url.dll,FileProtocolHandler", safeUrl]]
|
|
5044
|
-
};
|
|
5045
|
-
const [command, args] = opener[platform4()] ?? ["xdg-open", [safeUrl]];
|
|
5046
|
-
try {
|
|
5047
|
-
spawn3(command, args, { detached: true, stdio: "ignore" }).unref();
|
|
5048
|
-
} catch {}
|
|
5049
|
-
}
|
|
5050
5525
|
|
|
5051
5526
|
// src/agent/log-format.ts
|
|
5052
5527
|
function collapseRepeatedLines(lines) {
|
|
@@ -5103,15 +5578,15 @@ async function promptYesNo(question, defaultValue) {
|
|
|
5103
5578
|
}
|
|
5104
5579
|
|
|
5105
5580
|
// src/agent/service.ts
|
|
5106
|
-
import { chmodSync as
|
|
5581
|
+
import { chmodSync as chmodSync4, copyFileSync, existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync7, realpathSync as realpathSync3, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
5107
5582
|
import { platform as platform6 } from "os";
|
|
5108
|
-
import { join as
|
|
5583
|
+
import { join as join10 } from "path";
|
|
5109
5584
|
|
|
5110
5585
|
// src/agent/service-drivers.ts
|
|
5111
5586
|
import { spawnSync } from "child_process";
|
|
5112
|
-
import { existsSync as
|
|
5113
|
-
import { homedir as
|
|
5114
|
-
import { dirname as
|
|
5587
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync6, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
5588
|
+
import { homedir as homedir4, platform as platform5, userInfo } from "os";
|
|
5589
|
+
import { dirname as dirname5, join as join9 } from "path";
|
|
5115
5590
|
|
|
5116
5591
|
// src/agent/service-quoting.ts
|
|
5117
5592
|
function shellQuote(value) {
|
|
@@ -5124,66 +5599,28 @@ function xmlEscape(value) {
|
|
|
5124
5599
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
5125
5600
|
}
|
|
5126
5601
|
|
|
5127
|
-
// src/agent/service-paths.ts
|
|
5128
|
-
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
5129
|
-
import { homedir as homedir4 } from "os";
|
|
5130
|
-
import { join as join7 } from "path";
|
|
5131
|
-
var SERVICE_LABEL = "ai.kortix.agent-tunnel";
|
|
5132
|
-
var DEFAULT_INSTALL_BACKGROUND_SERVICE = true;
|
|
5133
|
-
var TERMINAL_SERVICE_EXIT_CODE = 0;
|
|
5134
|
-
var MAX_SERVICE_LOG_BYTES = 5 * 1024 * 1024;
|
|
5135
|
-
var RETAINED_LOG_LINES = 500;
|
|
5136
|
-
function getServicePaths() {
|
|
5137
|
-
const home = homedir4();
|
|
5138
|
-
const configDir = join7(home, ".agent-tunnel");
|
|
5139
|
-
const binDir = join7(configDir, "bin");
|
|
5140
|
-
return {
|
|
5141
|
-
configDir,
|
|
5142
|
-
logDir: join7(configDir, "logs"),
|
|
5143
|
-
binDir,
|
|
5144
|
-
vendoredRunner: join7(binDir, "agent-cli.js"),
|
|
5145
|
-
launchdPlist: join7(home, "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`),
|
|
5146
|
-
systemdUnit: join7(home, ".config", "systemd", "user", `${SERVICE_LABEL}.service`),
|
|
5147
|
-
windowsScript: join7(configDir, "agent-tunnel-service.ps1")
|
|
5148
|
-
};
|
|
5149
|
-
}
|
|
5150
|
-
function serviceLogFiles(paths = getServicePaths()) {
|
|
5151
|
-
return [
|
|
5152
|
-
join7(paths.logDir, "agent-tunnel.out.log"),
|
|
5153
|
-
join7(paths.logDir, "agent-tunnel.err.log")
|
|
5154
|
-
];
|
|
5155
|
-
}
|
|
5156
|
-
function rotateServiceLogs(paths = getServicePaths(), maxBytes = MAX_SERVICE_LOG_BYTES) {
|
|
5157
|
-
const rotated = [];
|
|
5158
|
-
for (const file of serviceLogFiles(paths)) {
|
|
5159
|
-
try {
|
|
5160
|
-
const contents = readFileSync4(file, "utf8");
|
|
5161
|
-
if (Buffer.byteLength(contents, "utf8") <= maxBytes)
|
|
5162
|
-
continue;
|
|
5163
|
-
const kept = contents.split(/\r?\n/).slice(-RETAINED_LOG_LINES).join(`
|
|
5164
|
-
`);
|
|
5165
|
-
writeFileSync2(file, `[agent-tunnel] earlier entries trimmed
|
|
5166
|
-
${kept}`, { mode: 384 });
|
|
5167
|
-
rotated.push(file);
|
|
5168
|
-
} catch {}
|
|
5169
|
-
}
|
|
5170
|
-
return rotated;
|
|
5171
|
-
}
|
|
5172
|
-
|
|
5173
5602
|
// src/agent/service-drivers.ts
|
|
5174
5603
|
function posixShellCommand(runner) {
|
|
5175
5604
|
const interpreter = `"$(command -v ${shellQuote(runner.command)} 2>/dev/null || command -v node)"`;
|
|
5176
|
-
|
|
5605
|
+
const env = Object.entries(runner.env ?? {});
|
|
5606
|
+
const exports = env.length ? `export ${env.map(([key, value]) => `${key}=${shellQuote(value)}`).join(" ")}; ` : "";
|
|
5607
|
+
return `${exports}exec ${interpreter} ${runner.args.map(shellQuote).join(" ")}`;
|
|
5177
5608
|
}
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5609
|
+
var supervisor = {
|
|
5610
|
+
run(command, args) {
|
|
5611
|
+
const result = spawnSync(command, args, { encoding: "utf8" });
|
|
5612
|
+
return {
|
|
5613
|
+
ok: result.status === 0,
|
|
5614
|
+
detail: [result.stdout, result.stderr].filter(Boolean).join(`
|
|
5183
5615
|
`).trim()
|
|
5184
|
-
|
|
5185
|
-
}
|
|
5616
|
+
};
|
|
5617
|
+
}
|
|
5618
|
+
};
|
|
5619
|
+
var run = (command, args) => supervisor.run(command, args);
|
|
5186
5620
|
var notInstalled = (what) => ({ ok: false, detail: `${what} is not installed.` });
|
|
5621
|
+
var pause = { ms(ms) {
|
|
5622
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
5623
|
+
} };
|
|
5187
5624
|
function joinDetails(...results) {
|
|
5188
5625
|
return results.map((result) => result.detail).filter(Boolean).join(`
|
|
5189
5626
|
`);
|
|
@@ -5194,8 +5631,8 @@ function launchdTarget() {
|
|
|
5194
5631
|
}
|
|
5195
5632
|
function logPaths(paths) {
|
|
5196
5633
|
return {
|
|
5197
|
-
stdout:
|
|
5198
|
-
stderr:
|
|
5634
|
+
stdout: join9(paths.logDir, "agent-tunnel.out.log"),
|
|
5635
|
+
stderr: join9(paths.logDir, "agent-tunnel.err.log")
|
|
5199
5636
|
};
|
|
5200
5637
|
}
|
|
5201
5638
|
function renderLaunchdPlist(command, paths = getServicePaths()) {
|
|
@@ -5205,7 +5642,7 @@ function renderLaunchdPlist(command, paths = getServicePaths()) {
|
|
|
5205
5642
|
<plist version="1.0">
|
|
5206
5643
|
<dict>
|
|
5207
5644
|
<key>Label</key>
|
|
5208
|
-
<string>${xmlEscape(
|
|
5645
|
+
<string>${xmlEscape(paths.label)}</string>
|
|
5209
5646
|
<key>ProgramArguments</key>
|
|
5210
5647
|
<array>
|
|
5211
5648
|
<string>/bin/sh</string>
|
|
@@ -5215,10 +5652,9 @@ function renderLaunchdPlist(command, paths = getServicePaths()) {
|
|
|
5215
5652
|
<key>RunAtLoad</key>
|
|
5216
5653
|
<true/>
|
|
5217
5654
|
<key>KeepAlive</key>
|
|
5218
|
-
<
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
</dict>
|
|
5655
|
+
<true/>
|
|
5656
|
+
<key>ThrottleInterval</key>
|
|
5657
|
+
<integer>10</integer>
|
|
5222
5658
|
<key>Umask</key>
|
|
5223
5659
|
<integer>63</integer>
|
|
5224
5660
|
<key>StandardOutPath</key>
|
|
@@ -5226,7 +5662,7 @@ function renderLaunchdPlist(command, paths = getServicePaths()) {
|
|
|
5226
5662
|
<key>StandardErrorPath</key>
|
|
5227
5663
|
<string>${xmlEscape(stderr)}</string>
|
|
5228
5664
|
<key>WorkingDirectory</key>
|
|
5229
|
-
<string>${xmlEscape(
|
|
5665
|
+
<string>${xmlEscape(homedir4())}</string>
|
|
5230
5666
|
<key>EnvironmentVariables</key>
|
|
5231
5667
|
<dict>
|
|
5232
5668
|
<key>PATH</key>
|
|
@@ -5247,9 +5683,9 @@ Wants=network-online.target
|
|
|
5247
5683
|
Type=simple
|
|
5248
5684
|
UMask=0077
|
|
5249
5685
|
ExecStart=/bin/sh -lc ${shellQuote(command)}
|
|
5250
|
-
Restart=
|
|
5686
|
+
Restart=always
|
|
5251
5687
|
RestartSec=5
|
|
5252
|
-
WorkingDirectory=${
|
|
5688
|
+
WorkingDirectory=${homedir4()}
|
|
5253
5689
|
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
|
5254
5690
|
StandardOutput=append:${stdout}
|
|
5255
5691
|
StandardError=append:${stderr}
|
|
@@ -5261,119 +5697,206 @@ WantedBy=default.target
|
|
|
5261
5697
|
function renderWindowsPowerShellScript(runner) {
|
|
5262
5698
|
const command = powershellQuote(runner.command);
|
|
5263
5699
|
const args = runner.args.map(powershellQuote).join(" ");
|
|
5700
|
+
const env = Object.entries(runner.env ?? {}).map(([key, value]) => `$env:${key} = ${powershellQuote(value)}
|
|
5701
|
+
`).join("");
|
|
5264
5702
|
return `$ErrorActionPreference = 'Continue'
|
|
5265
|
-
while ($true) {
|
|
5266
|
-
|
|
5267
|
-
#
|
|
5268
|
-
|
|
5269
|
-
|
|
5703
|
+
${env}while ($true) {
|
|
5704
|
+
# The pipe makes PowerShell wait for a GUI-subsystem binary (the desktop
|
|
5705
|
+
# app's Kortix.exe) and set $LASTEXITCODE; a bare call returns at once.
|
|
5706
|
+
& ${command}${args ? ` ${args}` : ""} | Out-Null
|
|
5707
|
+
# The agent never exits on its own (R2), so every exit is restarted.
|
|
5270
5708
|
Start-Sleep -Seconds 5
|
|
5271
5709
|
}
|
|
5272
5710
|
`;
|
|
5273
5711
|
}
|
|
5274
5712
|
var launchd = {
|
|
5275
5713
|
unitPath: (paths) => paths.launchdPlist,
|
|
5714
|
+
render: (paths, runner) => renderLaunchdPlist(posixShellCommand(runner), paths),
|
|
5276
5715
|
install(paths, runner) {
|
|
5277
|
-
|
|
5278
|
-
|
|
5716
|
+
mkdirSync3(dirname5(paths.launchdPlist), { recursive: true });
|
|
5717
|
+
writeFileSync4(paths.launchdPlist, launchd.render(paths, runner), { mode: 384 });
|
|
5279
5718
|
run("launchctl", ["bootout", launchdTarget(), paths.launchdPlist]);
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5719
|
+
run("launchctl", ["enable", `${launchdTarget()}/${paths.label}`]);
|
|
5720
|
+
let boot = { ok: false, detail: "" };
|
|
5721
|
+
let kick = { ok: false, detail: "" };
|
|
5722
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
5723
|
+
if (attempt > 0)
|
|
5724
|
+
pause.ms(1000);
|
|
5725
|
+
boot = run("launchctl", ["bootstrap", launchdTarget(), paths.launchdPlist]);
|
|
5726
|
+
kick = run("launchctl", ["kickstart", "-k", `${launchdTarget()}/${paths.label}`]);
|
|
5727
|
+
if (run("launchctl", ["print", `${launchdTarget()}/${paths.label}`]).ok) {
|
|
5728
|
+
return { installed: true, active: true, detail: joinDetails(boot, kick) };
|
|
5729
|
+
}
|
|
5730
|
+
}
|
|
5731
|
+
return { installed: true, active: null, detail: joinDetails(boot, kick) };
|
|
5283
5732
|
},
|
|
5284
5733
|
uninstall(paths) {
|
|
5285
|
-
const existed =
|
|
5734
|
+
const existed = existsSync5(paths.launchdPlist);
|
|
5286
5735
|
const stop = run("launchctl", ["bootout", launchdTarget(), paths.launchdPlist]);
|
|
5287
5736
|
if (existed)
|
|
5288
|
-
|
|
5737
|
+
rmSync3(paths.launchdPlist, { force: true });
|
|
5289
5738
|
return { detail: stop.detail };
|
|
5290
5739
|
},
|
|
5291
5740
|
start(paths, installed) {
|
|
5292
5741
|
const boot = installed ? run("launchctl", ["bootstrap", launchdTarget(), paths.launchdPlist]) : notInstalled("LaunchAgent");
|
|
5293
|
-
const kick = run("launchctl", ["kickstart", "-k", `${launchdTarget()}/${
|
|
5742
|
+
const kick = run("launchctl", ["kickstart", "-k", `${launchdTarget()}/${paths.label}`]);
|
|
5294
5743
|
return { active: boot.ok || kick.ok ? true : null, detail: joinDetails(boot, kick) };
|
|
5295
5744
|
},
|
|
5296
5745
|
stop(paths, installed) {
|
|
5297
5746
|
const stop = installed ? run("launchctl", ["bootout", launchdTarget(), paths.launchdPlist]) : notInstalled("LaunchAgent");
|
|
5298
5747
|
return { detail: stop.detail };
|
|
5299
5748
|
},
|
|
5749
|
+
pause(paths) {
|
|
5750
|
+
const disable = run("launchctl", ["disable", `${launchdTarget()}/${paths.label}`]);
|
|
5751
|
+
const stop = run("launchctl", ["bootout", launchdTarget(), paths.launchdPlist]);
|
|
5752
|
+
return { enabled: disable.ok ? false : undefined, detail: joinDetails(disable, stop) };
|
|
5753
|
+
},
|
|
5754
|
+
resume(paths, installed) {
|
|
5755
|
+
const enable = run("launchctl", ["enable", `${launchdTarget()}/${paths.label}`]);
|
|
5756
|
+
const boot = installed ? run("launchctl", ["bootstrap", launchdTarget(), paths.launchdPlist]) : notInstalled("LaunchAgent");
|
|
5757
|
+
const kick = run("launchctl", ["kickstart", `${launchdTarget()}/${paths.label}`]);
|
|
5758
|
+
return { enabled: enable.ok, active: boot.ok || kick.ok ? true : null, detail: joinDetails(enable, boot, kick) };
|
|
5759
|
+
},
|
|
5300
5760
|
status(paths, installed) {
|
|
5301
|
-
const
|
|
5761
|
+
const disabled = run("launchctl", ["print-disabled", launchdTarget()]);
|
|
5762
|
+
const enabled = !new RegExp(`"${paths.label.replace(/\./g, "\\.")}" => (disabled|true)`).test(disabled.detail);
|
|
5763
|
+
const status = run("launchctl", ["print", `${launchdTarget()}/${paths.label}`]);
|
|
5302
5764
|
return {
|
|
5303
5765
|
active: status.ok,
|
|
5304
|
-
|
|
5766
|
+
enabled,
|
|
5767
|
+
detail: status.detail || (installed ? readFileSync6(paths.launchdPlist, "utf8") : undefined)
|
|
5305
5768
|
};
|
|
5306
5769
|
}
|
|
5307
5770
|
};
|
|
5308
5771
|
var systemd = {
|
|
5309
5772
|
unitPath: (paths) => paths.systemdUnit,
|
|
5773
|
+
render: (paths, runner) => renderSystemdUnit(posixShellCommand(runner), paths),
|
|
5310
5774
|
install(paths, runner) {
|
|
5311
|
-
|
|
5312
|
-
|
|
5775
|
+
mkdirSync3(dirname5(paths.systemdUnit), { recursive: true });
|
|
5776
|
+
writeFileSync4(paths.systemdUnit, systemd.render(paths, runner), { mode: 384 });
|
|
5313
5777
|
const reload = run("systemctl", ["--user", "daemon-reload"]);
|
|
5314
|
-
const enable = run("systemctl", ["--user", "enable",
|
|
5315
|
-
|
|
5778
|
+
const enable = run("systemctl", ["--user", "enable", `${paths.label}.service`]);
|
|
5779
|
+
const restart = run("systemctl", ["--user", "restart", `${paths.label}.service`]);
|
|
5780
|
+
return {
|
|
5781
|
+
installed: true,
|
|
5782
|
+
active: restart.ok ? true : null,
|
|
5783
|
+
detail: joinDetails(reload, enable, restart, ensureLinger())
|
|
5784
|
+
};
|
|
5316
5785
|
},
|
|
5317
5786
|
uninstall(paths) {
|
|
5318
|
-
const existed =
|
|
5319
|
-
const disable = run("systemctl", ["--user", "disable", "--now", `${
|
|
5787
|
+
const existed = existsSync5(paths.systemdUnit);
|
|
5788
|
+
const disable = run("systemctl", ["--user", "disable", "--now", `${paths.label}.service`]);
|
|
5320
5789
|
if (existed)
|
|
5321
|
-
|
|
5790
|
+
rmSync3(paths.systemdUnit, { force: true });
|
|
5322
5791
|
run("systemctl", ["--user", "daemon-reload"]);
|
|
5323
5792
|
return { detail: disable.detail };
|
|
5324
5793
|
},
|
|
5325
|
-
start(
|
|
5326
|
-
const start = installed ? run("systemctl", ["--user", "start", `${
|
|
5794
|
+
start(paths, installed) {
|
|
5795
|
+
const start = installed ? run("systemctl", ["--user", "start", `${paths.label}.service`]) : notInstalled("systemd unit");
|
|
5327
5796
|
return { active: start.ok ? true : null, detail: start.detail };
|
|
5328
5797
|
},
|
|
5329
|
-
stop(
|
|
5330
|
-
const stop = installed ? run("systemctl", ["--user", "stop", `${
|
|
5798
|
+
stop(paths, installed) {
|
|
5799
|
+
const stop = installed ? run("systemctl", ["--user", "stop", `${paths.label}.service`]) : notInstalled("systemd unit");
|
|
5331
5800
|
return { detail: stop.detail };
|
|
5332
5801
|
},
|
|
5333
|
-
|
|
5334
|
-
const
|
|
5335
|
-
return {
|
|
5802
|
+
pause(paths) {
|
|
5803
|
+
const disable = run("systemctl", ["--user", "disable", "--now", `${paths.label}.service`]);
|
|
5804
|
+
return { enabled: disable.ok ? false : undefined, active: false, detail: disable.detail };
|
|
5805
|
+
},
|
|
5806
|
+
resume(paths) {
|
|
5807
|
+
const enable = run("systemctl", ["--user", "enable", "--now", `${paths.label}.service`]);
|
|
5808
|
+
return { enabled: enable.ok, active: enable.ok ? true : null, detail: enable.detail };
|
|
5809
|
+
},
|
|
5810
|
+
status(paths) {
|
|
5811
|
+
const status = run("systemctl", ["--user", "is-active", `${paths.label}.service`]);
|
|
5812
|
+
const enabled = run("systemctl", ["--user", "is-enabled", `${paths.label}.service`]);
|
|
5813
|
+
return { active: status.ok, enabled: enabled.ok, detail: status.detail };
|
|
5336
5814
|
}
|
|
5337
5815
|
};
|
|
5816
|
+
function ensureLinger() {
|
|
5817
|
+
const user = userInfo().username;
|
|
5818
|
+
const linger = run("loginctl", ["show-user", user, "-p", "Linger"]);
|
|
5819
|
+
if (/Linger=yes/.test(linger.detail))
|
|
5820
|
+
return { ok: true, detail: "" };
|
|
5821
|
+
if (run("loginctl", ["enable-linger", user]).ok)
|
|
5822
|
+
return { ok: true, detail: "Enabled lingering: the service keeps running after logout and starts at boot." };
|
|
5823
|
+
return {
|
|
5824
|
+
ok: false,
|
|
5825
|
+
detail: `Warning: the service stops when you log out and starts only at your next login. Run \`loginctl enable-linger ${user}\` (may need sudo) to keep it running.`
|
|
5826
|
+
};
|
|
5827
|
+
}
|
|
5828
|
+
function renderWindowsTaskXml(paths, user) {
|
|
5829
|
+
return `<?xml version="1.0" encoding="UTF-16"?>
|
|
5830
|
+
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
5831
|
+
<RegistrationInfo><Description>Kortix Agent Tunnel</Description></RegistrationInfo>
|
|
5832
|
+
<Triggers><LogonTrigger><Enabled>true</Enabled><UserId>${xmlEscape(user)}</UserId></LogonTrigger></Triggers>
|
|
5833
|
+
<Principals><Principal id="Author"><UserId>${xmlEscape(user)}</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>
|
|
5834
|
+
<Settings>
|
|
5835
|
+
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
5836
|
+
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
5837
|
+
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
5838
|
+
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
|
5839
|
+
<StartWhenAvailable>true</StartWhenAvailable>
|
|
5840
|
+
<Enabled>true</Enabled>
|
|
5841
|
+
</Settings>
|
|
5842
|
+
<Actions Context="Author"><Exec><Command>powershell.exe</Command><Arguments>${xmlEscape(`-NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File "${paths.windowsScript}"`)}</Arguments></Exec></Actions>
|
|
5843
|
+
</Task>
|
|
5844
|
+
`;
|
|
5845
|
+
}
|
|
5846
|
+
var windowsUser = () => process.env.USERDOMAIN ? `${process.env.USERDOMAIN}\\${userInfo().username}` : userInfo().username;
|
|
5847
|
+
var windowsTaskXmlPath = (paths) => paths.windowsScript.replace(/\.ps1$/, ".xml");
|
|
5338
5848
|
var scheduledTask = {
|
|
5339
5849
|
unitPath: (paths) => paths.windowsScript,
|
|
5850
|
+
render: (_paths, runner) => renderWindowsPowerShellScript(runner),
|
|
5340
5851
|
install(paths, runner) {
|
|
5341
|
-
|
|
5342
|
-
const
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
`powershell.exe -NoProfile -ExecutionPolicy Bypass -File "${paths.windowsScript}"`,
|
|
5348
|
-
"/SC",
|
|
5349
|
-
"ONLOGON",
|
|
5350
|
-
"/F",
|
|
5351
|
-
"/RL",
|
|
5352
|
-
"LIMITED"
|
|
5353
|
-
]);
|
|
5354
|
-
const start = run("schtasks.exe", ["/Run", "/TN", SERVICE_LABEL]);
|
|
5852
|
+
writeFileSync4(paths.windowsScript, renderWindowsPowerShellScript(runner), { mode: 384 });
|
|
5853
|
+
const xml = windowsTaskXmlPath(paths);
|
|
5854
|
+
writeFileSync4(xml, Buffer.from(`\uFEFF${renderWindowsTaskXml(paths, windowsUser())}`, "utf16le"), { mode: 384 });
|
|
5855
|
+
const create = run("schtasks.exe", ["/Create", "/TN", paths.label, "/XML", xml, "/F"]);
|
|
5856
|
+
run("schtasks.exe", ["/End", "/TN", paths.label]);
|
|
5857
|
+
const start = run("schtasks.exe", ["/Run", "/TN", paths.label]);
|
|
5355
5858
|
return { installed: create.ok, active: start.ok ? true : null, detail: joinDetails(create, start) };
|
|
5356
5859
|
},
|
|
5357
5860
|
uninstall(paths) {
|
|
5358
|
-
const existed =
|
|
5359
|
-
const stop = run("schtasks.exe", ["/End", "/TN",
|
|
5360
|
-
const del = run("schtasks.exe", ["/Delete", "/TN",
|
|
5861
|
+
const existed = existsSync5(paths.windowsScript);
|
|
5862
|
+
const stop = run("schtasks.exe", ["/End", "/TN", paths.label]);
|
|
5863
|
+
const del = run("schtasks.exe", ["/Delete", "/TN", paths.label, "/F"]);
|
|
5361
5864
|
if (existed)
|
|
5362
|
-
|
|
5865
|
+
rmSync3(paths.windowsScript, { force: true });
|
|
5866
|
+
rmSync3(windowsTaskXmlPath(paths), { force: true });
|
|
5363
5867
|
return { detail: joinDetails(stop, del) };
|
|
5364
5868
|
},
|
|
5365
|
-
start(
|
|
5366
|
-
const start = installed ? run("schtasks.exe", ["/Run", "/TN",
|
|
5869
|
+
start(paths, installed) {
|
|
5870
|
+
const start = installed ? run("schtasks.exe", ["/Run", "/TN", paths.label]) : notInstalled("Scheduled Task");
|
|
5367
5871
|
return { active: start.ok ? true : null, detail: start.detail };
|
|
5368
5872
|
},
|
|
5369
|
-
stop(
|
|
5370
|
-
const stop = installed ? run("schtasks.exe", ["/End", "/TN",
|
|
5873
|
+
stop(paths, installed) {
|
|
5874
|
+
const stop = installed ? run("schtasks.exe", ["/End", "/TN", paths.label]) : notInstalled("Scheduled Task");
|
|
5371
5875
|
return { detail: stop.detail };
|
|
5372
5876
|
},
|
|
5877
|
+
pause(paths) {
|
|
5878
|
+
const disable = run("schtasks.exe", ["/Change", "/TN", paths.label, "/DISABLE"]);
|
|
5879
|
+
const stop = run("schtasks.exe", ["/End", "/TN", paths.label]);
|
|
5880
|
+
return { enabled: disable.ok ? false : undefined, active: false, detail: joinDetails(disable, stop) };
|
|
5881
|
+
},
|
|
5882
|
+
resume(paths) {
|
|
5883
|
+
const enable = run("schtasks.exe", ["/Change", "/TN", paths.label, "/ENABLE"]);
|
|
5884
|
+
const start = run("schtasks.exe", ["/Run", "/TN", paths.label]);
|
|
5885
|
+
return { enabled: enable.ok, active: start.ok ? true : null, detail: joinDetails(enable, start) };
|
|
5886
|
+
},
|
|
5373
5887
|
status(paths, installed) {
|
|
5374
|
-
const status = run("
|
|
5375
|
-
|
|
5376
|
-
|
|
5888
|
+
const status = run("powershell.exe", [
|
|
5889
|
+
"-NoProfile",
|
|
5890
|
+
"-NonInteractive",
|
|
5891
|
+
"-Command",
|
|
5892
|
+
`(Get-ScheduledTask -TaskName ${powershellQuote(paths.label)} -ErrorAction Stop).State`
|
|
5893
|
+
]);
|
|
5894
|
+
const state = status.ok ? status.detail.trim() : "";
|
|
5895
|
+
return {
|
|
5896
|
+
active: state === "Running",
|
|
5897
|
+
enabled: state !== "Disabled",
|
|
5898
|
+
detail: state || (installed ? readFileSync6(paths.windowsScript, "utf8") : undefined)
|
|
5899
|
+
};
|
|
5377
5900
|
}
|
|
5378
5901
|
};
|
|
5379
5902
|
var DRIVERS = {
|
|
@@ -5382,32 +5905,51 @@ var DRIVERS = {
|
|
|
5382
5905
|
win32: scheduledTask
|
|
5383
5906
|
};
|
|
5384
5907
|
var SUPPORTED_PLATFORMS_MESSAGE = "Background services are supported on macOS launchd, Linux systemd user services, and Windows Scheduled Tasks.";
|
|
5908
|
+
function serviceDriverFor(os) {
|
|
5909
|
+
return DRIVERS[os];
|
|
5910
|
+
}
|
|
5385
5911
|
function serviceDriver() {
|
|
5386
|
-
return
|
|
5912
|
+
return serviceDriverFor(platform5());
|
|
5387
5913
|
}
|
|
5388
5914
|
|
|
5389
5915
|
// src/agent/service.ts
|
|
5390
5916
|
function isEphemeralRunnerPath(path) {
|
|
5391
5917
|
const normalized = path.replace(/\\/g, "/");
|
|
5392
|
-
return normalized.includes("/_npx/") || normalized.includes("/_cacache/") || normalized.includes("/.pnpm-store/") || normalized.includes("/.yarn/$$virtual/");
|
|
5918
|
+
return normalized.includes("/_npx/") || normalized.includes("/_cacache/") || normalized.includes("/.pnpm-store/") || normalized.includes("/.yarn/$$virtual/") || normalized.includes("/.mount_");
|
|
5393
5919
|
}
|
|
5394
5920
|
function vendorRunner(scriptPath, paths = getServicePaths()) {
|
|
5395
5921
|
if (!isEphemeralRunnerPath(scriptPath))
|
|
5396
5922
|
return scriptPath;
|
|
5397
|
-
|
|
5923
|
+
mkdirSync4(paths.binDir, { recursive: true, mode: 448 });
|
|
5398
5924
|
const source = realpathSync3(scriptPath);
|
|
5399
5925
|
copyFileSync(source, paths.vendoredRunner);
|
|
5400
5926
|
try {
|
|
5401
|
-
|
|
5927
|
+
chmodSync4(paths.vendoredRunner, 448);
|
|
5402
5928
|
} catch {}
|
|
5403
|
-
|
|
5929
|
+
writeFileSync5(join10(paths.binDir, "package.json"), JSON.stringify({ name: "@kortix/agent-tunnel", version: agentTunnelVersion(), private: true }), { mode: 384 });
|
|
5930
|
+
writeFileSync5(join10(paths.binDir, "agent-cli.source.json"), JSON.stringify({ source, vendoredFrom: scriptPath }, null, 2), { mode: 384 });
|
|
5404
5931
|
return paths.vendoredRunner;
|
|
5405
5932
|
}
|
|
5406
|
-
function
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5933
|
+
function runnerPartsFor(script, runtime = {
|
|
5934
|
+
execPath: process.execPath,
|
|
5935
|
+
electron: process.versions.electron,
|
|
5936
|
+
appImage: process.env.APPIMAGE
|
|
5937
|
+
}, paths = getServicePaths(), vendor = vendorRunner) {
|
|
5938
|
+
const env = serviceHomeEnv(paths.configDir);
|
|
5939
|
+
if (runtime.electron) {
|
|
5940
|
+
return {
|
|
5941
|
+
command: runtime.appImage || runtime.execPath,
|
|
5942
|
+
args: [runtime.appImage ? vendor(script, paths) : script, "run", "--service"],
|
|
5943
|
+
env: { ...env, ELECTRON_RUN_AS_NODE: "1" }
|
|
5944
|
+
};
|
|
5410
5945
|
}
|
|
5946
|
+
return { command: runtime.execPath, args: [vendor(script, paths), "run", "--service"], env };
|
|
5947
|
+
}
|
|
5948
|
+
var plannedRunner = (script, paths) => isEphemeralRunnerPath(script) ? paths.vendoredRunner : script;
|
|
5949
|
+
function currentRunnerParts(vendor = vendorRunner) {
|
|
5950
|
+
const script = process.argv[1];
|
|
5951
|
+
if (script && existsSync6(script))
|
|
5952
|
+
return runnerPartsFor(script, undefined, getServicePaths(), vendor);
|
|
5411
5953
|
throw new Error("Cannot install the background service because the current Agent Tunnel executable was not found");
|
|
5412
5954
|
}
|
|
5413
5955
|
function withDriver(operate, fallback) {
|
|
@@ -5416,26 +5958,28 @@ function withDriver(operate, fallback) {
|
|
|
5416
5958
|
if (!driver)
|
|
5417
5959
|
throw new Error(SUPPORTED_PLATFORMS_MESSAGE);
|
|
5418
5960
|
const path = driver.unitPath(paths);
|
|
5419
|
-
const installed =
|
|
5961
|
+
const installed = existsSync6(path);
|
|
5420
5962
|
const outcome = operate(driver, paths, installed);
|
|
5421
5963
|
return {
|
|
5422
5964
|
platform: platform6(),
|
|
5423
5965
|
installed: outcome.installed ?? installed,
|
|
5424
5966
|
active: outcome.active ?? fallback.active,
|
|
5967
|
+
...outcome.enabled !== undefined ? { enabled: outcome.enabled } : {},
|
|
5968
|
+
...outcome.upToDate !== undefined ? { upToDate: outcome.upToDate } : {},
|
|
5425
5969
|
path,
|
|
5426
5970
|
detail: outcome.detail
|
|
5427
5971
|
};
|
|
5428
5972
|
}
|
|
5429
5973
|
function installService() {
|
|
5430
5974
|
const paths = getServicePaths();
|
|
5431
|
-
|
|
5432
|
-
|
|
5975
|
+
mkdirSync4(paths.configDir, { recursive: true, mode: 448 });
|
|
5976
|
+
mkdirSync4(paths.logDir, { recursive: true, mode: 448 });
|
|
5433
5977
|
rotateServiceLogs(paths);
|
|
5434
5978
|
const runner = currentRunnerParts();
|
|
5435
5979
|
return withDriver((driver, servicePaths) => driver.install(servicePaths, runner), { installed: false, active: null });
|
|
5436
5980
|
}
|
|
5437
5981
|
function uninstallService() {
|
|
5438
|
-
|
|
5982
|
+
rmSync4(getServicePaths().binDir, { recursive: true, force: true });
|
|
5439
5983
|
return withDriver((driver, paths) => ({ ...driver.uninstall(paths), installed: false, active: false }), { installed: false, active: false });
|
|
5440
5984
|
}
|
|
5441
5985
|
function startService() {
|
|
@@ -5444,6 +5988,18 @@ function startService() {
|
|
|
5444
5988
|
function stopService() {
|
|
5445
5989
|
return withDriver((driver, paths, installed) => ({ ...driver.stop(paths, installed), active: false }), { installed: false, active: false });
|
|
5446
5990
|
}
|
|
5991
|
+
function pauseService() {
|
|
5992
|
+
return withDriver((driver, paths, installed) => ({ ...driver.pause(paths, installed), active: false }), { installed: false, active: false });
|
|
5993
|
+
}
|
|
5994
|
+
function resumeService() {
|
|
5995
|
+
const current = getServiceStatus();
|
|
5996
|
+
if (current.installed && current.upToDate === false)
|
|
5997
|
+
return installService();
|
|
5998
|
+
return resumeUnit();
|
|
5999
|
+
}
|
|
6000
|
+
function resumeUnit() {
|
|
6001
|
+
return withDriver((driver, paths, installed) => driver.resume(paths, installed), { installed: false, active: null });
|
|
6002
|
+
}
|
|
5447
6003
|
function restartService() {
|
|
5448
6004
|
stopService();
|
|
5449
6005
|
return startService();
|
|
@@ -5457,7 +6013,13 @@ function getServiceStatus() {
|
|
|
5457
6013
|
detail: SUPPORTED_PLATFORMS_MESSAGE
|
|
5458
6014
|
};
|
|
5459
6015
|
}
|
|
5460
|
-
return withDriver((driver, paths, installed) =>
|
|
6016
|
+
return withDriver((driver, paths, installed) => {
|
|
6017
|
+
let upToDate = false;
|
|
6018
|
+
try {
|
|
6019
|
+
upToDate = installed && readFileSync7(driver.unitPath(paths), "utf8") === driver.render(paths, currentRunnerParts(plannedRunner));
|
|
6020
|
+
} catch {}
|
|
6021
|
+
return { ...driver.status(paths, installed), upToDate };
|
|
6022
|
+
}, { installed: false, active: null });
|
|
5461
6023
|
}
|
|
5462
6024
|
|
|
5463
6025
|
// src/agent/service-control.ts
|
|
@@ -5477,8 +6039,8 @@ function acquireTunnelLease() {
|
|
|
5477
6039
|
};
|
|
5478
6040
|
}
|
|
5479
6041
|
var SERVICE_ACTIONS = {
|
|
5480
|
-
start: { run:
|
|
5481
|
-
stop: { run:
|
|
6042
|
+
start: { run: resumeService, label: "started" },
|
|
6043
|
+
stop: { run: pauseService, label: "paused until `agent-tunnel start`" },
|
|
5482
6044
|
restart: { run: restartService, label: "restarted" },
|
|
5483
6045
|
uninstall: { run: uninstallService, label: "removed" },
|
|
5484
6046
|
install: { run: installService, label: "installed" }
|
|
@@ -5486,6 +6048,8 @@ var SERVICE_ACTIONS = {
|
|
|
5486
6048
|
function describeService(status) {
|
|
5487
6049
|
if (!status.installed)
|
|
5488
6050
|
return `${glyph.off} not installed`;
|
|
6051
|
+
if (status.enabled === false)
|
|
6052
|
+
return `${c.yellow}○${c.reset} paused ${c.dim}· stays stopped until \`agent-tunnel start\`${c.reset}`;
|
|
5489
6053
|
if (status.active)
|
|
5490
6054
|
return `${glyph.on} running ${c.dim}· starts at login${c.reset}`;
|
|
5491
6055
|
return `${c.yellow}○${c.reset} installed ${c.dim}· stopped${c.reset}`;
|
|
@@ -5500,6 +6064,41 @@ function renderServiceAction(action, status) {
|
|
|
5500
6064
|
blankLine();
|
|
5501
6065
|
}
|
|
5502
6066
|
|
|
6067
|
+
// src/agent/state-file.ts
|
|
6068
|
+
import { chmodSync as chmodSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync8, renameSync as renameSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
6069
|
+
import { join as join11 } from "path";
|
|
6070
|
+
function stateFilePath(home = agentTunnelHome()) {
|
|
6071
|
+
return join11(home, "state.json");
|
|
6072
|
+
}
|
|
6073
|
+
function writeAgentState(status, tunnel, home = agentTunnelHome()) {
|
|
6074
|
+
const state = {
|
|
6075
|
+
tunnelId: tunnel.tunnelId,
|
|
6076
|
+
apiUrl: tunnel.apiUrl,
|
|
6077
|
+
status,
|
|
6078
|
+
since: new Date().toISOString(),
|
|
6079
|
+
agentVersion: agentTunnelVersion(),
|
|
6080
|
+
pid: process.pid
|
|
6081
|
+
};
|
|
6082
|
+
try {
|
|
6083
|
+
mkdirSync5(home, { recursive: true, mode: 448 });
|
|
6084
|
+
const tmp = join11(home, `state.${process.pid}.${Date.now()}.tmp`);
|
|
6085
|
+
writeFileSync6(tmp, `${JSON.stringify(state, null, 2)}
|
|
6086
|
+
`, { mode: 384 });
|
|
6087
|
+
try {
|
|
6088
|
+
chmodSync5(tmp, 384);
|
|
6089
|
+
} catch {}
|
|
6090
|
+
renameSync3(tmp, stateFilePath(home));
|
|
6091
|
+
} catch {}
|
|
6092
|
+
}
|
|
6093
|
+
function readAgentState(home = agentTunnelHome()) {
|
|
6094
|
+
try {
|
|
6095
|
+
const parsed = JSON.parse(readFileSync8(stateFilePath(home), "utf8"));
|
|
6096
|
+
return parsed && typeof parsed === "object" && typeof parsed.status === "string" ? parsed : null;
|
|
6097
|
+
} catch {
|
|
6098
|
+
return null;
|
|
6099
|
+
}
|
|
6100
|
+
}
|
|
6101
|
+
|
|
5503
6102
|
// src/agent/cli.ts
|
|
5504
6103
|
var ALL_CAPABILITIES = ["filesystem", "shell", "desktop"];
|
|
5505
6104
|
var BACKGROUND_FLAGS = ["daemon", "service", "background", "always-online"];
|
|
@@ -5516,8 +6115,17 @@ function parseArgs(argv) {
|
|
|
5516
6115
|
}
|
|
5517
6116
|
return { command: argv[2] || "help", flags };
|
|
5518
6117
|
}
|
|
6118
|
+
var jsonMode = false;
|
|
6119
|
+
var writeStdout = process.stdout.write.bind(process.stdout);
|
|
6120
|
+
function emit(event, fields = {}) {
|
|
6121
|
+
writeStdout(`${JSON.stringify({ event, ...fields })}
|
|
6122
|
+
`);
|
|
6123
|
+
}
|
|
5519
6124
|
function fail(message) {
|
|
5520
|
-
|
|
6125
|
+
if (jsonMode)
|
|
6126
|
+
emit("error", { message });
|
|
6127
|
+
else
|
|
6128
|
+
console.error(` ${glyph.bad} ${message}`);
|
|
5521
6129
|
process.exit(1);
|
|
5522
6130
|
}
|
|
5523
6131
|
function shortenHomePath(path) {
|
|
@@ -5525,12 +6133,17 @@ function shortenHomePath(path) {
|
|
|
5525
6133
|
return home && path.startsWith(home) ? `~${path.slice(home.length)}` : path;
|
|
5526
6134
|
}
|
|
5527
6135
|
function startAgent(config, options = {}) {
|
|
6136
|
+
if (jsonMode) {
|
|
6137
|
+
process.stdout.write = process.stderr.write.bind(process.stderr);
|
|
6138
|
+
options = { ...options, service: true };
|
|
6139
|
+
}
|
|
5528
6140
|
const registry = createEnabledCapabilityRegistry(config);
|
|
5529
6141
|
if (config.enabledCapabilities?.includes("desktop") && !registry.has("desktop")) {
|
|
5530
6142
|
console.error("[agent-tunnel] Computer Use is approved but unavailable: install the trusted cua-driver locally, then restart Agent Tunnel.");
|
|
5531
6143
|
}
|
|
5532
6144
|
if (options.service) {
|
|
5533
|
-
|
|
6145
|
+
process.stdout.write(`[agent-tunnel] service starting: ${config.tunnelId} -> ${config.apiUrl}
|
|
6146
|
+
`);
|
|
5534
6147
|
} else {
|
|
5535
6148
|
clearScreen();
|
|
5536
6149
|
printStartupBanner({
|
|
@@ -5540,13 +6153,9 @@ function startAgent(config, options = {}) {
|
|
|
5540
6153
|
version: agentTunnelVersion()
|
|
5541
6154
|
});
|
|
5542
6155
|
}
|
|
5543
|
-
const agent = new TunnelAgent(config, registry, {
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
return;
|
|
5547
|
-
console.log(`[agent-tunnel] stopping service: ${reason}`);
|
|
5548
|
-
process.exit(TERMINAL_SERVICE_EXIT_CODE);
|
|
5549
|
-
}
|
|
6156
|
+
const agent = new TunnelAgent(config, registry, { onStatus: (status, current) => writeAgentState(status, current) }, {
|
|
6157
|
+
persistent: options.service === true,
|
|
6158
|
+
reloadConfig: () => loadConfig({ apiUrl: config.apiUrl })
|
|
5550
6159
|
});
|
|
5551
6160
|
agent.connect();
|
|
5552
6161
|
const shutdown = () => {
|
|
@@ -5564,7 +6173,7 @@ async function chooseBackgroundMode(flags) {
|
|
|
5564
6173
|
return true;
|
|
5565
6174
|
if (anyFlag(flags, FOREGROUND_FLAGS))
|
|
5566
6175
|
return false;
|
|
5567
|
-
if (!isInteractiveTerminal())
|
|
6176
|
+
if (jsonMode || !isInteractiveTerminal())
|
|
5568
6177
|
return false;
|
|
5569
6178
|
blankLine();
|
|
5570
6179
|
console.log(` ${glyph.warn} ${c.bold}Security note${c.reset}`);
|
|
@@ -5576,45 +6185,72 @@ async function chooseBackgroundMode(flags) {
|
|
|
5576
6185
|
async function launch(config, flags, lease) {
|
|
5577
6186
|
if (await chooseBackgroundMode(flags)) {
|
|
5578
6187
|
saveCredentials(config.tunnelId, config.token, config.apiUrl);
|
|
5579
|
-
|
|
6188
|
+
if (!jsonMode) {
|
|
6189
|
+
renderServiceAction("install", SERVICE_ACTIONS.install.run());
|
|
6190
|
+
return;
|
|
6191
|
+
}
|
|
6192
|
+
try {
|
|
6193
|
+
const status = SERVICE_ACTIONS.install.run();
|
|
6194
|
+
const ok = status.installed && status.active === true;
|
|
6195
|
+
emit("service", { action: "install", ok, active: status.active, detail: status.detail ?? "" });
|
|
6196
|
+
if (!ok)
|
|
6197
|
+
process.exitCode = 1;
|
|
6198
|
+
} catch (error) {
|
|
6199
|
+
emit("service", { action: "install", ok: false, detail: error instanceof Error ? error.message : String(error) });
|
|
6200
|
+
process.exitCode = 1;
|
|
6201
|
+
}
|
|
5580
6202
|
return;
|
|
5581
6203
|
}
|
|
5582
|
-
if (lease?.serviceWasActive) {
|
|
6204
|
+
if (lease?.serviceWasActive && !jsonMode) {
|
|
5583
6205
|
console.log(` ${c.dim}Background service stays paused while this terminal holds the tunnel.${c.reset}`);
|
|
5584
6206
|
console.log(` ${c.dim}Resume it with${c.reset} ${c.white}agent-tunnel start${c.reset}${c.dim}, or leave it — it starts again at login.${c.reset}`);
|
|
5585
6207
|
}
|
|
5586
6208
|
startAgent(config);
|
|
5587
6209
|
}
|
|
5588
|
-
async function pairThisMachine(apiUrl, flags) {
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
6210
|
+
async function pairThisMachine(apiUrl, flags, lease) {
|
|
6211
|
+
if (!jsonMode) {
|
|
6212
|
+
blankLine();
|
|
6213
|
+
console.log(` ${glyph.mark} ${c.bold}Device Authorization${c.reset}`);
|
|
6214
|
+
blankLine();
|
|
6215
|
+
}
|
|
5592
6216
|
let challenge;
|
|
5593
6217
|
try {
|
|
5594
|
-
challenge = await requestDeviceAuthorization(apiUrl);
|
|
6218
|
+
challenge = await requestDeviceAuthorization(apiUrl, { projectId: flags["project-id"] });
|
|
5595
6219
|
} catch (error) {
|
|
5596
6220
|
fail(error instanceof InvalidDeviceAuthResponseError ? error.message : "Failed to start device authorization");
|
|
5597
6221
|
}
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
6222
|
+
if (jsonMode) {
|
|
6223
|
+
emit("challenge", {
|
|
6224
|
+
deviceCode: challenge.deviceCode,
|
|
6225
|
+
verificationUrl: challenge.verificationUrl,
|
|
6226
|
+
expiresAt: challenge.expiresAt
|
|
6227
|
+
});
|
|
6228
|
+
} else {
|
|
6229
|
+
console.log(` ${c.dim}Code:${c.reset} ${c.bold}${c.white}${challenge.deviceCode}${c.reset}`);
|
|
6230
|
+
blankLine();
|
|
6231
|
+
console.log(` ${c.dim}Open this URL on any device to approve:${c.reset}`);
|
|
6232
|
+
console.log(` ${c.cyan}${challenge.verificationUrl}${c.reset}`);
|
|
6233
|
+
blankLine();
|
|
6234
|
+
openBrowser(challenge.verificationUrl);
|
|
6235
|
+
}
|
|
6236
|
+
const clearWaiting = () => {
|
|
6237
|
+
if (!jsonMode)
|
|
6238
|
+
process.stdout.write(`\r${" ".repeat(60)}\r`);
|
|
6239
|
+
};
|
|
5604
6240
|
let outcome;
|
|
5605
6241
|
try {
|
|
5606
6242
|
outcome = await awaitDeviceAuthorization(apiUrl, challenge, {
|
|
5607
|
-
onWaiting: (secondsRemaining) => {
|
|
6243
|
+
onWaiting: jsonMode ? undefined : (secondsRemaining) => {
|
|
5608
6244
|
const minutes = Math.floor(secondsRemaining / 60);
|
|
5609
6245
|
const seconds = String(secondsRemaining % 60).padStart(2, "0");
|
|
5610
6246
|
process.stdout.write(`\r ${c.dim}Waiting for approval... ${c.white}${minutes}:${seconds}${c.reset} `);
|
|
5611
6247
|
}
|
|
5612
6248
|
});
|
|
5613
6249
|
} catch (error) {
|
|
5614
|
-
|
|
6250
|
+
clearWaiting();
|
|
5615
6251
|
fail(error instanceof Error ? error.message : "Device authorization failed");
|
|
5616
6252
|
}
|
|
5617
|
-
|
|
6253
|
+
clearWaiting();
|
|
5618
6254
|
if (outcome.status === "denied")
|
|
5619
6255
|
fail("Authorization denied.");
|
|
5620
6256
|
if (outcome.status === "expired")
|
|
@@ -5623,6 +6259,8 @@ async function pairThisMachine(apiUrl, flags) {
|
|
|
5623
6259
|
fail("Authorization was approved, but the setup token was not available. Run connect again.");
|
|
5624
6260
|
}
|
|
5625
6261
|
if (outcome.capabilities.length === 0) {
|
|
6262
|
+
if (jsonMode)
|
|
6263
|
+
fail("No capabilities were approved. Nothing was saved. Pair again and approve at least one capability.");
|
|
5626
6264
|
console.log(` ${glyph.bad} ${c.bold}No capabilities were approved${c.reset}`);
|
|
5627
6265
|
blankLine();
|
|
5628
6266
|
console.log(` ${c.dim}A tunnel with no capabilities connects but cannot act, and the${c.reset}`);
|
|
@@ -5632,12 +6270,22 @@ async function pairThisMachine(apiUrl, flags) {
|
|
|
5632
6270
|
blankLine();
|
|
5633
6271
|
process.exit(1);
|
|
5634
6272
|
}
|
|
5635
|
-
console.log(` ${glyph.on} ${c.bold}Authorized${c.reset}`);
|
|
5636
6273
|
saveCredentials(outcome.tunnelId, outcome.token, apiUrl, outcome.capabilities);
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
6274
|
+
writeAccess({
|
|
6275
|
+
mode: existsSync7(desktopAppPath()) ? "ask" : "always",
|
|
6276
|
+
grantedUntil: null,
|
|
6277
|
+
deniedUntil: null,
|
|
6278
|
+
keepAwake: readAccess().keepAwake
|
|
6279
|
+
});
|
|
6280
|
+
if (jsonMode) {
|
|
6281
|
+
emit("approved", { tunnelId: outcome.tunnelId, capabilities: outcome.capabilities });
|
|
6282
|
+
} else {
|
|
6283
|
+
console.log(` ${glyph.on} ${c.bold}Authorized${c.reset}`);
|
|
6284
|
+
console.log(` ${c.dim}Saved to ${CONFIG_FILE2}${c.reset}`);
|
|
6285
|
+
console.log(` ${c.dim}Access: ${outcome.capabilities.join(", ")}${c.reset}`);
|
|
6286
|
+
blankLine();
|
|
6287
|
+
}
|
|
6288
|
+
await launch(loadConfig({ apiUrl }), flags, lease);
|
|
5641
6289
|
}
|
|
5642
6290
|
async function commandConnect(flags) {
|
|
5643
6291
|
const config = loadConfig({
|
|
@@ -5649,18 +6297,24 @@ async function commandConnect(flags) {
|
|
|
5649
6297
|
if (Boolean(config.token) !== Boolean(config.tunnelId)) {
|
|
5650
6298
|
fail("Provide both --token and --tunnel-id, or neither (for device auth)");
|
|
5651
6299
|
}
|
|
5652
|
-
if (!
|
|
5653
|
-
|
|
6300
|
+
if (flags["project-id"] !== undefined && !UUID_PATTERN.test(flags["project-id"])) {
|
|
6301
|
+
fail("--project-id must be a project UUID");
|
|
6302
|
+
}
|
|
6303
|
+
const savedForOtherBackend = Boolean(config.token) && !explicitCredentials && Boolean(flags["api-url"]) && new URL(loadConfig().apiUrl).origin !== new URL(config.apiUrl).origin;
|
|
6304
|
+
const lease = acquireTunnelLease();
|
|
6305
|
+
if (!config.token || savedForOtherBackend) {
|
|
6306
|
+
await pairThisMachine(config.apiUrl, flags, lease);
|
|
5654
6307
|
return;
|
|
5655
6308
|
}
|
|
5656
6309
|
if (isTruthyFlag(flags.reauth) && !explicitCredentials) {
|
|
5657
6310
|
clearSavedCredentials();
|
|
5658
|
-
await pairThisMachine(config.apiUrl, flags);
|
|
6311
|
+
await pairThisMachine(config.apiUrl, flags, lease);
|
|
5659
6312
|
return;
|
|
5660
6313
|
}
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
6314
|
+
if (!jsonMode) {
|
|
6315
|
+
blankLine();
|
|
6316
|
+
console.log(` ${glyph.mark} ${c.dim}Checking saved credentials…${c.reset}`);
|
|
6317
|
+
}
|
|
5664
6318
|
let probe;
|
|
5665
6319
|
try {
|
|
5666
6320
|
probe = await probeCredentials(config, {
|
|
@@ -5677,11 +6331,15 @@ async function commandConnect(flags) {
|
|
|
5677
6331
|
if (probe === "rejected") {
|
|
5678
6332
|
if (explicitCredentials)
|
|
5679
6333
|
fail("The supplied --token was rejected for this tunnel.");
|
|
5680
|
-
|
|
6334
|
+
if (!jsonMode)
|
|
6335
|
+
console.log(` ${glyph.warn} ${c.dim}Saved token rejected — re-authorizing${c.reset}`);
|
|
5681
6336
|
clearSavedCredentials();
|
|
5682
|
-
await pairThisMachine(config.apiUrl, flags);
|
|
6337
|
+
await pairThisMachine(config.apiUrl, flags, lease);
|
|
5683
6338
|
return;
|
|
5684
6339
|
}
|
|
6340
|
+
if (jsonMode) {
|
|
6341
|
+
emit("approved", { tunnelId: config.tunnelId, capabilities: config.enabledCapabilities ?? [], existing: true });
|
|
6342
|
+
}
|
|
5685
6343
|
await launch(config, flags, lease);
|
|
5686
6344
|
}
|
|
5687
6345
|
function commandRun(flags) {
|
|
@@ -5692,8 +6350,21 @@ function commandRun(flags) {
|
|
|
5692
6350
|
});
|
|
5693
6351
|
const asService = flags.service === "true";
|
|
5694
6352
|
if (!config.token || !config.tunnelId) {
|
|
5695
|
-
|
|
5696
|
-
|
|
6353
|
+
if (!asService) {
|
|
6354
|
+
console.error(` ${glyph.bad} No saved tunnel credentials found. Run \`agent-tunnel connect\` first.`);
|
|
6355
|
+
process.exit(1);
|
|
6356
|
+
}
|
|
6357
|
+
process.stdout.write(`[agent-tunnel] no saved credential — waiting for a credential (pair this computer to start)
|
|
6358
|
+
`);
|
|
6359
|
+
const wait = setInterval(() => {
|
|
6360
|
+
const next = loadConfig({ apiUrl: flags["api-url"] });
|
|
6361
|
+
if (!next.token || !next.tunnelId)
|
|
6362
|
+
return;
|
|
6363
|
+
clearInterval(wait);
|
|
6364
|
+
rotateServiceLogs();
|
|
6365
|
+
startAgent(next, { service: true });
|
|
6366
|
+
}, 60000);
|
|
6367
|
+
return;
|
|
5697
6368
|
}
|
|
5698
6369
|
if (asService)
|
|
5699
6370
|
rotateServiceLogs();
|
|
@@ -5701,7 +6372,7 @@ function commandRun(flags) {
|
|
|
5701
6372
|
}
|
|
5702
6373
|
function lastServiceActivity() {
|
|
5703
6374
|
try {
|
|
5704
|
-
const lines =
|
|
6375
|
+
const lines = readFileSync9(join12(getServicePaths().logDir, "agent-tunnel.out.log"), "utf8").split(/\r?\n/).map((line) => stripAnsi(line).trim()).filter((line) => line.length > 0);
|
|
5705
6376
|
return lines.at(-1) ?? null;
|
|
5706
6377
|
} catch {
|
|
5707
6378
|
return null;
|
|
@@ -5719,7 +6390,11 @@ function commandStatus(flags) {
|
|
|
5719
6390
|
apiUrl: config.apiUrl,
|
|
5720
6391
|
capabilities: [...approved],
|
|
5721
6392
|
version: agentTunnelVersion(),
|
|
6393
|
+
home: agentTunnelHome(),
|
|
6394
|
+
serviceLabel: getServicePaths().label,
|
|
5722
6395
|
service,
|
|
6396
|
+
state: readAgentState(),
|
|
6397
|
+
access: readAccess(),
|
|
5723
6398
|
lastActivity: lastServiceActivity()
|
|
5724
6399
|
}, null, 2));
|
|
5725
6400
|
return;
|
|
@@ -5753,12 +6428,40 @@ function commandStatus(flags) {
|
|
|
5753
6428
|
console.log(` ${c.dim}Recent logs:${c.reset} ${c.white}agent-tunnel logs${c.reset}`);
|
|
5754
6429
|
blankLine();
|
|
5755
6430
|
}
|
|
5756
|
-
function
|
|
6431
|
+
async function unpairOnServer() {
|
|
6432
|
+
let config;
|
|
6433
|
+
try {
|
|
6434
|
+
config = loadConfig();
|
|
6435
|
+
} catch {
|
|
6436
|
+
return false;
|
|
6437
|
+
}
|
|
6438
|
+
if (!config.token || !config.tunnelId)
|
|
6439
|
+
return false;
|
|
6440
|
+
try {
|
|
6441
|
+
const response = await fetch(`${config.apiUrl}/self`, {
|
|
6442
|
+
method: "DELETE",
|
|
6443
|
+
headers: { Authorization: `Bearer ${config.token}`, "X-Tunnel-Id": config.tunnelId },
|
|
6444
|
+
signal: AbortSignal.timeout(1e4)
|
|
6445
|
+
});
|
|
6446
|
+
return response.ok;
|
|
6447
|
+
} catch {
|
|
6448
|
+
return false;
|
|
6449
|
+
}
|
|
6450
|
+
}
|
|
6451
|
+
async function commandLogout(flags) {
|
|
6452
|
+
const serverUnpaired = await unpairOnServer();
|
|
5757
6453
|
const removed = clearSavedCredentials();
|
|
6454
|
+
rmSync5(accessFilePath(), { force: true });
|
|
6455
|
+
rmSync5(accessRequestPath(), { force: true });
|
|
5758
6456
|
const keepService = isTruthyFlag(flags["keep-service"]);
|
|
5759
6457
|
if (!keepService)
|
|
5760
6458
|
SERVICE_ACTIONS.uninstall.run();
|
|
6459
|
+
if (isTruthyFlag(flags.json)) {
|
|
6460
|
+
console.log(JSON.stringify({ ok: true, serverUnpaired, credentialsCleared: removed, serviceRemoved: !keepService }));
|
|
6461
|
+
return;
|
|
6462
|
+
}
|
|
5761
6463
|
blankLine();
|
|
6464
|
+
console.log(serverUnpaired ? ` ${glyph.on} ${c.dim}Removed this computer from Kortix${c.reset}` : ` ${glyph.warn} ${c.dim}Could not reach Kortix — remove this computer from its settings${c.reset}`);
|
|
5762
6465
|
console.log(removed ? ` ${glyph.on} ${c.bold}Signed out${c.reset} ${c.dim}(credentials cleared from ${CONFIG_FILE2})${c.reset}` : ` ${glyph.off} ${c.dim}No saved credentials to clear${c.reset}`);
|
|
5763
6466
|
console.log(keepService ? ` ${glyph.warn} ${c.dim}Background service kept — it cannot authenticate until you connect again${c.reset}` : ` ${c.dim}Background service removed${c.reset}`);
|
|
5764
6467
|
blankLine();
|
|
@@ -5770,7 +6473,7 @@ function commandLogs(flags) {
|
|
|
5770
6473
|
if (isTruthyFlag(flags.clear)) {
|
|
5771
6474
|
for (const file of serviceLogFiles(paths)) {
|
|
5772
6475
|
try {
|
|
5773
|
-
|
|
6476
|
+
writeFileSync7(file, "", { mode: 384 });
|
|
5774
6477
|
} catch {}
|
|
5775
6478
|
}
|
|
5776
6479
|
blankLine();
|
|
@@ -5782,16 +6485,16 @@ function commandLogs(flags) {
|
|
|
5782
6485
|
const limit = Number.isSafeInteger(requested) && requested > 0 ? requested : DEFAULT_LOG_LINES;
|
|
5783
6486
|
const showAll = isTruthyFlag(flags.all);
|
|
5784
6487
|
for (const [label, file] of [
|
|
5785
|
-
["output",
|
|
5786
|
-
["errors",
|
|
6488
|
+
["output", join12(paths.logDir, "agent-tunnel.out.log")],
|
|
6489
|
+
["errors", join12(paths.logDir, "agent-tunnel.err.log")]
|
|
5787
6490
|
]) {
|
|
5788
6491
|
blankLine();
|
|
5789
6492
|
console.log(` ${c.bold}${c.white}${label}${c.reset} ${c.dim}${shortenHomePath(file)}${c.reset}`);
|
|
5790
|
-
if (!
|
|
6493
|
+
if (!existsSync7(file)) {
|
|
5791
6494
|
console.log(` ${c.dim}not created yet${c.reset}`);
|
|
5792
6495
|
continue;
|
|
5793
6496
|
}
|
|
5794
|
-
const kept =
|
|
6497
|
+
const kept = readFileSync9(file, "utf8").split(/\r?\n/).map((line) => line.trimEnd()).filter((line) => line.trim().length > 0).filter((line) => showAll || !isShellStartupNoise(line));
|
|
5795
6498
|
const lines = collapseRepeatedLines(kept).slice(-limit);
|
|
5796
6499
|
if (lines.length === 0) {
|
|
5797
6500
|
console.log(` ${c.dim}empty${c.reset}`);
|
|
@@ -5827,11 +6530,15 @@ var COMMANDS = {
|
|
|
5827
6530
|
},
|
|
5828
6531
|
status: { summary: "Show pairing, capabilities, and service state (--json)", run: commandStatus },
|
|
5829
6532
|
logs: { summary: "Show recent service logs (--lines <n>, --all, --clear)", run: commandLogs },
|
|
5830
|
-
start: {
|
|
6533
|
+
start: {
|
|
6534
|
+
summary: "Start the background service (resumes a paused one)",
|
|
6535
|
+
run: (f) => commandServiceAction("start", f),
|
|
6536
|
+
aliases: ["resume"]
|
|
6537
|
+
},
|
|
5831
6538
|
stop: {
|
|
5832
|
-
summary: "
|
|
6539
|
+
summary: "Pause the background service; it stays stopped after restart until `start`",
|
|
5833
6540
|
run: (f) => commandServiceAction("stop", f),
|
|
5834
|
-
aliases: ["disable"]
|
|
6541
|
+
aliases: ["disable", "pause"]
|
|
5835
6542
|
},
|
|
5836
6543
|
restart: { summary: "Restart the background service", run: (f) => commandServiceAction("restart", f) },
|
|
5837
6544
|
"install-service": {
|
|
@@ -5843,10 +6550,10 @@ var COMMANDS = {
|
|
|
5843
6550
|
run: (f) => commandServiceAction("uninstall", f)
|
|
5844
6551
|
},
|
|
5845
6552
|
"service-status": {
|
|
5846
|
-
summary: "Show the background service state (same view as status)",
|
|
6553
|
+
summary: "Show the background service state (same view as status, --json)",
|
|
5847
6554
|
run: commandStatus
|
|
5848
6555
|
},
|
|
5849
|
-
logout: { summary: "
|
|
6556
|
+
logout: { summary: "Remove this computer from Kortix, clear credentials, remove the service", run: commandLogout },
|
|
5850
6557
|
run: { summary: "Run using saved credentials (used by the service)", run: commandRun },
|
|
5851
6558
|
"start-service": { summary: "", run: (f) => commandServiceAction("start", f), hidden: true },
|
|
5852
6559
|
"stop-service": { summary: "", run: (f) => commandServiceAction("stop", f), hidden: true },
|
|
@@ -5859,7 +6566,8 @@ var OPTIONS = [
|
|
|
5859
6566
|
["--token <token> --tunnel-id <id>", "Skip device auth and use an explicit credential"],
|
|
5860
6567
|
["--reauth", "With connect: discard the saved credential and pair again"],
|
|
5861
6568
|
["--daemon / --foreground", "With connect: skip the prompt and choose the mode"],
|
|
5862
|
-
["--
|
|
6569
|
+
["--project-id <uuid>", 'With connect: offer "Also share with <project>" on the approval page; the computer is yours in every project either way'],
|
|
6570
|
+
["--json", "With connect: NDJSON events, no prompts. With status and logout: machine-readable output"],
|
|
5863
6571
|
["--keep-service", "With logout: keep the background service installed"]
|
|
5864
6572
|
];
|
|
5865
6573
|
function showHelp() {
|
|
@@ -5885,10 +6593,12 @@ function showHelp() {
|
|
|
5885
6593
|
}
|
|
5886
6594
|
blankLine();
|
|
5887
6595
|
console.log(` ${c.dim}Config: ${CONFIG_FILE2}${c.reset}`);
|
|
6596
|
+
console.log(` ${c.dim}Set AGENT_TUNNEL_HOME to use another config directory and service.${c.reset}`);
|
|
5888
6597
|
console.log(` ${c.dim}powered by ${c.cyan}kortix${c.reset}`);
|
|
5889
6598
|
blankLine();
|
|
5890
6599
|
}
|
|
5891
6600
|
var { command, flags } = parseArgs(process.argv);
|
|
6601
|
+
jsonMode = command === "connect" && isTruthyFlag(flags.json);
|
|
5892
6602
|
if (Object.prototype.hasOwnProperty.call(flags, "keep-awake")) {
|
|
5893
6603
|
console.error(` ${glyph.bad} --keep-awake is not supported. Configure sleep behavior in the operating system.`);
|
|
5894
6604
|
process.exit(2);
|
|
@@ -5898,7 +6608,6 @@ if (!resolved) {
|
|
|
5898
6608
|
showHelp();
|
|
5899
6609
|
} else {
|
|
5900
6610
|
Promise.resolve(resolved.run(flags)).catch((error) => {
|
|
5901
|
-
|
|
5902
|
-
process.exit(1);
|
|
6611
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
5903
6612
|
});
|
|
5904
6613
|
}
|