@anusornneal/chat-relay 0.6.0 → 0.7.0
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 +311 -230
- package/agent/desktop-manager.mjs +175 -0
- package/agent/file-manager.mjs +369 -265
- package/agent/local-agent.mjs +212 -157
- package/agent/windows-desktop.ps1 +263 -0
- package/cli/config.mjs +114 -113
- package/cli/device-login.mjs +170 -168
- package/cli/index.mjs +201 -145
- package/cli/remote.mjs +94 -88
- package/package.json +75 -65
package/cli/config.mjs
CHANGED
|
@@ -1,113 +1,114 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
|
|
5
|
-
export function configDir() {
|
|
6
|
-
if (process.env.CHAT_RELAY_HOME) return path.resolve(process.env.CHAT_RELAY_HOME);
|
|
7
|
-
if (process.platform === "win32") {
|
|
8
|
-
const base = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
|
|
9
|
-
return path.join(base, "chat-relay");
|
|
10
|
-
}
|
|
11
|
-
const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
12
|
-
return path.join(base, "chat-relay");
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function configPath() {
|
|
16
|
-
return path.join(configDir(), "config.json");
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function identityPath() {
|
|
20
|
-
return path.join(configDir(), "identity.json");
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function loadIdentity() {
|
|
24
|
-
const file = identityPath();
|
|
25
|
-
if (!fs.existsSync(file)) return null;
|
|
26
|
-
try {
|
|
27
|
-
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
28
|
-
} catch {
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function saveIdentity(identity) {
|
|
34
|
-
const dir = configDir();
|
|
35
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
36
|
-
const file = identityPath();
|
|
37
|
-
fs.writeFileSync(file, JSON.stringify(identity, null, 2) + os.EOL, {
|
|
38
|
-
encoding: "utf8",
|
|
39
|
-
mode: 0o600,
|
|
40
|
-
});
|
|
41
|
-
try { fs.chmodSync(file, 0o600); } catch {}
|
|
42
|
-
return file;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function parseEnvFile(file) {
|
|
46
|
-
const result = {};
|
|
47
|
-
for (const raw of fs.readFileSync(file, "utf8").split(/\r?\n/)) {
|
|
48
|
-
const line = raw.trim();
|
|
49
|
-
if (!line || line.startsWith("#") || !line.includes("=")) continue;
|
|
50
|
-
const index = line.indexOf("=");
|
|
51
|
-
result[line.slice(0, index)] = line.slice(index + 1);
|
|
52
|
-
}
|
|
53
|
-
return result;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function migrateLegacyConfig() {
|
|
57
|
-
const legacyPath = path.resolve(".dev.vars");
|
|
58
|
-
if (!fs.existsSync(legacyPath)) return null;
|
|
59
|
-
const legacy = parseEnvFile(legacyPath);
|
|
60
|
-
if (!legacy.RELAY_URL || !legacy.CALLER_TOKEN || !legacy.AGENT_TOKEN) return null;
|
|
61
|
-
|
|
62
|
-
const config = {
|
|
63
|
-
relayUrl: legacy.RELAY_URL.replace(/\/$/, ""),
|
|
64
|
-
user: { id: "owner", name: "Owner", login: null },
|
|
65
|
-
userToken: legacy.CALLER_TOKEN,
|
|
66
|
-
agentId: legacy.AGENT_ID || "default",
|
|
67
|
-
agentName: legacy.AGENT_NAME || "Primary PC",
|
|
68
|
-
agentToken: legacy.AGENT_TOKEN,
|
|
69
|
-
allowedRoots: legacy.ALLOWED_ROOTS || defaultAllowedRoot(),
|
|
70
|
-
terminalEnabled: legacy.TERMINAL_ENABLED !== "0",
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export function configDir() {
|
|
6
|
+
if (process.env.CHAT_RELAY_HOME) return path.resolve(process.env.CHAT_RELAY_HOME);
|
|
7
|
+
if (process.platform === "win32") {
|
|
8
|
+
const base = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
|
|
9
|
+
return path.join(base, "chat-relay");
|
|
10
|
+
}
|
|
11
|
+
const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
12
|
+
return path.join(base, "chat-relay");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function configPath() {
|
|
16
|
+
return path.join(configDir(), "config.json");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function identityPath() {
|
|
20
|
+
return path.join(configDir(), "identity.json");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function loadIdentity() {
|
|
24
|
+
const file = identityPath();
|
|
25
|
+
if (!fs.existsSync(file)) return null;
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function saveIdentity(identity) {
|
|
34
|
+
const dir = configDir();
|
|
35
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
36
|
+
const file = identityPath();
|
|
37
|
+
fs.writeFileSync(file, JSON.stringify(identity, null, 2) + os.EOL, {
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
mode: 0o600,
|
|
40
|
+
});
|
|
41
|
+
try { fs.chmodSync(file, 0o600); } catch {}
|
|
42
|
+
return file;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseEnvFile(file) {
|
|
46
|
+
const result = {};
|
|
47
|
+
for (const raw of fs.readFileSync(file, "utf8").split(/\r?\n/)) {
|
|
48
|
+
const line = raw.trim();
|
|
49
|
+
if (!line || line.startsWith("#") || !line.includes("=")) continue;
|
|
50
|
+
const index = line.indexOf("=");
|
|
51
|
+
result[line.slice(0, index)] = line.slice(index + 1);
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function migrateLegacyConfig() {
|
|
57
|
+
const legacyPath = path.resolve(".dev.vars");
|
|
58
|
+
if (!fs.existsSync(legacyPath)) return null;
|
|
59
|
+
const legacy = parseEnvFile(legacyPath);
|
|
60
|
+
if (!legacy.RELAY_URL || !legacy.CALLER_TOKEN || !legacy.AGENT_TOKEN) return null;
|
|
61
|
+
|
|
62
|
+
const config = {
|
|
63
|
+
relayUrl: legacy.RELAY_URL.replace(/\/$/, ""),
|
|
64
|
+
user: { id: "owner", name: "Owner", login: null },
|
|
65
|
+
userToken: legacy.CALLER_TOKEN,
|
|
66
|
+
agentId: legacy.AGENT_ID || "default",
|
|
67
|
+
agentName: legacy.AGENT_NAME || "Primary PC",
|
|
68
|
+
agentToken: legacy.AGENT_TOKEN,
|
|
69
|
+
allowedRoots: legacy.ALLOWED_ROOTS || defaultAllowedRoot(),
|
|
70
|
+
terminalEnabled: legacy.TERMINAL_ENABLED !== "0",
|
|
71
|
+
desktopEnabled: legacy.DESKTOP_ENABLED === "1",
|
|
72
|
+
migratedAt: new Date().toISOString(),
|
|
73
|
+
};
|
|
74
|
+
saveConfig(config);
|
|
75
|
+
saveIdentity({
|
|
76
|
+
agentId: config.agentId,
|
|
77
|
+
createdAt: new Date().toISOString(),
|
|
78
|
+
updatedAt: new Date().toISOString(),
|
|
79
|
+
});
|
|
80
|
+
return config;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function loadConfig() {
|
|
84
|
+
const file = configPath();
|
|
85
|
+
if (!fs.existsSync(file)) return migrateLegacyConfig();
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
88
|
+
} catch {
|
|
89
|
+
throw new Error(`Invalid Chat Relay config: ${file}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function saveConfig(config) {
|
|
94
|
+
const dir = configDir();
|
|
95
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
96
|
+
const file = configPath();
|
|
97
|
+
fs.writeFileSync(file, JSON.stringify(config, null, 2) + os.EOL, {
|
|
98
|
+
encoding: "utf8",
|
|
99
|
+
mode: 0o600,
|
|
100
|
+
});
|
|
101
|
+
try { fs.chmodSync(file, 0o600); } catch {}
|
|
102
|
+
return file;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function clearConfig() {
|
|
106
|
+
const file = configPath();
|
|
107
|
+
if (fs.existsSync(file)) fs.rmSync(file, { force: true });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function defaultAllowedRoot() {
|
|
111
|
+
const projects = path.join(os.homedir(), "Projects");
|
|
112
|
+
if (fs.existsSync(projects)) return projects;
|
|
113
|
+
return os.homedir();
|
|
114
|
+
}
|
package/cli/device-login.mjs
CHANGED
|
@@ -1,168 +1,170 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { randomBytes } from "node:crypto";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import {
|
|
5
|
-
defaultAllowedRoot,
|
|
6
|
-
loadConfig,
|
|
7
|
-
loadIdentity,
|
|
8
|
-
saveConfig,
|
|
9
|
-
saveIdentity,
|
|
10
|
-
} from "./config.mjs";
|
|
11
|
-
|
|
12
|
-
const DEFAULT_RELAY_URL = "https://chat-relay.anusorn-hank.workers.dev";
|
|
13
|
-
|
|
14
|
-
function sanitizeAgentId(value) {
|
|
15
|
-
const normalized = value.toLowerCase().trim()
|
|
16
|
-
.replace(/[^a-z0-9_-]+/g, "-")
|
|
17
|
-
.replace(/^-+|-+$/g, "")
|
|
18
|
-
.slice(0, 54) || "machine";
|
|
19
|
-
return normalized;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function createAgentId(hostname) {
|
|
23
|
-
return `${sanitizeAgentId(hostname)}-${randomBytes(4).toString("hex")}`;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function requestJson(url, options = {}, timeoutMs = 10_000) {
|
|
27
|
-
const controller = new AbortController();
|
|
28
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
29
|
-
try {
|
|
30
|
-
const response = await fetch(url, {
|
|
31
|
-
...options,
|
|
32
|
-
signal: controller.signal,
|
|
33
|
-
headers: {
|
|
34
|
-
accept: "application/json",
|
|
35
|
-
...(options.body ? { "content-type": "application/json" } : {}),
|
|
36
|
-
...(options.headers || {}),
|
|
37
|
-
},
|
|
38
|
-
});
|
|
39
|
-
const text = await response.text();
|
|
40
|
-
let data;
|
|
41
|
-
try { data = text ? JSON.parse(text) : {}; }
|
|
42
|
-
catch { data = { raw: text }; }
|
|
43
|
-
return { response, data };
|
|
44
|
-
} finally {
|
|
45
|
-
clearTimeout(timer);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function openBrowser(url) {
|
|
50
|
-
if (process.env.CHAT_RELAY_NO_BROWSER === "1") return false;
|
|
51
|
-
try {
|
|
52
|
-
if (process.platform === "win32") {
|
|
53
|
-
const child = spawn("cmd.exe", ["/c", "start", "", url], {
|
|
54
|
-
detached: true,
|
|
55
|
-
stdio: "ignore",
|
|
56
|
-
windowsHide: true,
|
|
57
|
-
});
|
|
58
|
-
child.unref();
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
if (process.platform === "darwin") {
|
|
62
|
-
const child = spawn("open", [url], { detached: true, stdio: "ignore" });
|
|
63
|
-
child.unref();
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
const child = spawn("xdg-open", [url], { detached: true, stdio: "ignore" });
|
|
67
|
-
child.unref();
|
|
68
|
-
return true;
|
|
69
|
-
} catch {
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export async function login(options = {}) {
|
|
75
|
-
const existing = loadConfig() || {};
|
|
76
|
-
const relayUrl = String(options.relayUrl || existing.relayUrl || DEFAULT_RELAY_URL).replace(/\/$/, "");
|
|
77
|
-
const hostname = os.hostname();
|
|
78
|
-
const identity = loadIdentity() || {};
|
|
79
|
-
const requestedAgentId = options.agentId ||
|
|
80
|
-
existing.agentId ||
|
|
81
|
-
identity.agentId ||
|
|
82
|
-
createAgentId(hostname);
|
|
83
|
-
const agentName = options.agentName || existing.agentName || hostname;
|
|
84
|
-
const allowedRoots = options.allowedRoot || existing.allowedRoots || defaultAllowedRoot();
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
console.log("
|
|
102
|
-
console.log("
|
|
103
|
-
console.log(
|
|
104
|
-
console.log(`
|
|
105
|
-
console.log(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
console.log(`
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import {
|
|
5
|
+
defaultAllowedRoot,
|
|
6
|
+
loadConfig,
|
|
7
|
+
loadIdentity,
|
|
8
|
+
saveConfig,
|
|
9
|
+
saveIdentity,
|
|
10
|
+
} from "./config.mjs";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_RELAY_URL = "https://chat-relay.anusorn-hank.workers.dev";
|
|
13
|
+
|
|
14
|
+
function sanitizeAgentId(value) {
|
|
15
|
+
const normalized = value.toLowerCase().trim()
|
|
16
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
17
|
+
.replace(/^-+|-+$/g, "")
|
|
18
|
+
.slice(0, 54) || "machine";
|
|
19
|
+
return normalized;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createAgentId(hostname) {
|
|
23
|
+
return `${sanitizeAgentId(hostname)}-${randomBytes(4).toString("hex")}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function requestJson(url, options = {}, timeoutMs = 10_000) {
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
29
|
+
try {
|
|
30
|
+
const response = await fetch(url, {
|
|
31
|
+
...options,
|
|
32
|
+
signal: controller.signal,
|
|
33
|
+
headers: {
|
|
34
|
+
accept: "application/json",
|
|
35
|
+
...(options.body ? { "content-type": "application/json" } : {}),
|
|
36
|
+
...(options.headers || {}),
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
const text = await response.text();
|
|
40
|
+
let data;
|
|
41
|
+
try { data = text ? JSON.parse(text) : {}; }
|
|
42
|
+
catch { data = { raw: text }; }
|
|
43
|
+
return { response, data };
|
|
44
|
+
} finally {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function openBrowser(url) {
|
|
50
|
+
if (process.env.CHAT_RELAY_NO_BROWSER === "1") return false;
|
|
51
|
+
try {
|
|
52
|
+
if (process.platform === "win32") {
|
|
53
|
+
const child = spawn("cmd.exe", ["/c", "start", "", url], {
|
|
54
|
+
detached: true,
|
|
55
|
+
stdio: "ignore",
|
|
56
|
+
windowsHide: true,
|
|
57
|
+
});
|
|
58
|
+
child.unref();
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
if (process.platform === "darwin") {
|
|
62
|
+
const child = spawn("open", [url], { detached: true, stdio: "ignore" });
|
|
63
|
+
child.unref();
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
const child = spawn("xdg-open", [url], { detached: true, stdio: "ignore" });
|
|
67
|
+
child.unref();
|
|
68
|
+
return true;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function login(options = {}) {
|
|
75
|
+
const existing = loadConfig() || {};
|
|
76
|
+
const relayUrl = String(options.relayUrl || existing.relayUrl || DEFAULT_RELAY_URL).replace(/\/$/, "");
|
|
77
|
+
const hostname = os.hostname();
|
|
78
|
+
const identity = loadIdentity() || {};
|
|
79
|
+
const requestedAgentId = options.agentId ||
|
|
80
|
+
existing.agentId ||
|
|
81
|
+
identity.agentId ||
|
|
82
|
+
createAgentId(hostname);
|
|
83
|
+
const agentName = options.agentName || existing.agentName || hostname;
|
|
84
|
+
const allowedRoots = options.allowedRoot || existing.allowedRoots || defaultAllowedRoot();
|
|
85
|
+
const desktopEnabled = options.desktopEnabled ?? existing.desktopEnabled ?? false;
|
|
86
|
+
|
|
87
|
+
saveIdentity({
|
|
88
|
+
agentId: requestedAgentId,
|
|
89
|
+
createdAt: identity.createdAt || new Date().toISOString(),
|
|
90
|
+
updatedAt: new Date().toISOString(),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const { response, data } = await requestJson(`${relayUrl}/auth/device/start`, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
body: JSON.stringify({ agentId: requestedAgentId, agentName }),
|
|
96
|
+
});
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
throw new Error(`Unable to start device login: ${data.error || response.status}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log("");
|
|
102
|
+
console.log("Chat Relay sign in");
|
|
103
|
+
console.log("------------------");
|
|
104
|
+
console.log(`Code: ${data.userCode}`);
|
|
105
|
+
console.log(`Open: ${data.verificationUriComplete}`);
|
|
106
|
+
console.log("");
|
|
107
|
+
|
|
108
|
+
const opened = options.noOpen ? false : openBrowser(data.verificationUriComplete);
|
|
109
|
+
console.log(opened ? "Browser opened. Waiting for authorization..." : "Open the URL above to continue.");
|
|
110
|
+
|
|
111
|
+
const deadline = Date.now() + Number(data.expiresIn || 600) * 1000;
|
|
112
|
+
const intervalMs = Math.max(Number(data.interval || 2), 1) * 1000;
|
|
113
|
+
|
|
114
|
+
while (Date.now() < deadline) {
|
|
115
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
116
|
+
const tokenResult = await requestJson(`${relayUrl}/auth/device/token`, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
body: JSON.stringify({ deviceCode: data.deviceCode }),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (tokenResult.response.status === 428 && tokenResult.data.error === "authorization_pending") {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (!tokenResult.response.ok) {
|
|
125
|
+
throw new Error(`Device login failed: ${tokenResult.data.error || tokenResult.response.status}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const result = tokenResult.data;
|
|
129
|
+
|
|
130
|
+
if (existing.userToken && existing.relayUrl) {
|
|
131
|
+
const revoke = await requestJson(`${String(existing.relayUrl).replace(/\/$/, "")}/auth/session/revoke`, {
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: { authorization: `Bearer ${existing.userToken}` },
|
|
134
|
+
}).catch(() => null);
|
|
135
|
+
if (revoke && !revoke.response.ok && revoke.response.status !== 401) {
|
|
136
|
+
console.warn("Warning: previous Chat Relay session could not be revoked.");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
saveIdentity({
|
|
141
|
+
agentId: result.agent.id,
|
|
142
|
+
createdAt: identity.createdAt || new Date().toISOString(),
|
|
143
|
+
updatedAt: new Date().toISOString(),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const config = {
|
|
147
|
+
relayUrl,
|
|
148
|
+
user: result.user,
|
|
149
|
+
userToken: result.userToken,
|
|
150
|
+
userTokenExpiresAt: result.userTokenExpiresAt,
|
|
151
|
+
agentId: result.agent.id,
|
|
152
|
+
agentName: result.agent.name,
|
|
153
|
+
agentToken: result.agentToken,
|
|
154
|
+
allowedRoots,
|
|
155
|
+
terminalEnabled: true,
|
|
156
|
+
desktopEnabled: Boolean(desktopEnabled),
|
|
157
|
+
loggedInAt: new Date().toISOString(),
|
|
158
|
+
};
|
|
159
|
+
const file = saveConfig(config);
|
|
160
|
+
|
|
161
|
+
console.log(`Signed in as ${result.user.name}.`);
|
|
162
|
+
console.log(`Registered ${result.agent.name} (${result.agent.id}).`);
|
|
163
|
+
console.log(`Saved credentials to ${file}`);
|
|
164
|
+
return config;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
throw new Error("Device login expired. Run the login command again.");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export { DEFAULT_RELAY_URL, requestJson };
|