@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.
@@ -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
- return new Promise((resolve, reject) => {
99
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
100
- resolve();
101
- return;
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", this.config.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(`[ORCHESTRATOR] HTTP proxy request: ${method} ${path}`);
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. with utf-8
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 (!token) {
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] ORCHESTRATOR_TOKEN not set, running in standalone mode",
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 || !token) {
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] URL or token not configured, running in standalone mode",
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