u-foo 1.9.8 → 2.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/package.json +2 -4
- package/src/agent/claudeEventTranslator.js +267 -0
- package/src/agent/claudeOauthTokenReader.js +52 -0
- package/src/agent/claudeThreadProvider.js +343 -0
- package/src/agent/cliRunner.js +4 -16
- package/src/agent/codexEventTranslator.js +78 -0
- package/src/agent/codexThreadProvider.js +181 -0
- package/src/agent/controllerToolExecutor.js +233 -0
- package/src/agent/credentials/claude.js +324 -0
- package/src/agent/credentials/codex.js +203 -0
- package/src/agent/credentials/index.js +106 -0
- package/src/agent/defaultBootstrap.js +128 -5
- package/src/agent/internalRunner.js +333 -2
- package/src/agent/loopObservability.js +190 -0
- package/src/agent/loopRuntime.js +457 -0
- package/src/agent/ufooAgent.js +178 -120
- package/src/agent/upstreamTransport.js +464 -0
- package/src/bus/utils.js +3 -2
- package/src/chat/dashboardView.js +51 -1
- package/src/chat/index.js +3 -1
- package/src/config.js +53 -17
- package/src/controller/flags.js +160 -0
- package/src/controller/gateRouter.js +201 -0
- package/src/controller/routerFastPath.js +22 -0
- package/src/controller/shadowGuard.js +280 -0
- package/src/daemon/index.js +2 -3
- package/src/daemon/promptLoop.js +33 -224
- package/src/daemon/promptRequest.js +360 -5
- package/src/daemon/status.js +2 -0
- package/src/history/inputTimeline.js +9 -4
- package/src/memory/index.js +24 -0
- package/src/providerapi/redactor.js +87 -0
- package/src/providerapi/shadowDiff.js +174 -0
- package/src/report/store.js +4 -3
- package/src/tools/handlers/ackBus.js +26 -0
- package/src/tools/handlers/common.js +64 -0
- package/src/tools/handlers/dispatchMessage.js +81 -0
- package/src/tools/handlers/listAgents.js +14 -0
- package/src/tools/handlers/readBusSummary.js +34 -0
- package/src/tools/handlers/readOpenDecisions.js +26 -0
- package/src/tools/handlers/readProjectRegistry.js +20 -0
- package/src/tools/handlers/readPromptHistory.js +123 -0
- package/src/tools/handlers/tier2.js +134 -0
- package/src/tools/index.js +55 -0
- package/src/tools/registry.js +69 -0
- package/src/tools/schemaFixtures.js +415 -0
- package/src/tools/tier0/listAgents.js +14 -0
- package/src/tools/tier0/readBusSummary.js +14 -0
- package/src/tools/tier0/readOpenDecisions.js +14 -0
- package/src/tools/tier0/readProjectRegistry.js +14 -0
- package/src/tools/tier0/readPromptHistory.js +14 -0
- package/src/tools/tier1/ackBus.js +14 -0
- package/src/tools/tier1/dispatchMessage.js +14 -0
- package/src/tools/tier1/routeAgent.js +14 -0
- package/src/tools/tier2/closeAgent.js +14 -0
- package/src/tools/tier2/launchAgent.js +14 -0
- package/src/tools/tier2/manageCron.js +14 -0
- package/src/tools/tier2/renameAgent.js +14 -0
- package/src/tools/types.js +75 -0
- package/src/tools/unimplemented.js +13 -0
- package/src/ufoo/paths.js +4 -0
- package/bin/ufoo-assistant-agent.js +0 -5
- package/bin/ufoo-engine.js +0 -25
- package/src/assistant/agent.js +0 -261
- package/src/assistant/bridge.js +0 -178
- package/src/assistant/constants.js +0 -15
- package/src/assistant/engine.js +0 -252
- package/src/assistant/stdio.js +0 -58
- package/src/assistant/ufooEngineCli.js +0 -312
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const {
|
|
7
|
+
buildCredentialDescriptor,
|
|
8
|
+
parseTimestamp,
|
|
9
|
+
} = require("./index");
|
|
10
|
+
|
|
11
|
+
const DEFAULT_REFRESH_WINDOW_MS = 5 * 60 * 1000;
|
|
12
|
+
const DEFAULT_LOCK_TIMEOUT_MS = 3000;
|
|
13
|
+
const DEFAULT_LOCK_RETRY_MS = 25;
|
|
14
|
+
const DEFAULT_STALE_LOCK_MS = 30 * 1000;
|
|
15
|
+
|
|
16
|
+
function sleep(ms) {
|
|
17
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function defaultClaudeConfigDir() {
|
|
21
|
+
return path.join(os.homedir(), ".claude");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function resolveClaudeOauthPaths(options = {}) {
|
|
25
|
+
const configDir = String(options.configDir || defaultClaudeConfigDir()).trim() || defaultClaudeConfigDir();
|
|
26
|
+
const profile = String(options.profile || "").trim();
|
|
27
|
+
const explicitTokenPath = String(options.tokenPath || "").trim();
|
|
28
|
+
const profileDir = profile ? path.join(configDir, "profiles", profile) : configDir;
|
|
29
|
+
const tokenPath = explicitTokenPath || path.join(profileDir, "oauth.json");
|
|
30
|
+
return {
|
|
31
|
+
configDir,
|
|
32
|
+
profile,
|
|
33
|
+
profileDir,
|
|
34
|
+
tokenPath,
|
|
35
|
+
lockPath: `${tokenPath}.lock`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function classifyTokenState(expiresAtMs, nowMs, refreshWindowMs) {
|
|
40
|
+
if (!Number.isFinite(expiresAtMs)) return "invalid";
|
|
41
|
+
if (expiresAtMs <= nowMs) return "expired";
|
|
42
|
+
if ((expiresAtMs - nowMs) <= refreshWindowMs) return "near_expiry";
|
|
43
|
+
return "fresh";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseClaudeOauthFile(raw = {}) {
|
|
47
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
48
|
+
const err = new Error("Claude OAuth token payload must be a JSON object");
|
|
49
|
+
err.code = "CLAUDE_OAUTH_INVALID";
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (
|
|
54
|
+
raw.version === "claude-code-oauth-v1"
|
|
55
|
+
&& typeof raw.accessToken === "string"
|
|
56
|
+
&& typeof raw.refreshToken === "string"
|
|
57
|
+
) {
|
|
58
|
+
return {
|
|
59
|
+
schemaVersion: "claude-code-oauth-v1",
|
|
60
|
+
raw,
|
|
61
|
+
accessToken: raw.accessToken,
|
|
62
|
+
refreshToken: raw.refreshToken,
|
|
63
|
+
expiresAt: typeof raw.expiresAt === "string" ? raw.expiresAt : "",
|
|
64
|
+
tokenType: typeof raw.tokenType === "string" ? raw.tokenType : "Bearer",
|
|
65
|
+
refreshUrl: typeof raw.refreshUrl === "string" ? raw.refreshUrl : "",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (
|
|
70
|
+
raw.version === 2
|
|
71
|
+
&& raw.oauth
|
|
72
|
+
&& typeof raw.oauth === "object"
|
|
73
|
+
&& typeof raw.oauth.access_token === "string"
|
|
74
|
+
&& typeof raw.oauth.refresh_token === "string"
|
|
75
|
+
) {
|
|
76
|
+
return {
|
|
77
|
+
schemaVersion: "claude-code-oauth-v2",
|
|
78
|
+
raw,
|
|
79
|
+
accessToken: raw.oauth.access_token,
|
|
80
|
+
refreshToken: raw.oauth.refresh_token,
|
|
81
|
+
expiresAt: typeof raw.oauth.expires_at === "string" ? raw.oauth.expires_at : "",
|
|
82
|
+
tokenType: typeof raw.oauth.token_type === "string" ? raw.oauth.token_type : "Bearer",
|
|
83
|
+
refreshUrl: typeof raw.oauth.refresh_url === "string" ? raw.oauth.refresh_url : "",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const err = new Error("Unsupported Claude OAuth token schema");
|
|
88
|
+
err.code = "CLAUDE_OAUTH_SCHEMA_UNSUPPORTED";
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function serializeClaudeOauthToken(parsedToken, updates = {}) {
|
|
93
|
+
const accessToken = typeof updates.accessToken === "string" ? updates.accessToken : parsedToken.accessToken;
|
|
94
|
+
const refreshToken = typeof updates.refreshToken === "string" ? updates.refreshToken : parsedToken.refreshToken;
|
|
95
|
+
const expiresAt = typeof updates.expiresAt === "string" ? updates.expiresAt : parsedToken.expiresAt;
|
|
96
|
+
const tokenType = typeof updates.tokenType === "string" ? updates.tokenType : parsedToken.tokenType;
|
|
97
|
+
const refreshUrl = typeof updates.refreshUrl === "string" ? updates.refreshUrl : parsedToken.refreshUrl;
|
|
98
|
+
|
|
99
|
+
if (parsedToken.schemaVersion === "claude-code-oauth-v1") {
|
|
100
|
+
return {
|
|
101
|
+
...parsedToken.raw,
|
|
102
|
+
accessToken,
|
|
103
|
+
refreshToken,
|
|
104
|
+
expiresAt,
|
|
105
|
+
tokenType,
|
|
106
|
+
refreshUrl,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (parsedToken.schemaVersion === "claude-code-oauth-v2") {
|
|
111
|
+
return {
|
|
112
|
+
...parsedToken.raw,
|
|
113
|
+
oauth: {
|
|
114
|
+
...parsedToken.raw.oauth,
|
|
115
|
+
access_token: accessToken,
|
|
116
|
+
refresh_token: refreshToken,
|
|
117
|
+
expires_at: expiresAt,
|
|
118
|
+
token_type: tokenType,
|
|
119
|
+
refresh_url: refreshUrl,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const err = new Error(`Unsupported Claude OAuth schema version: ${parsedToken.schemaVersion}`);
|
|
125
|
+
err.code = "CLAUDE_OAUTH_SCHEMA_UNSUPPORTED";
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function withLockFile(lockPath, options = {}, fn) {
|
|
130
|
+
const fsModule = options.fsModule || fs;
|
|
131
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : DEFAULT_LOCK_TIMEOUT_MS;
|
|
132
|
+
const retryMs = Number.isFinite(options.retryMs) ? options.retryMs : DEFAULT_LOCK_RETRY_MS;
|
|
133
|
+
const staleMs = Number.isFinite(options.staleMs) ? options.staleMs : DEFAULT_STALE_LOCK_MS;
|
|
134
|
+
const sleepFn = typeof options.sleep === "function" ? options.sleep : sleep;
|
|
135
|
+
const startedAt = Date.now();
|
|
136
|
+
|
|
137
|
+
fsModule.mkdirSync(path.dirname(lockPath), { recursive: true, mode: 0o700 });
|
|
138
|
+
|
|
139
|
+
while ((Date.now() - startedAt) <= timeoutMs) {
|
|
140
|
+
let fd = null;
|
|
141
|
+
try {
|
|
142
|
+
fd = fsModule.openSync(lockPath, "wx", 0o600);
|
|
143
|
+
try {
|
|
144
|
+
return await fn();
|
|
145
|
+
} finally {
|
|
146
|
+
try { fsModule.closeSync(fd); } catch {}
|
|
147
|
+
try { fsModule.unlinkSync(lockPath); } catch {}
|
|
148
|
+
}
|
|
149
|
+
} catch (err) {
|
|
150
|
+
if (!err || err.code !== "EEXIST") throw err;
|
|
151
|
+
try {
|
|
152
|
+
const stat = fsModule.statSync(lockPath);
|
|
153
|
+
if ((Date.now() - stat.mtimeMs) > staleMs) {
|
|
154
|
+
fsModule.unlinkSync(lockPath);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
} catch {}
|
|
158
|
+
// eslint-disable-next-line no-await-in-loop
|
|
159
|
+
await sleepFn(retryMs);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const err = new Error(`Timed out waiting for Claude OAuth lock: ${lockPath}`);
|
|
164
|
+
err.code = "CLAUDE_OAUTH_LOCK_TIMEOUT";
|
|
165
|
+
throw err;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function atomicWriteJson(filePath, value, options = {}) {
|
|
169
|
+
const fsModule = options.fsModule || fs;
|
|
170
|
+
const dir = path.dirname(filePath);
|
|
171
|
+
const tmpPath = path.join(dir, `.tmp-${path.basename(filePath)}.${process.pid}.${Date.now()}`);
|
|
172
|
+
fsModule.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
173
|
+
fsModule.writeFileSync(tmpPath, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
174
|
+
fsModule.renameSync(tmpPath, filePath);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
class ClaudeUpstreamCredentialResolver {
|
|
178
|
+
constructor(options = {}) {
|
|
179
|
+
this.fs = options.fsModule || fs;
|
|
180
|
+
this.env = options.env || process.env;
|
|
181
|
+
this.now = typeof options.now === "function" ? options.now : () => Date.now();
|
|
182
|
+
this.refreshAccessToken = typeof options.refreshAccessToken === "function"
|
|
183
|
+
? options.refreshAccessToken
|
|
184
|
+
: null;
|
|
185
|
+
this.sleep = typeof options.sleep === "function" ? options.sleep : sleep;
|
|
186
|
+
this.lockTimeoutMs = Number.isFinite(options.lockTimeoutMs) ? options.lockTimeoutMs : DEFAULT_LOCK_TIMEOUT_MS;
|
|
187
|
+
this.lockRetryMs = Number.isFinite(options.lockRetryMs) ? options.lockRetryMs : DEFAULT_LOCK_RETRY_MS;
|
|
188
|
+
this.lockStaleMs = Number.isFinite(options.lockStaleMs) ? options.lockStaleMs : DEFAULT_STALE_LOCK_MS;
|
|
189
|
+
this.refreshWindowMs = Number.isFinite(options.refreshWindowMs)
|
|
190
|
+
? options.refreshWindowMs
|
|
191
|
+
: DEFAULT_REFRESH_WINDOW_MS;
|
|
192
|
+
this.paths = resolveClaudeOauthPaths(options);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
resolvePaths() {
|
|
196
|
+
return { ...this.paths };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
readTokenFile() {
|
|
200
|
+
const raw = JSON.parse(this.fs.readFileSync(this.paths.tokenPath, "utf8"));
|
|
201
|
+
const parsed = parseClaudeOauthFile(raw);
|
|
202
|
+
const nowMs = this.now();
|
|
203
|
+
const expiresAtMs = parseTimestamp(parsed.expiresAt);
|
|
204
|
+
return {
|
|
205
|
+
...parsed,
|
|
206
|
+
expiresAtMs,
|
|
207
|
+
state: classifyTokenState(expiresAtMs, nowMs, this.refreshWindowMs),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
buildResolvedCredential(tokenRecord) {
|
|
212
|
+
return buildCredentialDescriptor({
|
|
213
|
+
provider: "claude",
|
|
214
|
+
credentialKind: "oauth",
|
|
215
|
+
source: "oauth",
|
|
216
|
+
accessToken: tokenRecord.accessToken,
|
|
217
|
+
refreshToken: tokenRecord.refreshToken,
|
|
218
|
+
tokenType: tokenRecord.tokenType || "Bearer",
|
|
219
|
+
state: tokenRecord.state,
|
|
220
|
+
expiresAt: tokenRecord.expiresAt,
|
|
221
|
+
expiresAtMs: tokenRecord.expiresAtMs,
|
|
222
|
+
refreshable: Boolean(tokenRecord.refreshToken),
|
|
223
|
+
profile: this.paths.profile,
|
|
224
|
+
credentialPath: this.paths.tokenPath,
|
|
225
|
+
schemaVersion: tokenRecord.schemaVersion,
|
|
226
|
+
refreshWindowMs: this.refreshWindowMs,
|
|
227
|
+
nowMs: this.now(),
|
|
228
|
+
metadata: {
|
|
229
|
+
tokenPath: this.paths.tokenPath,
|
|
230
|
+
refreshUrl: tokenRecord.refreshUrl || "",
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async refreshIfNeeded(initialRecord) {
|
|
236
|
+
if (!this.refreshAccessToken) {
|
|
237
|
+
return this.buildResolvedCredential(initialRecord);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return withLockFile(this.paths.lockPath, {
|
|
241
|
+
fsModule: this.fs,
|
|
242
|
+
timeoutMs: this.lockTimeoutMs,
|
|
243
|
+
retryMs: this.lockRetryMs,
|
|
244
|
+
staleMs: this.lockStaleMs,
|
|
245
|
+
sleep: this.sleep,
|
|
246
|
+
}, async () => {
|
|
247
|
+
const currentRecord = this.readTokenFile();
|
|
248
|
+
if (currentRecord.state === "fresh") {
|
|
249
|
+
return this.buildResolvedCredential(currentRecord);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const refreshed = await this.refreshAccessToken({
|
|
253
|
+
profile: this.paths.profile,
|
|
254
|
+
tokenPath: this.paths.tokenPath,
|
|
255
|
+
refreshToken: currentRecord.refreshToken,
|
|
256
|
+
accessToken: currentRecord.accessToken,
|
|
257
|
+
expiresAt: currentRecord.expiresAt,
|
|
258
|
+
refreshUrl: currentRecord.refreshUrl,
|
|
259
|
+
tokenType: currentRecord.tokenType,
|
|
260
|
+
schemaVersion: currentRecord.schemaVersion,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const nextRaw = serializeClaudeOauthToken(currentRecord, refreshed || {});
|
|
264
|
+
atomicWriteJson(this.paths.tokenPath, nextRaw, { fsModule: this.fs });
|
|
265
|
+
|
|
266
|
+
const nextRecord = this.readTokenFile();
|
|
267
|
+
return this.buildResolvedCredential(nextRecord);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async resolveCredentials() {
|
|
272
|
+
const apiKey = typeof this.env.ANTHROPIC_API_KEY === "string" ? this.env.ANTHROPIC_API_KEY.trim() : "";
|
|
273
|
+
if (apiKey) {
|
|
274
|
+
return buildCredentialDescriptor({
|
|
275
|
+
provider: "claude",
|
|
276
|
+
credentialKind: "api-key",
|
|
277
|
+
source: "api-key",
|
|
278
|
+
apiKey,
|
|
279
|
+
tokenType: "Bearer",
|
|
280
|
+
state: "fresh",
|
|
281
|
+
refreshable: false,
|
|
282
|
+
profile: this.paths.profile,
|
|
283
|
+
credentialPath: "",
|
|
284
|
+
nowMs: this.now(),
|
|
285
|
+
refreshWindowMs: this.refreshWindowMs,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
let tokenRecord;
|
|
290
|
+
try {
|
|
291
|
+
tokenRecord = this.readTokenFile();
|
|
292
|
+
} catch (err) {
|
|
293
|
+
if (err && err.code === "ENOENT") {
|
|
294
|
+
const missing = new Error("Claude OAuth token not found and ANTHROPIC_API_KEY is unset");
|
|
295
|
+
missing.code = "CLAUDE_AUTH_UNAVAILABLE";
|
|
296
|
+
throw missing;
|
|
297
|
+
}
|
|
298
|
+
throw err;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (tokenRecord.state === "fresh") {
|
|
302
|
+
return this.buildResolvedCredential(tokenRecord);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return this.refreshIfNeeded(tokenRecord);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function resolveClaudeUpstreamCredentials(options = {}) {
|
|
310
|
+
const resolver = new ClaudeUpstreamCredentialResolver(options);
|
|
311
|
+
return resolver.resolveCredentials();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
module.exports = {
|
|
315
|
+
ClaudeUpstreamCredentialResolver,
|
|
316
|
+
DEFAULT_REFRESH_WINDOW_MS,
|
|
317
|
+
resolveClaudeUpstreamCredentials,
|
|
318
|
+
resolveClaudeOauthPaths,
|
|
319
|
+
parseClaudeOauthFile,
|
|
320
|
+
serializeClaudeOauthToken,
|
|
321
|
+
classifyTokenState,
|
|
322
|
+
withLockFile,
|
|
323
|
+
atomicWriteJson,
|
|
324
|
+
};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const {
|
|
7
|
+
buildCredentialDescriptor,
|
|
8
|
+
parseTimestamp,
|
|
9
|
+
} = require("./index");
|
|
10
|
+
|
|
11
|
+
function defaultCodexConfigDir() {
|
|
12
|
+
return path.join(os.homedir(), ".codex");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function resolveCodexAuthPaths(options = {}) {
|
|
16
|
+
const configDir = String(options.configDir || defaultCodexConfigDir()).trim() || defaultCodexConfigDir();
|
|
17
|
+
const explicitAuthPath = String(options.authPath || "").trim();
|
|
18
|
+
const authPath = explicitAuthPath || path.join(configDir, "auth.json");
|
|
19
|
+
return {
|
|
20
|
+
configDir,
|
|
21
|
+
authPath,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function decodeJwtPayload(token = "") {
|
|
26
|
+
const parts = String(token || "").split(".");
|
|
27
|
+
if (parts.length < 2) return {};
|
|
28
|
+
try {
|
|
29
|
+
const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
30
|
+
const padded = payload.padEnd(Math.ceil(payload.length / 4) * 4, "=");
|
|
31
|
+
return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
|
|
32
|
+
} catch {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseCodexAuthFile(raw = {}) {
|
|
38
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
39
|
+
const err = new Error("Codex auth payload must be a JSON object");
|
|
40
|
+
err.code = "CODEX_AUTH_INVALID";
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const tokens = raw.tokens && typeof raw.tokens === "object" ? raw.tokens : null;
|
|
45
|
+
const apiKey = typeof raw.OPENAI_API_KEY === "string" ? raw.OPENAI_API_KEY : "";
|
|
46
|
+
const accessToken = tokens && typeof tokens.access_token === "string"
|
|
47
|
+
? tokens.access_token
|
|
48
|
+
: (typeof raw.access_token === "string" ? raw.access_token : "");
|
|
49
|
+
const refreshToken = tokens && typeof tokens.refresh_token === "string"
|
|
50
|
+
? tokens.refresh_token
|
|
51
|
+
: (typeof raw.refresh_token === "string" ? raw.refresh_token : "");
|
|
52
|
+
const idToken = tokens && typeof tokens.id_token === "string"
|
|
53
|
+
? tokens.id_token
|
|
54
|
+
: (typeof raw.id_token === "string" ? raw.id_token : "");
|
|
55
|
+
const accountId = tokens && typeof tokens.account_id === "string"
|
|
56
|
+
? tokens.account_id
|
|
57
|
+
: (typeof raw.account_id === "string" ? raw.account_id : "");
|
|
58
|
+
const expiresAt = typeof raw.expired === "string"
|
|
59
|
+
? raw.expired
|
|
60
|
+
: (typeof raw.expire === "string" ? raw.expire : "");
|
|
61
|
+
const email = typeof raw.email === "string" ? raw.email : "";
|
|
62
|
+
|
|
63
|
+
if (!apiKey && !accessToken) {
|
|
64
|
+
const err = new Error("Unsupported Codex auth schema");
|
|
65
|
+
err.code = "CODEX_AUTH_SCHEMA_UNSUPPORTED";
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
schemaVersion: "codex-auth-v1",
|
|
71
|
+
raw,
|
|
72
|
+
apiKey,
|
|
73
|
+
accessToken,
|
|
74
|
+
refreshToken,
|
|
75
|
+
idToken,
|
|
76
|
+
accountId,
|
|
77
|
+
email,
|
|
78
|
+
expiresAt,
|
|
79
|
+
lastRefresh: typeof raw.last_refresh === "string" ? raw.last_refresh : "",
|
|
80
|
+
authMode: typeof raw.auth_mode === "string" ? raw.auth_mode : "",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
class CodexUpstreamCredentialResolver {
|
|
85
|
+
constructor(options = {}) {
|
|
86
|
+
this.fs = options.fsModule || fs;
|
|
87
|
+
this.env = options.env || process.env;
|
|
88
|
+
this.now = typeof options.now === "function" ? options.now : () => Date.now();
|
|
89
|
+
this.paths = resolveCodexAuthPaths(options);
|
|
90
|
+
this.refreshWindowMs = Number.isFinite(options.refreshWindowMs) ? options.refreshWindowMs : 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
resolvePaths() {
|
|
94
|
+
return { ...this.paths };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
readAuthFile() {
|
|
98
|
+
const raw = JSON.parse(this.fs.readFileSync(this.paths.authPath, "utf8"));
|
|
99
|
+
const parsed = parseCodexAuthFile(raw);
|
|
100
|
+
const claims = decodeJwtPayload(parsed.idToken);
|
|
101
|
+
const derivedEmail = typeof claims.email === "string" ? claims.email : "";
|
|
102
|
+
const derivedExpiresAt = Number.isFinite(Number(claims.exp))
|
|
103
|
+
? new Date(Number(claims.exp) * 1000).toISOString()
|
|
104
|
+
: "";
|
|
105
|
+
const expiresAt = parsed.expiresAt || derivedExpiresAt;
|
|
106
|
+
return {
|
|
107
|
+
...parsed,
|
|
108
|
+
accountEmail: parsed.email || derivedEmail,
|
|
109
|
+
expiresAt,
|
|
110
|
+
expiresAtMs: parseTimestamp(expiresAt),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
buildResolvedCredential(record) {
|
|
115
|
+
if (record.apiKey) {
|
|
116
|
+
return buildCredentialDescriptor({
|
|
117
|
+
provider: "codex",
|
|
118
|
+
credentialKind: "api-key",
|
|
119
|
+
source: "auth-file",
|
|
120
|
+
apiKey: record.apiKey,
|
|
121
|
+
tokenType: "Bearer",
|
|
122
|
+
state: "fresh",
|
|
123
|
+
refreshable: false,
|
|
124
|
+
credentialPath: this.paths.authPath,
|
|
125
|
+
accountId: record.accountId,
|
|
126
|
+
accountEmail: record.accountEmail,
|
|
127
|
+
schemaVersion: record.schemaVersion,
|
|
128
|
+
nowMs: this.now(),
|
|
129
|
+
refreshWindowMs: this.refreshWindowMs,
|
|
130
|
+
metadata: {
|
|
131
|
+
authMode: record.authMode,
|
|
132
|
+
lastRefresh: record.lastRefresh,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return buildCredentialDescriptor({
|
|
138
|
+
provider: "codex",
|
|
139
|
+
credentialKind: "oauth",
|
|
140
|
+
source: "auth-file",
|
|
141
|
+
accessToken: record.accessToken,
|
|
142
|
+
refreshToken: record.refreshToken,
|
|
143
|
+
tokenType: "Bearer",
|
|
144
|
+
expiresAt: record.expiresAt,
|
|
145
|
+
expiresAtMs: record.expiresAtMs,
|
|
146
|
+
refreshable: Boolean(record.refreshToken),
|
|
147
|
+
credentialPath: this.paths.authPath,
|
|
148
|
+
accountId: record.accountId,
|
|
149
|
+
accountEmail: record.accountEmail,
|
|
150
|
+
schemaVersion: record.schemaVersion,
|
|
151
|
+
nowMs: this.now(),
|
|
152
|
+
refreshWindowMs: this.refreshWindowMs,
|
|
153
|
+
metadata: {
|
|
154
|
+
authMode: record.authMode,
|
|
155
|
+
idTokenPresent: Boolean(record.idToken),
|
|
156
|
+
lastRefresh: record.lastRefresh,
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async resolveCredentials() {
|
|
162
|
+
const apiKey = typeof this.env.OPENAI_API_KEY === "string" ? this.env.OPENAI_API_KEY.trim() : "";
|
|
163
|
+
if (apiKey) {
|
|
164
|
+
return buildCredentialDescriptor({
|
|
165
|
+
provider: "codex",
|
|
166
|
+
credentialKind: "api-key",
|
|
167
|
+
source: "env:OPENAI_API_KEY",
|
|
168
|
+
apiKey,
|
|
169
|
+
tokenType: "Bearer",
|
|
170
|
+
state: "fresh",
|
|
171
|
+
refreshable: false,
|
|
172
|
+
credentialPath: "",
|
|
173
|
+
nowMs: this.now(),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let authRecord;
|
|
178
|
+
try {
|
|
179
|
+
authRecord = this.readAuthFile();
|
|
180
|
+
} catch (err) {
|
|
181
|
+
if (err && err.code === "ENOENT") {
|
|
182
|
+
const missing = new Error("Codex auth file not found and OPENAI_API_KEY is unset");
|
|
183
|
+
missing.code = "CODEX_AUTH_UNAVAILABLE";
|
|
184
|
+
throw missing;
|
|
185
|
+
}
|
|
186
|
+
throw err;
|
|
187
|
+
}
|
|
188
|
+
return this.buildResolvedCredential(authRecord);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function resolveCodexUpstreamCredentials(options = {}) {
|
|
193
|
+
const resolver = new CodexUpstreamCredentialResolver(options);
|
|
194
|
+
return resolver.resolveCredentials();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = {
|
|
198
|
+
CodexUpstreamCredentialResolver,
|
|
199
|
+
resolveCodexUpstreamCredentials,
|
|
200
|
+
resolveCodexAuthPaths,
|
|
201
|
+
parseCodexAuthFile,
|
|
202
|
+
decodeJwtPayload,
|
|
203
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function normalizeString(value) {
|
|
4
|
+
return typeof value === "string" ? value.trim() : "";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function normalizeScopes(scopes) {
|
|
8
|
+
if (!Array.isArray(scopes)) return [];
|
|
9
|
+
return scopes
|
|
10
|
+
.map((scope) => normalizeString(scope))
|
|
11
|
+
.filter(Boolean);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseTimestamp(value) {
|
|
15
|
+
const normalized = normalizeString(value);
|
|
16
|
+
if (!normalized) return NaN;
|
|
17
|
+
return Date.parse(normalized);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function classifyCredentialState(expiresAtMs, nowMs, refreshWindowMs) {
|
|
21
|
+
if (!Number.isFinite(expiresAtMs)) return "unknown";
|
|
22
|
+
if (expiresAtMs <= nowMs) return "expired";
|
|
23
|
+
if ((expiresAtMs - nowMs) <= refreshWindowMs) return "near_expiry";
|
|
24
|
+
return "fresh";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function buildCredentialDescriptor(input = {}) {
|
|
28
|
+
const provider = normalizeString(input.provider);
|
|
29
|
+
const credentialKind = normalizeString(input.credentialKind) || "oauth";
|
|
30
|
+
const tokenType = normalizeString(input.tokenType) || "Bearer";
|
|
31
|
+
const expiresAt = normalizeString(input.expiresAt);
|
|
32
|
+
const nowMs = Number.isFinite(input.nowMs) ? input.nowMs : Date.now();
|
|
33
|
+
const refreshWindowMs = Number.isFinite(input.refreshWindowMs) ? input.refreshWindowMs : 0;
|
|
34
|
+
const expiresAtMs = Number.isFinite(input.expiresAtMs) ? input.expiresAtMs : parseTimestamp(expiresAt);
|
|
35
|
+
const explicitState = normalizeString(input.state);
|
|
36
|
+
return {
|
|
37
|
+
provider,
|
|
38
|
+
credentialKind,
|
|
39
|
+
source: normalizeString(input.source),
|
|
40
|
+
tokenType,
|
|
41
|
+
apiKey: normalizeString(input.apiKey),
|
|
42
|
+
accessToken: normalizeString(input.accessToken),
|
|
43
|
+
refreshToken: normalizeString(input.refreshToken),
|
|
44
|
+
expiresAt,
|
|
45
|
+
expiresAtMs,
|
|
46
|
+
state: explicitState || classifyCredentialState(expiresAtMs, nowMs, refreshWindowMs),
|
|
47
|
+
refreshable: input.refreshable === true,
|
|
48
|
+
profile: normalizeString(input.profile),
|
|
49
|
+
credentialPath: normalizeString(input.credentialPath),
|
|
50
|
+
accountId: normalizeString(input.accountId),
|
|
51
|
+
accountEmail: normalizeString(input.accountEmail),
|
|
52
|
+
schemaVersion: normalizeString(input.schemaVersion),
|
|
53
|
+
scopes: normalizeScopes(input.scopes),
|
|
54
|
+
metadata: input.metadata && typeof input.metadata === "object" && !Array.isArray(input.metadata)
|
|
55
|
+
? { ...input.metadata }
|
|
56
|
+
: {},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function toLegacyResolvedAuth(descriptor = {}) {
|
|
61
|
+
const token = normalizeString(descriptor.accessToken) || normalizeString(descriptor.apiKey);
|
|
62
|
+
return {
|
|
63
|
+
source: descriptor.credentialKind === "api-key" ? "api-key" : "oauth",
|
|
64
|
+
token,
|
|
65
|
+
apiKey: normalizeString(descriptor.apiKey),
|
|
66
|
+
accessToken: normalizeString(descriptor.accessToken),
|
|
67
|
+
refreshToken: normalizeString(descriptor.refreshToken),
|
|
68
|
+
tokenType: normalizeString(descriptor.tokenType) || "Bearer",
|
|
69
|
+
state: normalizeString(descriptor.state),
|
|
70
|
+
expiresAt: normalizeString(descriptor.expiresAt),
|
|
71
|
+
schemaVersion: normalizeString(descriptor.schemaVersion),
|
|
72
|
+
profile: normalizeString(descriptor.profile),
|
|
73
|
+
tokenPath: normalizeString(descriptor.credentialPath),
|
|
74
|
+
provider: normalizeString(descriptor.provider),
|
|
75
|
+
credentialKind: normalizeString(descriptor.credentialKind),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildUpstreamAuthFromCredential(descriptor = {}) {
|
|
80
|
+
const tokenType = normalizeString(descriptor.tokenType) || "Bearer";
|
|
81
|
+
const apiKey = normalizeString(descriptor.apiKey);
|
|
82
|
+
const accessToken = normalizeString(descriptor.accessToken);
|
|
83
|
+
if (descriptor.credentialKind === "api-key" && apiKey) {
|
|
84
|
+
return { apiKey };
|
|
85
|
+
}
|
|
86
|
+
if (accessToken) {
|
|
87
|
+
return {
|
|
88
|
+
headers: {
|
|
89
|
+
authorization: `${tokenType} ${accessToken}`,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const err = new Error(`Unsupported upstream credential descriptor for provider ${normalizeString(descriptor.provider) || "unknown"}`);
|
|
94
|
+
err.code = "UPSTREAM_CREDENTIAL_UNSUPPORTED";
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {
|
|
99
|
+
normalizeString,
|
|
100
|
+
normalizeScopes,
|
|
101
|
+
parseTimestamp,
|
|
102
|
+
classifyCredentialState,
|
|
103
|
+
buildCredentialDescriptor,
|
|
104
|
+
toLegacyResolvedAuth,
|
|
105
|
+
buildUpstreamAuthFromCredential,
|
|
106
|
+
};
|