@epiphytic/claudecodeui 1.1.0 → 1.2.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/dist/assets/{index-D0xTNXrF.js → index-DqxzEd_8.js} +213 -215
- package/dist/assets/index-r43D8sh4.css +32 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/database/db.js +98 -0
- package/server/database/init.sql +13 -1
- package/server/index.js +69 -6
- package/server/orchestrator/client.js +361 -16
- package/server/orchestrator/index.js +83 -8
- package/server/orchestrator/protocol.js +67 -0
- package/server/projects.js +2 -1
- package/dist/assets/index-DKDK7xNY.css +0 -32
package/server/index.js
CHANGED
|
@@ -589,7 +589,7 @@ app.get(
|
|
|
589
589
|
},
|
|
590
590
|
);
|
|
591
591
|
|
|
592
|
-
// Get messages for a specific session
|
|
592
|
+
// Get messages for a specific session with ETag caching support
|
|
593
593
|
app.get(
|
|
594
594
|
"/api/projects/:projectName/sessions/:sessionId/messages",
|
|
595
595
|
authenticateToken,
|
|
@@ -609,6 +609,27 @@ app.get(
|
|
|
609
609
|
parsedOffset,
|
|
610
610
|
);
|
|
611
611
|
|
|
612
|
+
// Generate ETag based on message count and last timestamp
|
|
613
|
+
const messages = Array.isArray(result) ? result : result.messages || [];
|
|
614
|
+
const total = Array.isArray(result) ? messages.length : result.total || 0;
|
|
615
|
+
const lastTimestamp =
|
|
616
|
+
messages.length > 0
|
|
617
|
+
? messages[messages.length - 1]?.timestamp || ""
|
|
618
|
+
: "";
|
|
619
|
+
const currentETag = `"${sessionId}-${total}-${Buffer.from(lastTimestamp).toString("base64").slice(0, 16)}"`;
|
|
620
|
+
|
|
621
|
+
// Check If-None-Match header for conditional request
|
|
622
|
+
const clientETag = req.headers["if-none-match"];
|
|
623
|
+
if (clientETag && clientETag === currentETag) {
|
|
624
|
+
return res.status(304).end();
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Set caching headers
|
|
628
|
+
res.set({
|
|
629
|
+
"Cache-Control": "private, max-age=5",
|
|
630
|
+
ETag: currentETag,
|
|
631
|
+
});
|
|
632
|
+
|
|
612
633
|
// Handle both old and new response formats
|
|
613
634
|
if (Array.isArray(result)) {
|
|
614
635
|
// Backward compatibility: no pagination parameters were provided
|
|
@@ -1126,6 +1147,32 @@ async function handleChatMessage(ws, writer, messageData) {
|
|
|
1126
1147
|
const sessionIdForTracking =
|
|
1127
1148
|
data.options?.sessionId || data.sessionId || `session-${Date.now()}`;
|
|
1128
1149
|
|
|
1150
|
+
// Handle proactive external session check (before user submits a prompt)
|
|
1151
|
+
if (data.type === "check-external-session") {
|
|
1152
|
+
const projectPath = data.projectPath;
|
|
1153
|
+
if (projectPath) {
|
|
1154
|
+
const externalCheck = detectExternalClaude(projectPath);
|
|
1155
|
+
writer.send({
|
|
1156
|
+
type: "external-session-check-result",
|
|
1157
|
+
projectPath,
|
|
1158
|
+
hasExternalSession: externalCheck.hasExternalSession,
|
|
1159
|
+
details: externalCheck.hasExternalSession
|
|
1160
|
+
? {
|
|
1161
|
+
processIds: externalCheck.processes.map((p) => p.pid),
|
|
1162
|
+
commands: externalCheck.processes.map((p) => p.command),
|
|
1163
|
+
tmuxSessions: externalCheck.tmuxSessions.map(
|
|
1164
|
+
(s) => s.sessionName,
|
|
1165
|
+
),
|
|
1166
|
+
lockFile: externalCheck.lockFile.exists
|
|
1167
|
+
? externalCheck.lockFile.lockFile
|
|
1168
|
+
: null,
|
|
1169
|
+
}
|
|
1170
|
+
: null,
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1129
1176
|
if (data.type === "claude-command") {
|
|
1130
1177
|
console.log("[DEBUG] User message:", data.command || "[Continue/Resume]");
|
|
1131
1178
|
console.log("📁 Project:", data.options?.projectPath || "Unknown");
|
|
@@ -2389,6 +2436,9 @@ app.get(
|
|
|
2389
2436
|
try {
|
|
2390
2437
|
const { projectName, sessionId } = req.params;
|
|
2391
2438
|
const { provider = "claude" } = req.query;
|
|
2439
|
+
console.log(
|
|
2440
|
+
`[TOKEN-USAGE] Request for project: ${projectName}, session: ${sessionId}, provider: ${provider}`,
|
|
2441
|
+
);
|
|
2392
2442
|
const homeDir = os.homedir();
|
|
2393
2443
|
|
|
2394
2444
|
// Allow only safe characters in sessionId
|
|
@@ -2507,8 +2557,8 @@ app.get(
|
|
|
2507
2557
|
|
|
2508
2558
|
// Construct the JSONL file path
|
|
2509
2559
|
// Claude stores session files in ~/.claude/projects/[encoded-project-path]/[session-id].jsonl
|
|
2510
|
-
// The encoding replaces /, spaces, ~, and
|
|
2511
|
-
const encodedPath = projectPath.replace(/[\\/:\s~_]/g, "-");
|
|
2560
|
+
// The encoding replaces /, spaces, ~, _, and . with -
|
|
2561
|
+
const encodedPath = projectPath.replace(/[\\/:\s~_.]/g, "-");
|
|
2512
2562
|
const projectDir = path.join(homeDir, ".claude", "projects", encodedPath);
|
|
2513
2563
|
|
|
2514
2564
|
const jsonlPath = path.join(projectDir, `${safeSessionId}.jsonl`);
|
|
@@ -2528,9 +2578,22 @@ app.get(
|
|
|
2528
2578
|
fileContent = await fsPromises.readFile(jsonlPath, "utf8");
|
|
2529
2579
|
} catch (error) {
|
|
2530
2580
|
if (error.code === "ENOENT") {
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2581
|
+
// Session file doesn't exist yet (new session with no messages)
|
|
2582
|
+
// Return zero token usage instead of 404
|
|
2583
|
+
const parsedContextWindow = parseInt(process.env.CONTEXT_WINDOW, 10);
|
|
2584
|
+
const contextWindow = Number.isFinite(parsedContextWindow)
|
|
2585
|
+
? parsedContextWindow
|
|
2586
|
+
: 160000;
|
|
2587
|
+
return res.json({
|
|
2588
|
+
used: 0,
|
|
2589
|
+
total: contextWindow,
|
|
2590
|
+
breakdown: {
|
|
2591
|
+
input: 0,
|
|
2592
|
+
cacheCreation: 0,
|
|
2593
|
+
cacheRead: 0,
|
|
2594
|
+
},
|
|
2595
|
+
newSession: true,
|
|
2596
|
+
});
|
|
2534
2597
|
}
|
|
2535
2598
|
throw error; // Re-throw other errors to be caught by outer try-catch
|
|
2536
2599
|
}
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
import { EventEmitter } from "events";
|
|
9
9
|
import WebSocket from "ws";
|
|
10
10
|
import os from "os";
|
|
11
|
-
import { userDb } from "../database/db.js";
|
|
11
|
+
import { userDb, orchestratorTokensDb } from "../database/db.js";
|
|
12
12
|
import { generateToken } from "../middleware/auth.js";
|
|
13
13
|
import {
|
|
14
14
|
createRegisterMessage,
|
|
15
15
|
createStatusUpdateMessage,
|
|
16
16
|
createPingMessage,
|
|
17
|
+
createPendingRegisterMessage,
|
|
17
18
|
createResponseChunkMessage,
|
|
18
19
|
createResponseCompleteMessage,
|
|
19
20
|
createErrorMessage,
|
|
@@ -49,12 +50,16 @@ export class OrchestratorClient extends EventEmitter {
|
|
|
49
50
|
* Creates a new OrchestratorClient
|
|
50
51
|
* @param {Object} config - Configuration options
|
|
51
52
|
* @param {string} config.url - Orchestrator WebSocket URL
|
|
52
|
-
* @param {string} config.token - Authentication token
|
|
53
|
+
* @param {string} [config.token] - Authentication token (optional for pending mode)
|
|
53
54
|
* @param {string} [config.clientId] - Custom client ID (defaults to hostname-pid)
|
|
54
55
|
* @param {number} [config.reconnectInterval] - Base reconnect interval in ms
|
|
55
56
|
* @param {number} [config.heartbeatInterval] - Heartbeat interval in ms
|
|
56
57
|
* @param {Object} [config.metadata] - Additional metadata to send on register
|
|
57
58
|
* @param {string} [config.callbackUrl] - HTTP callback URL for proxying (e.g., http://localhost:3010)
|
|
59
|
+
* @param {Object} [config.claimPatterns] - Claim patterns for pending mode authorization
|
|
60
|
+
* @param {string} [config.claimPatterns.user] - GitHub username claim
|
|
61
|
+
* @param {string} [config.claimPatterns.org] - GitHub organization claim
|
|
62
|
+
* @param {string} [config.claimPatterns.team] - GitHub team claim (format: org/team-slug)
|
|
58
63
|
*/
|
|
59
64
|
constructor(config) {
|
|
60
65
|
super();
|
|
@@ -62,13 +67,10 @@ export class OrchestratorClient extends EventEmitter {
|
|
|
62
67
|
if (!config.url) {
|
|
63
68
|
throw new Error("Orchestrator URL is required");
|
|
64
69
|
}
|
|
65
|
-
if (!config.token) {
|
|
66
|
-
throw new Error("Orchestrator token is required");
|
|
67
|
-
}
|
|
68
70
|
|
|
69
71
|
this.config = {
|
|
70
72
|
url: config.url,
|
|
71
|
-
token: config.token,
|
|
73
|
+
token: config.token || null,
|
|
72
74
|
clientId: config.clientId || `${os.hostname()}-${process.pid}`,
|
|
73
75
|
reconnectInterval: config.reconnectInterval || DEFAULTS.reconnectInterval,
|
|
74
76
|
heartbeatInterval: config.heartbeatInterval || DEFAULTS.heartbeatInterval,
|
|
@@ -76,6 +78,7 @@ export class OrchestratorClient extends EventEmitter {
|
|
|
76
78
|
config.maxReconnectAttempts || DEFAULTS.maxReconnectAttempts,
|
|
77
79
|
metadata: config.metadata || {},
|
|
78
80
|
callbackUrl: config.callbackUrl || null,
|
|
81
|
+
claimPatterns: config.claimPatterns || {},
|
|
79
82
|
};
|
|
80
83
|
|
|
81
84
|
this.ws = null;
|
|
@@ -88,27 +91,97 @@ export class OrchestratorClient extends EventEmitter {
|
|
|
88
91
|
this.isConnected = false;
|
|
89
92
|
this.isRegistered = false;
|
|
90
93
|
this.shouldReconnect = true;
|
|
94
|
+
|
|
95
|
+
// Pending mode state
|
|
96
|
+
this.pendingMode = false;
|
|
97
|
+
this.pendingId = null;
|
|
98
|
+
this.orchestratorHost = null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Resolves the orchestrator token using precedence rules
|
|
103
|
+
* @returns {Promise<string|null>} The token or null if none available
|
|
104
|
+
*/
|
|
105
|
+
async resolveToken() {
|
|
106
|
+
// 1. Check config first (from .env ORCHESTRATOR_TOKEN)
|
|
107
|
+
if (this.config.token && this.config.token.trim() !== "") {
|
|
108
|
+
return this.config.token;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 2. Check database for host-specific token
|
|
112
|
+
try {
|
|
113
|
+
const url = new URL(
|
|
114
|
+
this.config.url
|
|
115
|
+
.replace("wss://", "https://")
|
|
116
|
+
.replace("ws://", "http://"),
|
|
117
|
+
);
|
|
118
|
+
this.orchestratorHost = url.host;
|
|
119
|
+
|
|
120
|
+
const stored = orchestratorTokensDb.getToken(this.orchestratorHost);
|
|
121
|
+
if (stored?.token) {
|
|
122
|
+
console.log(
|
|
123
|
+
`[ORCHESTRATOR] Using stored token for host: ${this.orchestratorHost}`,
|
|
124
|
+
);
|
|
125
|
+
return stored.token;
|
|
126
|
+
}
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error("[ORCHESTRATOR] Error resolving token:", error.message);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 3. No token available
|
|
132
|
+
return null;
|
|
91
133
|
}
|
|
92
134
|
|
|
93
135
|
/**
|
|
94
136
|
* Connects to the orchestrator server
|
|
137
|
+
* Determines whether to use authenticated or pending mode
|
|
95
138
|
* @returns {Promise<void>} Resolves when connected and registered
|
|
96
139
|
*/
|
|
97
140
|
async connect() {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
141
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Parse host for token storage
|
|
146
|
+
try {
|
|
147
|
+
const url = new URL(
|
|
148
|
+
this.config.url
|
|
149
|
+
.replace("wss://", "https://")
|
|
150
|
+
.replace("ws://", "http://"),
|
|
151
|
+
);
|
|
152
|
+
this.orchestratorHost = url.host;
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.error("[ORCHESTRATOR] Invalid orchestrator URL:", error.message);
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Resolve token
|
|
159
|
+
const token = await this.resolveToken();
|
|
160
|
+
|
|
161
|
+
if (token) {
|
|
162
|
+
// Authenticated mode - existing flow
|
|
163
|
+
this.pendingMode = false;
|
|
164
|
+
return this.connectWithToken(token);
|
|
165
|
+
} else {
|
|
166
|
+
// Pending mode - new flow
|
|
167
|
+
this.pendingMode = true;
|
|
168
|
+
return this.connectPending();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
103
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Connects in authenticated mode with a token
|
|
174
|
+
* @param {string} token - The authentication token
|
|
175
|
+
* @returns {Promise<void>} Resolves when connected and registered
|
|
176
|
+
*/
|
|
177
|
+
async connectWithToken(token) {
|
|
178
|
+
return new Promise((resolve, reject) => {
|
|
104
179
|
this.shouldReconnect = true;
|
|
105
180
|
|
|
106
181
|
try {
|
|
107
182
|
// Build connection URL with token and client_id
|
|
108
|
-
// This allows ORCHESTRATOR_URL to just be the base URL (e.g., wss://host/ws/connect)
|
|
109
|
-
// and the token from ORCHESTRATOR_TOKEN is automatically appended
|
|
110
183
|
const connectionUrl = new URL(this.config.url);
|
|
111
|
-
connectionUrl.searchParams.set("token",
|
|
184
|
+
connectionUrl.searchParams.set("token", token);
|
|
112
185
|
connectionUrl.searchParams.set("client_id", this.config.clientId);
|
|
113
186
|
const urlString = connectionUrl.toString();
|
|
114
187
|
|
|
@@ -194,6 +267,136 @@ export class OrchestratorClient extends EventEmitter {
|
|
|
194
267
|
});
|
|
195
268
|
}
|
|
196
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Connects in pending mode (no token)
|
|
272
|
+
* @returns {Promise<void>} Resolves when connected (but not fully registered)
|
|
273
|
+
*/
|
|
274
|
+
async connectPending() {
|
|
275
|
+
return new Promise((resolve, reject) => {
|
|
276
|
+
this.shouldReconnect = true;
|
|
277
|
+
|
|
278
|
+
// Build claim patterns from config
|
|
279
|
+
const { claimPatterns } = this.config;
|
|
280
|
+
const hasClaimPattern =
|
|
281
|
+
(claimPatterns.user && claimPatterns.user.trim()) ||
|
|
282
|
+
(claimPatterns.org && claimPatterns.org.trim()) ||
|
|
283
|
+
(claimPatterns.team && claimPatterns.team.trim());
|
|
284
|
+
|
|
285
|
+
if (!hasClaimPattern) {
|
|
286
|
+
const error = new Error(
|
|
287
|
+
"Pending mode requires at least one claim pattern (user, org, or team)",
|
|
288
|
+
);
|
|
289
|
+
console.error("[ORCHESTRATOR]", error.message);
|
|
290
|
+
reject(error);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
// Build pending connection URL
|
|
296
|
+
const pendingUrl = new URL(
|
|
297
|
+
this.config.url.replace("/ws/connect", "/ws/pending"),
|
|
298
|
+
);
|
|
299
|
+
if (claimPatterns.user) {
|
|
300
|
+
pendingUrl.searchParams.set("user", claimPatterns.user);
|
|
301
|
+
}
|
|
302
|
+
if (claimPatterns.org) {
|
|
303
|
+
pendingUrl.searchParams.set("org", claimPatterns.org);
|
|
304
|
+
}
|
|
305
|
+
if (claimPatterns.team) {
|
|
306
|
+
pendingUrl.searchParams.set("team", claimPatterns.team);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
console.log(
|
|
310
|
+
`[ORCHESTRATOR] Connecting in pending mode to ${pendingUrl.origin}${pendingUrl.pathname}`,
|
|
311
|
+
);
|
|
312
|
+
this.ws = new WebSocket(pendingUrl.toString());
|
|
313
|
+
|
|
314
|
+
const connectTimeout = setTimeout(() => {
|
|
315
|
+
if (!this.isConnected) {
|
|
316
|
+
this.ws.terminate();
|
|
317
|
+
reject(new Error("Connection timeout"));
|
|
318
|
+
}
|
|
319
|
+
}, 30000);
|
|
320
|
+
|
|
321
|
+
this.ws.on("open", () => {
|
|
322
|
+
clearTimeout(connectTimeout);
|
|
323
|
+
console.log("[ORCHESTRATOR] Pending mode connection established");
|
|
324
|
+
this.isConnected = true;
|
|
325
|
+
this.reconnectAttempts = 0;
|
|
326
|
+
this.currentReconnectInterval = this.config.reconnectInterval;
|
|
327
|
+
|
|
328
|
+
// Send pending registration message
|
|
329
|
+
this.sendPendingRegister();
|
|
330
|
+
this.startHeartbeat();
|
|
331
|
+
resolve();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
this.ws.on("message", (data) => {
|
|
335
|
+
this.handleMessage(data.toString());
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
this.ws.on("close", (code, reason) => {
|
|
339
|
+
clearTimeout(connectTimeout);
|
|
340
|
+
const wasConnected = this.isConnected;
|
|
341
|
+
this.isConnected = false;
|
|
342
|
+
this.isRegistered = false;
|
|
343
|
+
this.stopHeartbeat();
|
|
344
|
+
|
|
345
|
+
console.log(
|
|
346
|
+
`[ORCHESTRATOR] Pending connection closed: ${code} ${reason || ""}`,
|
|
347
|
+
);
|
|
348
|
+
this.emit("disconnected", { code, reason: reason?.toString() });
|
|
349
|
+
|
|
350
|
+
if (this.shouldReconnect) {
|
|
351
|
+
this.scheduleReconnect();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (!wasConnected) {
|
|
355
|
+
reject(new Error(`Connection failed: ${code}`));
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
this.ws.on("error", (error) => {
|
|
360
|
+
console.error("[ORCHESTRATOR] Pending mode error:", error.message);
|
|
361
|
+
this.emit("error", error);
|
|
362
|
+
});
|
|
363
|
+
} catch (error) {
|
|
364
|
+
console.error(
|
|
365
|
+
"[ORCHESTRATOR] Pending connection error:",
|
|
366
|
+
error.message,
|
|
367
|
+
);
|
|
368
|
+
reject(error);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Sends pending registration message for pending mode
|
|
375
|
+
*/
|
|
376
|
+
sendPendingRegister() {
|
|
377
|
+
this.pendingId = this.generatePendingId();
|
|
378
|
+
|
|
379
|
+
const message = createPendingRegisterMessage(
|
|
380
|
+
this.pendingId,
|
|
381
|
+
os.hostname(),
|
|
382
|
+
process.cwd(),
|
|
383
|
+
os.platform(),
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
this.sendMessage(message);
|
|
387
|
+
console.log(
|
|
388
|
+
"[ORCHESTRATOR] Sent pending registration, waiting for authorization...",
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Generates a unique pending ID
|
|
394
|
+
* @returns {string} Unique pending ID
|
|
395
|
+
*/
|
|
396
|
+
generatePendingId() {
|
|
397
|
+
return `pending_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
398
|
+
}
|
|
399
|
+
|
|
197
400
|
/**
|
|
198
401
|
* Disconnects from the orchestrator server
|
|
199
402
|
*/
|
|
@@ -375,6 +578,23 @@ export class OrchestratorClient extends EventEmitter {
|
|
|
375
578
|
this.handleHttpProxyRequest(message);
|
|
376
579
|
break;
|
|
377
580
|
|
|
581
|
+
// Pending mode messages
|
|
582
|
+
case InboundMessageTypes.PENDING_REGISTERED:
|
|
583
|
+
this.handlePendingRegistered(message);
|
|
584
|
+
break;
|
|
585
|
+
|
|
586
|
+
case InboundMessageTypes.TOKEN_GRANTED:
|
|
587
|
+
this.handleTokenGranted(message);
|
|
588
|
+
break;
|
|
589
|
+
|
|
590
|
+
case InboundMessageTypes.AUTHORIZATION_DENIED:
|
|
591
|
+
this.handleAuthorizationDenied(message);
|
|
592
|
+
break;
|
|
593
|
+
|
|
594
|
+
case InboundMessageTypes.AUTHORIZATION_TIMEOUT:
|
|
595
|
+
this.handleAuthorizationTimeout(message);
|
|
596
|
+
break;
|
|
597
|
+
|
|
378
598
|
default:
|
|
379
599
|
console.log("[ORCHESTRATOR] Unknown message type:", message.type);
|
|
380
600
|
}
|
|
@@ -452,6 +672,94 @@ export class OrchestratorClient extends EventEmitter {
|
|
|
452
672
|
this.emit("user_request", message);
|
|
453
673
|
}
|
|
454
674
|
|
|
675
|
+
/**
|
|
676
|
+
* Handles pending registration acknowledgment
|
|
677
|
+
* @param {Object} message - Pending registered message
|
|
678
|
+
*/
|
|
679
|
+
handlePendingRegistered(message) {
|
|
680
|
+
if (message.success) {
|
|
681
|
+
console.log(
|
|
682
|
+
"[ORCHESTRATOR] Pending registration accepted:",
|
|
683
|
+
message.message || "Waiting for authorization",
|
|
684
|
+
);
|
|
685
|
+
this.emit("pending_registered");
|
|
686
|
+
} else {
|
|
687
|
+
console.error(
|
|
688
|
+
"[ORCHESTRATOR] Pending registration failed:",
|
|
689
|
+
message.message || "Unknown error",
|
|
690
|
+
);
|
|
691
|
+
this.emit(
|
|
692
|
+
"error",
|
|
693
|
+
new Error(message.message || "Pending registration failed"),
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Handles token granted - authorization successful
|
|
700
|
+
* @param {Object} message - Token granted message
|
|
701
|
+
*/
|
|
702
|
+
async handleTokenGranted(message) {
|
|
703
|
+
const { token, client_id } = message;
|
|
704
|
+
|
|
705
|
+
console.log("[ORCHESTRATOR] Authorization granted! Received token.");
|
|
706
|
+
|
|
707
|
+
// Store token in database for future connections
|
|
708
|
+
try {
|
|
709
|
+
orchestratorTokensDb.saveToken(this.orchestratorHost, token, client_id);
|
|
710
|
+
console.log(
|
|
711
|
+
`[ORCHESTRATOR] Token stored for host: ${this.orchestratorHost}`,
|
|
712
|
+
);
|
|
713
|
+
} catch (error) {
|
|
714
|
+
console.error("[ORCHESTRATOR] Failed to store token:", error.message);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Update config with new token
|
|
718
|
+
this.config.token = token;
|
|
719
|
+
this.config.clientId = client_id;
|
|
720
|
+
this.pendingMode = false;
|
|
721
|
+
|
|
722
|
+
// Emit event for any listeners
|
|
723
|
+
this.emit("token_granted", { token, client_id });
|
|
724
|
+
|
|
725
|
+
// Disconnect and reconnect with the new token
|
|
726
|
+
console.log("[ORCHESTRATOR] Reconnecting with new token...");
|
|
727
|
+
this.disconnect();
|
|
728
|
+
|
|
729
|
+
// Small delay before reconnecting
|
|
730
|
+
setTimeout(async () => {
|
|
731
|
+
try {
|
|
732
|
+
await this.connectWithToken(token);
|
|
733
|
+
console.log("[ORCHESTRATOR] Successfully reconnected with new token");
|
|
734
|
+
} catch (error) {
|
|
735
|
+
console.error(
|
|
736
|
+
"[ORCHESTRATOR] Failed to reconnect with new token:",
|
|
737
|
+
error.message,
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
}, 1000);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Handles authorization denied
|
|
745
|
+
* @param {Object} message - Authorization denied message
|
|
746
|
+
*/
|
|
747
|
+
handleAuthorizationDenied(message) {
|
|
748
|
+
console.error("[ORCHESTRATOR] Authorization denied:", message.reason);
|
|
749
|
+
this.emit("authorization_denied", { reason: message.reason });
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Handles authorization timeout (e.g., 10 minutes expired)
|
|
754
|
+
* @param {Object} message - Authorization timeout message
|
|
755
|
+
*/
|
|
756
|
+
handleAuthorizationTimeout(message) {
|
|
757
|
+
console.warn("[ORCHESTRATOR] Authorization timed out:", message.message);
|
|
758
|
+
this.emit("authorization_timeout", { message: message.message });
|
|
759
|
+
// The WebSocket will be closed by the server
|
|
760
|
+
// The reconnection logic will attempt to reconnect in pending mode again
|
|
761
|
+
}
|
|
762
|
+
|
|
455
763
|
/**
|
|
456
764
|
* Handles HTTP proxy request from orchestrator
|
|
457
765
|
* Makes a local HTTP request and sends the response back
|
|
@@ -460,7 +768,9 @@ export class OrchestratorClient extends EventEmitter {
|
|
|
460
768
|
async handleHttpProxyRequest(message) {
|
|
461
769
|
const { request_id, method, path, headers, body, query, proxy_base } =
|
|
462
770
|
message;
|
|
463
|
-
console.log(
|
|
771
|
+
console.log(
|
|
772
|
+
`[ORCHESTRATOR] HTTP proxy request: ${method} ${path} (proxy_base: ${proxy_base || "none"})`,
|
|
773
|
+
);
|
|
464
774
|
|
|
465
775
|
try {
|
|
466
776
|
// Extract orchestrator user info from headers for auto-authentication
|
|
@@ -548,6 +858,30 @@ export class OrchestratorClient extends EventEmitter {
|
|
|
548
858
|
// Make the local HTTP request
|
|
549
859
|
const response = await fetch(url, fetchOptions);
|
|
550
860
|
|
|
861
|
+
// Log non-200 responses for debugging
|
|
862
|
+
if (!response.ok) {
|
|
863
|
+
console.log(
|
|
864
|
+
`[ORCHESTRATOR] HTTP proxy non-OK response: ${response.status} for ${path}`,
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// Handle 304 Not Modified - return immediately with no body
|
|
869
|
+
if (response.status === 304) {
|
|
870
|
+
const responseHeaders = [];
|
|
871
|
+
response.headers.forEach((value, key) => {
|
|
872
|
+
responseHeaders.push([key, value]);
|
|
873
|
+
});
|
|
874
|
+
const proxyResponse = createHttpProxyResponseMessage(
|
|
875
|
+
request_id,
|
|
876
|
+
304,
|
|
877
|
+
responseHeaders,
|
|
878
|
+
"",
|
|
879
|
+
);
|
|
880
|
+
this.sendMessage(proxyResponse);
|
|
881
|
+
console.log(`[ORCHESTRATOR] HTTP proxy response: 304 Not Modified`);
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
|
|
551
885
|
// Collect response headers
|
|
552
886
|
const responseHeaders = [];
|
|
553
887
|
let contentType = "";
|
|
@@ -559,23 +893,34 @@ export class OrchestratorClient extends EventEmitter {
|
|
|
559
893
|
});
|
|
560
894
|
|
|
561
895
|
// Determine if content is binary based on content-type
|
|
562
|
-
// Text types: text/*, application/json, application/javascript, application/xml, etc.
|
|
896
|
+
// Text types: text/*, application/json, application/javascript, application/xml, image/svg+xml, etc.
|
|
563
897
|
const isTextContent =
|
|
564
898
|
contentType.startsWith("text/") ||
|
|
565
899
|
contentType.includes("application/json") ||
|
|
566
900
|
contentType.includes("application/javascript") ||
|
|
567
901
|
contentType.includes("application/xml") ||
|
|
902
|
+
contentType.includes("image/svg+xml") ||
|
|
568
903
|
contentType.includes("utf-8");
|
|
569
904
|
|
|
570
905
|
// Get response body - use arrayBuffer for binary, text for text content
|
|
571
906
|
let responseBody;
|
|
572
907
|
if (isTextContent) {
|
|
573
908
|
responseBody = await response.text();
|
|
909
|
+
if (path.includes("/icons/")) {
|
|
910
|
+
console.log(
|
|
911
|
+
`[ORCHESTRATOR] Icon response (text): ${path} - ${responseBody.length} bytes, content-type: ${contentType}`,
|
|
912
|
+
);
|
|
913
|
+
}
|
|
574
914
|
} else {
|
|
575
915
|
// Binary content - read as arrayBuffer and base64 encode
|
|
576
916
|
const arrayBuffer = await response.arrayBuffer();
|
|
577
917
|
responseBody = Buffer.from(arrayBuffer).toString("base64");
|
|
578
918
|
responseHeaders.push(["x-orch-encoding", "base64"]);
|
|
919
|
+
if (path.includes("/icons/")) {
|
|
920
|
+
console.log(
|
|
921
|
+
`[ORCHESTRATOR] Icon response (binary/base64): ${path} - original ${arrayBuffer.byteLength} bytes, base64 ${responseBody.length} chars, content-type: ${contentType}`,
|
|
922
|
+
);
|
|
923
|
+
}
|
|
579
924
|
}
|
|
580
925
|
|
|
581
926
|
// Rewrite URLs if proxy_base is provided and content type is HTML or JavaScript
|