@wingman-ai/gateway 0.2.4 → 0.3.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/.wingman/agents/coding/agent.md +5 -0
- package/.wingman/agents/coding-v2/agent.md +58 -0
- package/.wingman/agents/game-dev/agent.md +94 -0
- package/.wingman/agents/game-dev/art-generation.md +37 -0
- package/.wingman/agents/game-dev/asset-refinement.md +17 -0
- package/.wingman/agents/game-dev/planning-idea.md +17 -0
- package/.wingman/agents/game-dev/ui-specialist.md +17 -0
- package/.wingman/agents/main/agent.md +2 -0
- package/README.md +1 -0
- package/dist/agent/config/agentConfig.d.ts +4 -0
- package/dist/agent/config/mcpClientManager.cjs +44 -10
- package/dist/agent/config/mcpClientManager.d.ts +6 -2
- package/dist/agent/config/mcpClientManager.js +44 -10
- package/dist/agent/config/toolRegistry.cjs +3 -1
- package/dist/agent/config/toolRegistry.js +3 -1
- package/dist/agent/tests/mcpClientManager.test.cjs +124 -0
- package/dist/agent/tests/mcpClientManager.test.d.ts +1 -0
- package/dist/agent/tests/mcpClientManager.test.js +118 -0
- package/dist/agent/tools/command_execute.cjs +1 -1
- package/dist/agent/tools/command_execute.js +1 -1
- package/dist/cli/config/schema.d.ts +2 -0
- package/dist/cli/core/agentInvoker.cjs +55 -66
- package/dist/cli/core/agentInvoker.d.ts +10 -13
- package/dist/cli/core/agentInvoker.js +42 -62
- package/dist/cli/core/imagePersistence.cjs +125 -0
- package/dist/cli/core/imagePersistence.d.ts +24 -0
- package/dist/cli/core/imagePersistence.js +85 -0
- package/dist/cli/core/sessionManager.cjs +297 -40
- package/dist/cli/core/sessionManager.d.ts +9 -0
- package/dist/cli/core/sessionManager.js +297 -40
- package/dist/debug/terminalProbe.cjs +57 -0
- package/dist/debug/terminalProbe.d.ts +10 -0
- package/dist/debug/terminalProbe.js +20 -0
- package/dist/debug/terminalProbeAuth.cjs +140 -0
- package/dist/debug/terminalProbeAuth.d.ts +20 -0
- package/dist/debug/terminalProbeAuth.js +97 -0
- package/dist/gateway/http/fs.cjs +19 -0
- package/dist/gateway/http/fs.js +19 -0
- package/dist/gateway/http/sessions.cjs +25 -5
- package/dist/gateway/http/sessions.js +25 -5
- package/dist/gateway/server.cjs +112 -11
- package/dist/gateway/server.d.ts +2 -0
- package/dist/gateway/server.js +112 -11
- package/dist/providers/codex.cjs +230 -37
- package/dist/providers/codex.d.ts +2 -0
- package/dist/providers/codex.js +231 -38
- package/dist/tests/agentInvokerSummarization.test.cjs +56 -37
- package/dist/tests/agentInvokerSummarization.test.js +58 -39
- package/dist/tests/agentInvokerWorkdir.test.cjs +50 -0
- package/dist/tests/agentInvokerWorkdir.test.js +52 -2
- package/dist/tests/cli-init.test.cjs +36 -0
- package/dist/tests/cli-init.test.js +36 -0
- package/dist/tests/codex-provider.test.cjs +173 -0
- package/dist/tests/codex-provider.test.js +174 -1
- package/dist/tests/falRuntime.test.cjs +78 -0
- package/dist/tests/falRuntime.test.d.ts +1 -0
- package/dist/tests/falRuntime.test.js +72 -0
- package/dist/tests/falSummary.test.cjs +51 -0
- package/dist/tests/falSummary.test.d.ts +1 -0
- package/dist/tests/falSummary.test.js +45 -0
- package/dist/tests/gateway.test.cjs +109 -1
- package/dist/tests/gateway.test.js +109 -1
- package/dist/tests/imagePersistence.test.cjs +143 -0
- package/dist/tests/imagePersistence.test.d.ts +1 -0
- package/dist/tests/imagePersistence.test.js +137 -0
- package/dist/tests/sessionMessageAttachments.test.cjs +30 -0
- package/dist/tests/sessionMessageAttachments.test.js +30 -0
- package/dist/tests/sessionStateMessages.test.cjs +126 -0
- package/dist/tests/sessionStateMessages.test.js +126 -0
- package/dist/tests/sessions-api.test.cjs +117 -3
- package/dist/tests/sessions-api.test.js +118 -4
- package/dist/tests/terminalProbe.test.cjs +45 -0
- package/dist/tests/terminalProbe.test.d.ts +1 -0
- package/dist/tests/terminalProbe.test.js +39 -0
- package/dist/tests/terminalProbeAuth.test.cjs +85 -0
- package/dist/tests/terminalProbeAuth.test.d.ts +1 -0
- package/dist/tests/terminalProbeAuth.test.js +79 -0
- package/dist/tools/fal/runtime.cjs +103 -0
- package/dist/tools/fal/runtime.d.ts +10 -0
- package/dist/tools/fal/runtime.js +60 -0
- package/dist/tools/fal/summary.cjs +78 -0
- package/dist/tools/fal/summary.d.ts +22 -0
- package/dist/tools/fal/summary.js +41 -0
- package/dist/tools/mcp-fal-ai.cjs +1041 -0
- package/dist/tools/mcp-fal-ai.d.ts +1 -0
- package/dist/tools/mcp-fal-ai.js +1025 -0
- package/dist/types/mcp.cjs +2 -0
- package/dist/types/mcp.d.ts +8 -0
- package/dist/types/mcp.js +3 -1
- package/dist/webui/assets/index-0nUBsUUq.js +278 -0
- package/dist/webui/assets/index-kk7OrD-G.css +11 -0
- package/dist/webui/index.html +2 -2
- package/package.json +16 -13
- package/dist/webui/assets/index-DVWQluit.css +0 -11
- package/dist/webui/assets/index-Dlyzwalc.js +0 -270
package/dist/gateway/server.js
CHANGED
|
@@ -336,12 +336,29 @@ class GatewayServer {
|
|
|
336
336
|
const sessionManager = await this.getSessionManager(agentId);
|
|
337
337
|
const existingSession = sessionManager.getSession(sessionKey);
|
|
338
338
|
const session = existingSession || sessionManager.getOrCreateSession(sessionKey, agentId);
|
|
339
|
+
const requestId = msg.id || `req-${Date.now()}`;
|
|
339
340
|
const workdir = session.metadata?.workdir ?? null;
|
|
340
341
|
const defaultOutputDir = this.resolveDefaultOutputDir(agentId);
|
|
341
342
|
const preview = hasContent ? content.trim() : buildAttachmentPreview(attachments);
|
|
342
343
|
sessionManager.updateSession(session.id, {
|
|
344
|
+
messageCount: (session.messageCount ?? 0) + 1,
|
|
343
345
|
lastMessagePreview: preview.substring(0, 200)
|
|
344
346
|
});
|
|
347
|
+
try {
|
|
348
|
+
sessionManager.persistPendingMessage({
|
|
349
|
+
sessionId: sessionKey,
|
|
350
|
+
requestId,
|
|
351
|
+
message: {
|
|
352
|
+
id: `user-${requestId}`,
|
|
353
|
+
role: "user",
|
|
354
|
+
content,
|
|
355
|
+
attachments: attachments.length > 0 ? mapAttachmentsForPendingMessage(attachments) : void 0,
|
|
356
|
+
createdAt: Date.now()
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
} catch (error) {
|
|
360
|
+
this.logger.warn("Failed to persist pending user message", error);
|
|
361
|
+
}
|
|
345
362
|
if (!existingSession) this.internalHooks?.emit({
|
|
346
363
|
type: "session",
|
|
347
364
|
action: "start",
|
|
@@ -444,9 +461,15 @@ class GatewayServer {
|
|
|
444
461
|
this.activeSessionRequests.set(sessionQueueKey, msg.id);
|
|
445
462
|
const outputManager = new OutputManager("interactive");
|
|
446
463
|
let emittedAgentError = false;
|
|
464
|
+
let streamedCompletionResult;
|
|
447
465
|
const outputHandler = (event)=>{
|
|
448
466
|
const payloadWithSession = this.attachSessionContext(event, sessionKey, agentId);
|
|
449
|
-
|
|
467
|
+
const payloadType = payloadWithSession && "object" == typeof payloadWithSession && !Array.isArray(payloadWithSession) && "string" == typeof payloadWithSession.type ? payloadWithSession.type : "";
|
|
468
|
+
if ("agent-complete" === payloadType) {
|
|
469
|
+
if (payloadWithSession && "object" == typeof payloadWithSession && !Array.isArray(payloadWithSession)) streamedCompletionResult = payloadWithSession.result;
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if ("agent-error" === payloadType) emittedAgentError = true;
|
|
450
473
|
const baseMessage = {
|
|
451
474
|
type: "event:agent",
|
|
452
475
|
id: msg.id,
|
|
@@ -477,17 +500,43 @@ class GatewayServer {
|
|
|
477
500
|
abortController
|
|
478
501
|
});
|
|
479
502
|
try {
|
|
480
|
-
await invoker.invokeAgent(agentId, content, sessionKey, attachments, {
|
|
503
|
+
const invocationResult = await invoker.invokeAgent(agentId, content, sessionKey, attachments, {
|
|
481
504
|
signal: abortController.signal
|
|
482
505
|
});
|
|
483
|
-
|
|
484
|
-
if (
|
|
485
|
-
|
|
506
|
+
if (msg.id) sessionManager.clearPendingMessagesForRequest(sessionKey, msg.id);
|
|
507
|
+
if (emittedAgentError) return;
|
|
508
|
+
const invocationCancelled = abortController.signal.aborted || "object" == typeof invocationResult && null !== invocationResult && !Array.isArray(invocationResult) && true === invocationResult.cancelled;
|
|
509
|
+
if (invocationCancelled) return void this.sendAgentError(ws, msg.id, "Request cancelled", {
|
|
510
|
+
sessionId: sessionKey,
|
|
511
|
+
agentId,
|
|
512
|
+
broadcastToSession: true,
|
|
513
|
+
exclude: ws
|
|
514
|
+
});
|
|
515
|
+
const completionResult = void 0 === streamedCompletionResult ? invocationResult : streamedCompletionResult;
|
|
516
|
+
this.sendAgentComplete(ws, msg.id, completionResult, {
|
|
517
|
+
sessionId: sessionKey,
|
|
518
|
+
agentId,
|
|
519
|
+
broadcastToSession: true,
|
|
520
|
+
exclude: ws
|
|
486
521
|
});
|
|
487
522
|
} catch (error) {
|
|
488
523
|
this.logger.error("Agent invocation failed", error);
|
|
524
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
525
|
+
if (msg.id) try {
|
|
526
|
+
sessionManager.persistPendingMessage({
|
|
527
|
+
sessionId: sessionKey,
|
|
528
|
+
requestId: msg.id,
|
|
529
|
+
message: {
|
|
530
|
+
id: msg.id,
|
|
531
|
+
role: "assistant",
|
|
532
|
+
content: message,
|
|
533
|
+
createdAt: Date.now()
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
} catch (persistError) {
|
|
537
|
+
this.logger.warn("Failed to persist pending assistant error message", persistError);
|
|
538
|
+
}
|
|
489
539
|
if (!emittedAgentError) {
|
|
490
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
491
540
|
const stack = error instanceof Error ? error.stack : void 0;
|
|
492
541
|
this.sendAgentError(ws, msg.id, message, {
|
|
493
542
|
sessionId: sessionKey,
|
|
@@ -563,6 +612,12 @@ class GatewayServer {
|
|
|
563
612
|
},
|
|
564
613
|
timestamp: Date.now()
|
|
565
614
|
});
|
|
615
|
+
this.sendAgentError(ws, requestId, "Request cancelled", {
|
|
616
|
+
sessionId: queued.sessionKey,
|
|
617
|
+
agentId: queued.agentId,
|
|
618
|
+
broadcastToSession: true,
|
|
619
|
+
exclude: ws
|
|
620
|
+
});
|
|
566
621
|
return;
|
|
567
622
|
}
|
|
568
623
|
this.sendMessage(ws, {
|
|
@@ -737,11 +792,25 @@ class GatewayServer {
|
|
|
737
792
|
}
|
|
738
793
|
sendMessage(ws, message) {
|
|
739
794
|
try {
|
|
740
|
-
ws.send(JSON.stringify(message));
|
|
795
|
+
const result = ws.send(JSON.stringify(message));
|
|
796
|
+
if ("number" == typeof result && result <= 0) return false;
|
|
797
|
+
return true;
|
|
741
798
|
} catch (error) {
|
|
742
799
|
this.log("error", "Failed to send message", error);
|
|
800
|
+
return false;
|
|
743
801
|
}
|
|
744
802
|
}
|
|
803
|
+
sendMessageWithRetry(ws, message, attempt = 0) {
|
|
804
|
+
if (this.sendMessage(ws, message)) return;
|
|
805
|
+
if (attempt >= 2) return void this.log("warn", "Dropping websocket message after retry attempts", {
|
|
806
|
+
type: message.type,
|
|
807
|
+
id: message.id
|
|
808
|
+
});
|
|
809
|
+
const delayMs = 25 * (attempt + 1);
|
|
810
|
+
setTimeout(()=>{
|
|
811
|
+
this.sendMessageWithRetry(ws, message, attempt + 1);
|
|
812
|
+
}, delayMs);
|
|
813
|
+
}
|
|
745
814
|
sendError(ws, code, message) {
|
|
746
815
|
const errorPayload = {
|
|
747
816
|
code,
|
|
@@ -753,6 +822,25 @@ class GatewayServer {
|
|
|
753
822
|
timestamp: Date.now()
|
|
754
823
|
});
|
|
755
824
|
}
|
|
825
|
+
sendAgentComplete(ws, requestId, result, options) {
|
|
826
|
+
let payload = {
|
|
827
|
+
type: "agent-complete",
|
|
828
|
+
result: result ?? null,
|
|
829
|
+
timestamp: new Date().toISOString()
|
|
830
|
+
};
|
|
831
|
+
if (options?.sessionId && options?.agentId) payload = this.attachSessionContext(payload, options.sessionId, options.agentId);
|
|
832
|
+
const baseMessage = {
|
|
833
|
+
type: "event:agent",
|
|
834
|
+
id: requestId,
|
|
835
|
+
payload,
|
|
836
|
+
timestamp: Date.now()
|
|
837
|
+
};
|
|
838
|
+
this.sendMessageWithRetry(ws, {
|
|
839
|
+
...baseMessage,
|
|
840
|
+
clientId: ws.data.clientId
|
|
841
|
+
});
|
|
842
|
+
if (options?.broadcastToSession && options.sessionId) this.broadcastSessionEvent(options.sessionId, baseMessage, options.exclude, true);
|
|
843
|
+
}
|
|
756
844
|
sendAgentError(ws, requestId, message, options) {
|
|
757
845
|
let payload = {
|
|
758
846
|
type: "agent-error",
|
|
@@ -767,11 +855,11 @@ class GatewayServer {
|
|
|
767
855
|
payload,
|
|
768
856
|
timestamp: Date.now()
|
|
769
857
|
};
|
|
770
|
-
this.
|
|
858
|
+
this.sendMessageWithRetry(ws, {
|
|
771
859
|
...baseMessage,
|
|
772
860
|
clientId: ws.data.clientId
|
|
773
861
|
});
|
|
774
|
-
if (options?.broadcastToSession && options.sessionId) this.broadcastSessionEvent(options.sessionId, baseMessage, options.exclude);
|
|
862
|
+
if (options?.broadcastToSession && options.sessionId) this.broadcastSessionEvent(options.sessionId, baseMessage, options.exclude, true);
|
|
775
863
|
}
|
|
776
864
|
cancelSocketAgentRequests(ws) {
|
|
777
865
|
for (const [requestId, active] of this.activeAgentRequests)if (active.socket === ws) {
|
|
@@ -800,12 +888,13 @@ class GatewayServer {
|
|
|
800
888
|
agentId
|
|
801
889
|
};
|
|
802
890
|
}
|
|
803
|
-
broadcastSessionEvent(sessionId, message, exclude) {
|
|
891
|
+
broadcastSessionEvent(sessionId, message, exclude, reliable = false) {
|
|
804
892
|
const subscribers = this.sessionSubscriptions.get(sessionId);
|
|
805
893
|
if (!subscribers || 0 === subscribers.size) return 0;
|
|
806
894
|
let sent = 0;
|
|
807
895
|
for (const ws of subscribers)if (!exclude || ws !== exclude) {
|
|
808
|
-
this.
|
|
896
|
+
if (reliable) this.sendMessageWithRetry(ws, message);
|
|
897
|
+
else this.sendMessage(ws, message);
|
|
809
898
|
sent++;
|
|
810
899
|
}
|
|
811
900
|
return sent;
|
|
@@ -1379,6 +1468,18 @@ function buildAttachmentPreview(attachments) {
|
|
|
1379
1468
|
if (hasAudio) return count > 1 ? "Audio attachments" : "Audio attachment";
|
|
1380
1469
|
return count > 1 ? "Image attachments" : "Image attachment";
|
|
1381
1470
|
}
|
|
1471
|
+
function mapAttachmentsForPendingMessage(attachments) {
|
|
1472
|
+
return attachments.map((attachment)=>{
|
|
1473
|
+
const kind = isFileAttachment(attachment) ? "file" : isAudioAttachment(attachment) ? "audio" : "image";
|
|
1474
|
+
return {
|
|
1475
|
+
kind,
|
|
1476
|
+
dataUrl: attachment.dataUrl,
|
|
1477
|
+
name: attachment.name,
|
|
1478
|
+
mimeType: attachment.mimeType,
|
|
1479
|
+
size: attachment.size
|
|
1480
|
+
};
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1382
1483
|
function isAudioAttachment(attachment) {
|
|
1383
1484
|
if ("audio" === attachment.kind) return true;
|
|
1384
1485
|
if (attachment.mimeType?.startsWith("audio/")) return true;
|
package/dist/providers/codex.cjs
CHANGED
|
@@ -34,6 +34,9 @@ const external_node_path_namespaceObject = require("node:path");
|
|
|
34
34
|
const external_logger_cjs_namespaceObject = require("../logger.cjs");
|
|
35
35
|
const CODEX_HOME_ENV = "CODEX_HOME";
|
|
36
36
|
const CODEX_AUTH_FILE = "auth.json";
|
|
37
|
+
const CODEX_REFRESH_TOKEN_URL_OVERRIDE_ENV = "CODEX_REFRESH_TOKEN_URL_OVERRIDE";
|
|
38
|
+
const DEFAULT_CODEX_REFRESH_TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
39
|
+
const TOKEN_REFRESH_BUFFER_MS = 300000;
|
|
37
40
|
const DEFAULT_CODEX_INSTRUCTIONS = "You are Wingman, a coding assistant. Follow the user's request exactly and keep tool usage focused.";
|
|
38
41
|
const logger = (0, external_logger_cjs_namespaceObject.createLogger)();
|
|
39
42
|
function getCodexAuthPath() {
|
|
@@ -43,53 +46,77 @@ function getCodexAuthPath() {
|
|
|
43
46
|
}
|
|
44
47
|
function resolveCodexAuthFromFile() {
|
|
45
48
|
const authPath = getCodexAuthPath();
|
|
46
|
-
|
|
49
|
+
const root = readCodexAuthRoot(authPath);
|
|
50
|
+
if (!root) return {
|
|
51
|
+
authPath
|
|
52
|
+
};
|
|
53
|
+
const tokens = root.tokens && "object" == typeof root.tokens ? root.tokens : void 0;
|
|
54
|
+
const accessToken = firstNonEmptyString([
|
|
55
|
+
tokens?.access_token,
|
|
56
|
+
root.access_token
|
|
57
|
+
]);
|
|
58
|
+
const refreshToken = firstNonEmptyString([
|
|
59
|
+
tokens?.refresh_token,
|
|
60
|
+
root.refresh_token
|
|
61
|
+
]);
|
|
62
|
+
const idToken = firstNonEmptyString([
|
|
63
|
+
tokens?.id_token,
|
|
64
|
+
root.id_token
|
|
65
|
+
]);
|
|
66
|
+
const accountId = firstNonEmptyString([
|
|
67
|
+
tokens?.account_id,
|
|
68
|
+
root.account_id,
|
|
69
|
+
extractAccountIdFromIdToken(idToken)
|
|
70
|
+
]);
|
|
71
|
+
return {
|
|
72
|
+
accessToken,
|
|
73
|
+
refreshToken,
|
|
74
|
+
idToken,
|
|
75
|
+
accountId,
|
|
47
76
|
authPath
|
|
48
77
|
};
|
|
49
|
-
try {
|
|
50
|
-
const parsed = JSON.parse((0, external_node_fs_namespaceObject.readFileSync)(authPath, "utf-8"));
|
|
51
|
-
if (!parsed || "object" != typeof parsed) return {
|
|
52
|
-
authPath
|
|
53
|
-
};
|
|
54
|
-
const root = parsed;
|
|
55
|
-
const tokens = root.tokens && "object" == typeof root.tokens ? root.tokens : void 0;
|
|
56
|
-
const accessToken = firstNonEmptyString([
|
|
57
|
-
tokens?.access_token,
|
|
58
|
-
root.access_token
|
|
59
|
-
]);
|
|
60
|
-
const accountId = firstNonEmptyString([
|
|
61
|
-
tokens?.account_id,
|
|
62
|
-
root.account_id
|
|
63
|
-
]);
|
|
64
|
-
return {
|
|
65
|
-
accessToken,
|
|
66
|
-
accountId,
|
|
67
|
-
authPath
|
|
68
|
-
};
|
|
69
|
-
} catch {
|
|
70
|
-
return {
|
|
71
|
-
authPath
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
78
|
}
|
|
75
79
|
function createCodexFetch(options = {}) {
|
|
76
80
|
const baseFetch = options.baseFetch || globalThis.fetch.bind(globalThis);
|
|
77
81
|
return async (input, init)=>{
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
let codexAuth = await maybeRefreshCodexAuth({
|
|
83
|
+
authState: resolveCodexAuthFromFile(),
|
|
84
|
+
baseFetch
|
|
85
|
+
});
|
|
86
|
+
let accessToken = codexAuth.accessToken || options.fallbackToken;
|
|
87
|
+
let accountId = codexAuth.accountId || options.fallbackAccountId;
|
|
81
88
|
if (!accessToken) throw new Error("Codex credentials missing. Run `codex login` or set CODEX_ACCESS_TOKEN.");
|
|
82
|
-
const headers = new Headers(init?.headers || {});
|
|
83
|
-
headers.delete("authorization");
|
|
84
|
-
headers.delete("x-api-key");
|
|
85
|
-
headers.set("Authorization", `Bearer ${accessToken}`);
|
|
86
|
-
if (accountId) headers.set("ChatGPT-Account-ID", accountId);
|
|
87
89
|
const body = withCodexRequestDefaults(init?.body);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
let response = await dispatchCodexRequest({
|
|
91
|
+
input,
|
|
92
|
+
init,
|
|
93
|
+
baseFetch,
|
|
94
|
+
accessToken,
|
|
95
|
+
accountId,
|
|
91
96
|
body
|
|
92
97
|
});
|
|
98
|
+
if ((401 === response.status || 403 === response.status) && canRetryCodexRequest(body) && codexAuth.refreshToken) {
|
|
99
|
+
const refreshed = await maybeRefreshCodexAuth({
|
|
100
|
+
authState: codexAuth,
|
|
101
|
+
baseFetch,
|
|
102
|
+
force: true
|
|
103
|
+
});
|
|
104
|
+
const refreshedAccessToken = refreshed.accessToken || options.fallbackToken;
|
|
105
|
+
const refreshedAccountId = refreshed.accountId || options.fallbackAccountId;
|
|
106
|
+
if (refreshedAccessToken && refreshedAccessToken !== accessToken) {
|
|
107
|
+
codexAuth = refreshed;
|
|
108
|
+
accessToken = refreshedAccessToken;
|
|
109
|
+
accountId = refreshedAccountId;
|
|
110
|
+
response = await dispatchCodexRequest({
|
|
111
|
+
input,
|
|
112
|
+
init,
|
|
113
|
+
baseFetch,
|
|
114
|
+
accessToken,
|
|
115
|
+
accountId,
|
|
116
|
+
body
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
93
120
|
if (!response.ok) {
|
|
94
121
|
let responseBody = "";
|
|
95
122
|
try {
|
|
@@ -105,6 +132,172 @@ function createCodexFetch(options = {}) {
|
|
|
105
132
|
return response;
|
|
106
133
|
};
|
|
107
134
|
}
|
|
135
|
+
async function dispatchCodexRequest(input) {
|
|
136
|
+
const headers = new Headers(input.init?.headers || {});
|
|
137
|
+
headers.delete("authorization");
|
|
138
|
+
headers.delete("x-api-key");
|
|
139
|
+
headers.set("Authorization", `Bearer ${input.accessToken}`);
|
|
140
|
+
if (input.accountId) headers.set("ChatGPT-Account-ID", input.accountId);
|
|
141
|
+
return input.baseFetch(input.input, {
|
|
142
|
+
...input.init,
|
|
143
|
+
headers,
|
|
144
|
+
body: input.body
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function canRetryCodexRequest(body) {
|
|
148
|
+
return null == body || "string" == typeof body || body instanceof URLSearchParams;
|
|
149
|
+
}
|
|
150
|
+
async function maybeRefreshCodexAuth(input) {
|
|
151
|
+
const { authState, baseFetch, force = false } = input;
|
|
152
|
+
if (!authState.refreshToken) return authState;
|
|
153
|
+
const shouldRefresh = force || !authState.accessToken || isTokenExpiredOrExpiring(authState.accessToken);
|
|
154
|
+
if (!shouldRefresh) return authState;
|
|
155
|
+
try {
|
|
156
|
+
const refreshed = await refreshCodexAuthToken(authState, baseFetch);
|
|
157
|
+
if (refreshed) return refreshed;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
logger.warn("Failed to refresh Codex token", {
|
|
160
|
+
error: error instanceof Error ? error.message : String(error)
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return authState;
|
|
164
|
+
}
|
|
165
|
+
async function refreshCodexAuthToken(authState, baseFetch) {
|
|
166
|
+
const refreshToken = authState.refreshToken;
|
|
167
|
+
if (!refreshToken) return;
|
|
168
|
+
const clientId = extractClientIdForRefresh(authState);
|
|
169
|
+
const tokenUrl = resolveCodexRefreshTokenUrl();
|
|
170
|
+
const form = new URLSearchParams({
|
|
171
|
+
grant_type: "refresh_token",
|
|
172
|
+
refresh_token: refreshToken
|
|
173
|
+
});
|
|
174
|
+
if (clientId) form.set("client_id", clientId);
|
|
175
|
+
const response = await baseFetch(tokenUrl, {
|
|
176
|
+
method: "POST",
|
|
177
|
+
headers: {
|
|
178
|
+
Accept: "application/json",
|
|
179
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
180
|
+
},
|
|
181
|
+
body: form.toString()
|
|
182
|
+
});
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
const preview = await readResponsePreview(response);
|
|
185
|
+
logger.warn("Codex token refresh failed", {
|
|
186
|
+
status: response.status,
|
|
187
|
+
statusText: response.statusText || null,
|
|
188
|
+
bodyPreview: preview || null
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const payload = await response.json();
|
|
193
|
+
const accessToken = firstNonEmptyString([
|
|
194
|
+
payload.access_token
|
|
195
|
+
]);
|
|
196
|
+
if (!accessToken) return void logger.warn("Codex token refresh failed: missing access_token");
|
|
197
|
+
const idToken = firstNonEmptyString([
|
|
198
|
+
payload.id_token,
|
|
199
|
+
authState.idToken
|
|
200
|
+
]);
|
|
201
|
+
const refreshed = {
|
|
202
|
+
accessToken,
|
|
203
|
+
refreshToken: firstNonEmptyString([
|
|
204
|
+
payload.refresh_token,
|
|
205
|
+
authState.refreshToken
|
|
206
|
+
]),
|
|
207
|
+
idToken,
|
|
208
|
+
accountId: firstNonEmptyString([
|
|
209
|
+
extractAccountIdFromIdToken(idToken),
|
|
210
|
+
authState.accountId
|
|
211
|
+
])
|
|
212
|
+
};
|
|
213
|
+
persistCodexAuthUpdate(authState.authPath, refreshed);
|
|
214
|
+
return resolveCodexAuthFromFile();
|
|
215
|
+
}
|
|
216
|
+
async function readResponsePreview(response) {
|
|
217
|
+
try {
|
|
218
|
+
const text = await response.text();
|
|
219
|
+
return text.trim().slice(0, 1200);
|
|
220
|
+
} catch {
|
|
221
|
+
return "";
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function persistCodexAuthUpdate(authPath, updated) {
|
|
225
|
+
const root = readCodexAuthRoot(authPath) || {};
|
|
226
|
+
const existingTokens = root.tokens && "object" == typeof root.tokens && !Array.isArray(root.tokens) ? root.tokens : {};
|
|
227
|
+
const tokens = {
|
|
228
|
+
...existingTokens,
|
|
229
|
+
access_token: updated.accessToken
|
|
230
|
+
};
|
|
231
|
+
if (updated.refreshToken) tokens.refresh_token = updated.refreshToken;
|
|
232
|
+
if (updated.idToken) tokens.id_token = updated.idToken;
|
|
233
|
+
if (updated.accountId) tokens.account_id = updated.accountId;
|
|
234
|
+
root.tokens = tokens;
|
|
235
|
+
root.last_refresh = new Date().toISOString();
|
|
236
|
+
(0, external_node_fs_namespaceObject.writeFileSync)(authPath, `${JSON.stringify(root, null, 2)}\n`, "utf-8");
|
|
237
|
+
}
|
|
238
|
+
function readCodexAuthRoot(authPath) {
|
|
239
|
+
if (!(0, external_node_fs_namespaceObject.existsSync)(authPath)) return;
|
|
240
|
+
try {
|
|
241
|
+
const parsed = JSON.parse((0, external_node_fs_namespaceObject.readFileSync)(authPath, "utf-8"));
|
|
242
|
+
if (!parsed || "object" != typeof parsed || Array.isArray(parsed)) return;
|
|
243
|
+
return parsed;
|
|
244
|
+
} catch {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function resolveCodexRefreshTokenUrl() {
|
|
249
|
+
const override = process.env[CODEX_REFRESH_TOKEN_URL_OVERRIDE_ENV];
|
|
250
|
+
if (override?.trim()) return override.trim();
|
|
251
|
+
return DEFAULT_CODEX_REFRESH_TOKEN_URL;
|
|
252
|
+
}
|
|
253
|
+
function extractClientIdForRefresh(authState) {
|
|
254
|
+
const accessTokenClaims = parseJwtPayload(authState.accessToken);
|
|
255
|
+
const accessTokenClientId = accessTokenClaims && "string" == typeof accessTokenClaims.client_id ? accessTokenClaims.client_id : void 0;
|
|
256
|
+
if (accessTokenClientId?.trim()) return accessTokenClientId.trim();
|
|
257
|
+
const idTokenClaims = parseJwtPayload(authState.idToken);
|
|
258
|
+
if (!idTokenClaims) return;
|
|
259
|
+
const aud = idTokenClaims.aud;
|
|
260
|
+
if ("string" == typeof aud && aud.trim()) return aud.trim();
|
|
261
|
+
if (Array.isArray(aud)) {
|
|
262
|
+
for (const value of aud)if ("string" == typeof value && value.trim()) return value.trim();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function isTokenExpiredOrExpiring(token) {
|
|
266
|
+
const expiryMs = extractTokenExpiryMs(token);
|
|
267
|
+
if (!expiryMs) return false;
|
|
268
|
+
return expiryMs <= Date.now() + TOKEN_REFRESH_BUFFER_MS;
|
|
269
|
+
}
|
|
270
|
+
function extractTokenExpiryMs(token) {
|
|
271
|
+
const payload = parseJwtPayload(token);
|
|
272
|
+
if (!payload || "number" != typeof payload.exp) return;
|
|
273
|
+
return 1000 * payload.exp;
|
|
274
|
+
}
|
|
275
|
+
function extractAccountIdFromIdToken(idToken) {
|
|
276
|
+
const payload = parseJwtPayload(idToken);
|
|
277
|
+
if (!payload) return;
|
|
278
|
+
const nested = payload["https://api.openai.com/auth"];
|
|
279
|
+
if (nested && "object" == typeof nested && !Array.isArray(nested)) {
|
|
280
|
+
const accountId = nested.chatgpt_account_id;
|
|
281
|
+
if ("string" == typeof accountId && accountId.trim()) return accountId.trim();
|
|
282
|
+
}
|
|
283
|
+
const direct = payload.chatgpt_account_id;
|
|
284
|
+
if ("string" == typeof direct && direct.trim()) return direct.trim();
|
|
285
|
+
}
|
|
286
|
+
function parseJwtPayload(token) {
|
|
287
|
+
if (!token) return;
|
|
288
|
+
const parts = token.split(".");
|
|
289
|
+
if (3 !== parts.length) return;
|
|
290
|
+
try {
|
|
291
|
+
const payload = parts[1];
|
|
292
|
+
const normalized = payload + "=".repeat((4 - payload.length % 4) % 4);
|
|
293
|
+
const decoded = Buffer.from(normalized, "base64url").toString("utf-8");
|
|
294
|
+
const parsed = JSON.parse(decoded);
|
|
295
|
+
if (!parsed || "object" != typeof parsed || Array.isArray(parsed)) return;
|
|
296
|
+
return parsed;
|
|
297
|
+
} catch {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
108
301
|
function withCodexRequestDefaults(body) {
|
|
109
302
|
if ("string" != typeof body || !body.trim()) return body;
|
|
110
303
|
try {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
type FetchLike = (input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => ReturnType<typeof fetch>;
|
|
2
2
|
export interface CodexAuthState {
|
|
3
3
|
accessToken?: string;
|
|
4
|
+
refreshToken?: string;
|
|
5
|
+
idToken?: string;
|
|
4
6
|
accountId?: string;
|
|
5
7
|
authPath: string;
|
|
6
8
|
}
|