@epiphytic/claudecodeui 1.0.1 → 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-DqxzEd_8.js +1245 -0
- package/dist/assets/index-r43D8sh4.css +32 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/database/db.js +222 -0
- package/server/database/init.sql +27 -1
- package/server/external-session-detector.js +403 -0
- package/server/index.js +885 -116
- package/server/orchestrator/client.js +361 -16
- package/server/orchestrator/index.js +83 -8
- package/server/orchestrator/protocol.js +67 -0
- package/server/projects-cache.js +196 -0
- package/server/projects.js +760 -464
- package/server/routes/projects.js +248 -92
- package/server/routes/sessions.js +106 -0
- package/server/session-lock.js +253 -0
- package/server/sessions-cache.js +183 -0
- package/server/tmux-manager.js +403 -0
- package/dist/assets/index-COkp1acE.js +0 -1231
- package/dist/assets/index-DfR9xEkp.css +0 -32
|
@@ -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
|
|
@@ -18,6 +18,7 @@ export {
|
|
|
18
18
|
createRegisterMessage,
|
|
19
19
|
createStatusUpdateMessage,
|
|
20
20
|
createPingMessage,
|
|
21
|
+
createPendingRegisterMessage,
|
|
21
22
|
createResponseMessage,
|
|
22
23
|
createResponseChunkMessage,
|
|
23
24
|
createResponseCompleteMessage,
|
|
@@ -61,6 +62,11 @@ export {
|
|
|
61
62
|
/**
|
|
62
63
|
* Creates and configures an orchestrator client from environment variables
|
|
63
64
|
*
|
|
65
|
+
* Token Precedence:
|
|
66
|
+
* 1. .env file ORCHESTRATOR_TOKEN - Highest priority, always used if set
|
|
67
|
+
* 2. Database token for specific host - Used when .env token is empty/missing
|
|
68
|
+
* 3. No token available - Uses pending mode with claim patterns
|
|
69
|
+
*
|
|
64
70
|
* @param {Object} [overrides] - Configuration overrides
|
|
65
71
|
* @returns {Promise<OrchestratorClient|null>} Configured client or null if not in client mode
|
|
66
72
|
*/
|
|
@@ -74,7 +80,6 @@ export async function createOrchestratorClientFromEnv(overrides = {}) {
|
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
const url = overrides.url || process.env.ORCHESTRATOR_URL;
|
|
77
|
-
const token = overrides.token || process.env.ORCHESTRATOR_TOKEN;
|
|
78
83
|
|
|
79
84
|
if (!url) {
|
|
80
85
|
console.warn(
|
|
@@ -83,16 +88,46 @@ export async function createOrchestratorClientFromEnv(overrides = {}) {
|
|
|
83
88
|
return null;
|
|
84
89
|
}
|
|
85
90
|
|
|
86
|
-
if
|
|
91
|
+
// Token is now optional - if not set, pending mode will be used
|
|
92
|
+
const token = overrides.token || process.env.ORCHESTRATOR_TOKEN || null;
|
|
93
|
+
|
|
94
|
+
// Build claim patterns for pending mode
|
|
95
|
+
// Fall back to ORCHESTRATOR_GITHUB_* variables if ORCHESTRATOR_CLAIM_* not set
|
|
96
|
+
const claimPatterns = overrides.claimPatterns || {
|
|
97
|
+
user:
|
|
98
|
+
process.env.ORCHESTRATOR_CLAIM_USER ||
|
|
99
|
+
process.env.ORCHESTRATOR_GITHUB_USERS ||
|
|
100
|
+
"",
|
|
101
|
+
org:
|
|
102
|
+
process.env.ORCHESTRATOR_CLAIM_ORG ||
|
|
103
|
+
process.env.ORCHESTRATOR_GITHUB_ORG ||
|
|
104
|
+
"",
|
|
105
|
+
team:
|
|
106
|
+
process.env.ORCHESTRATOR_CLAIM_TEAM ||
|
|
107
|
+
process.env.ORCHESTRATOR_GITHUB_TEAM ||
|
|
108
|
+
"",
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Validate: need either a token or at least one claim pattern
|
|
112
|
+
const hasToken = token && token.trim() !== "";
|
|
113
|
+
const hasClaimPattern =
|
|
114
|
+
(claimPatterns.user && claimPatterns.user.trim()) ||
|
|
115
|
+
(claimPatterns.org && claimPatterns.org.trim()) ||
|
|
116
|
+
(claimPatterns.team && claimPatterns.team.trim());
|
|
117
|
+
|
|
118
|
+
if (!hasToken && !hasClaimPattern) {
|
|
87
119
|
console.warn(
|
|
88
|
-
"[ORCHESTRATOR]
|
|
120
|
+
"[ORCHESTRATOR] No token and no claim patterns set, running in standalone mode",
|
|
121
|
+
);
|
|
122
|
+
console.warn(
|
|
123
|
+
"[ORCHESTRATOR] Set ORCHESTRATOR_TOKEN or at least one of ORCHESTRATOR_CLAIM_USER, ORCHESTRATOR_CLAIM_ORG, ORCHESTRATOR_CLAIM_TEAM",
|
|
89
124
|
);
|
|
90
125
|
return null;
|
|
91
126
|
}
|
|
92
127
|
|
|
93
128
|
const config = {
|
|
94
129
|
url,
|
|
95
|
-
token,
|
|
130
|
+
token: hasToken ? token : null,
|
|
96
131
|
clientId: overrides.clientId || process.env.ORCHESTRATOR_CLIENT_ID,
|
|
97
132
|
reconnectInterval:
|
|
98
133
|
overrides.reconnectInterval ||
|
|
@@ -103,6 +138,7 @@ export async function createOrchestratorClientFromEnv(overrides = {}) {
|
|
|
103
138
|
parseInt(process.env.ORCHESTRATOR_HEARTBEAT_INTERVAL) ||
|
|
104
139
|
30000,
|
|
105
140
|
metadata: overrides.metadata || {},
|
|
141
|
+
claimPatterns,
|
|
106
142
|
};
|
|
107
143
|
|
|
108
144
|
return new OrchestratorClient(config);
|
|
@@ -117,6 +153,11 @@ export async function createOrchestratorClientFromEnv(overrides = {}) {
|
|
|
117
153
|
* 3. Connects to the orchestrator
|
|
118
154
|
* 4. Sets up user request handling
|
|
119
155
|
*
|
|
156
|
+
* Token Precedence:
|
|
157
|
+
* 1. .env file ORCHESTRATOR_TOKEN - Highest priority, always used if set
|
|
158
|
+
* 2. Database token for specific host - Used when .env token is empty/missing
|
|
159
|
+
* 3. No token available - Uses pending mode with claim patterns
|
|
160
|
+
*
|
|
120
161
|
* @param {Object} options - Initialization options
|
|
121
162
|
* @param {Object} options.handlers - Handler functions for proxied requests
|
|
122
163
|
* @param {Object} [options.config] - Configuration overrides
|
|
@@ -138,11 +179,44 @@ export async function initializeOrchestrator(options = {}) {
|
|
|
138
179
|
}
|
|
139
180
|
|
|
140
181
|
const url = config.url || process.env.ORCHESTRATOR_URL;
|
|
141
|
-
const token = config.token || process.env.ORCHESTRATOR_TOKEN;
|
|
142
182
|
|
|
143
|
-
if (!url
|
|
183
|
+
if (!url) {
|
|
184
|
+
console.warn(
|
|
185
|
+
"[ORCHESTRATOR] URL not configured, running in standalone mode",
|
|
186
|
+
);
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Token is now optional - if not set, pending mode will be used
|
|
191
|
+
const token = config.token || process.env.ORCHESTRATOR_TOKEN || null;
|
|
192
|
+
|
|
193
|
+
// Build claim patterns for pending mode
|
|
194
|
+
// Fall back to ORCHESTRATOR_GITHUB_* variables if ORCHESTRATOR_CLAIM_* not set
|
|
195
|
+
const claimPatterns = config.claimPatterns || {
|
|
196
|
+
user:
|
|
197
|
+
process.env.ORCHESTRATOR_CLAIM_USER ||
|
|
198
|
+
process.env.ORCHESTRATOR_GITHUB_USERS ||
|
|
199
|
+
"",
|
|
200
|
+
org:
|
|
201
|
+
process.env.ORCHESTRATOR_CLAIM_ORG ||
|
|
202
|
+
process.env.ORCHESTRATOR_GITHUB_ORG ||
|
|
203
|
+
"",
|
|
204
|
+
team:
|
|
205
|
+
process.env.ORCHESTRATOR_CLAIM_TEAM ||
|
|
206
|
+
process.env.ORCHESTRATOR_GITHUB_TEAM ||
|
|
207
|
+
"",
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Validate: need either a token or at least one claim pattern
|
|
211
|
+
const hasToken = token && token.trim() !== "";
|
|
212
|
+
const hasClaimPattern =
|
|
213
|
+
(claimPatterns.user && claimPatterns.user.trim()) ||
|
|
214
|
+
(claimPatterns.org && claimPatterns.org.trim()) ||
|
|
215
|
+
(claimPatterns.team && claimPatterns.team.trim());
|
|
216
|
+
|
|
217
|
+
if (!hasToken && !hasClaimPattern) {
|
|
144
218
|
console.warn(
|
|
145
|
-
"[ORCHESTRATOR]
|
|
219
|
+
"[ORCHESTRATOR] No token and no claim patterns set, running in standalone mode",
|
|
146
220
|
);
|
|
147
221
|
return null;
|
|
148
222
|
}
|
|
@@ -155,7 +229,7 @@ export async function initializeOrchestrator(options = {}) {
|
|
|
155
229
|
// Create client
|
|
156
230
|
const client = new OrchestratorClient({
|
|
157
231
|
url,
|
|
158
|
-
token,
|
|
232
|
+
token: hasToken ? token : null,
|
|
159
233
|
clientId: config.clientId || process.env.ORCHESTRATOR_CLIENT_ID,
|
|
160
234
|
reconnectInterval:
|
|
161
235
|
config.reconnectInterval ||
|
|
@@ -167,6 +241,7 @@ export async function initializeOrchestrator(options = {}) {
|
|
|
167
241
|
30000,
|
|
168
242
|
metadata: config.metadata || {},
|
|
169
243
|
callbackUrl,
|
|
244
|
+
claimPatterns,
|
|
170
245
|
});
|
|
171
246
|
|
|
172
247
|
// Create status hooks
|