@demicodes/provider-codex 0.3.2 → 0.4.1
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/index.d.mts +36 -3
- package/dist/index.mjs +140 -28
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -2,6 +2,23 @@ import { ModelPolicy, Provider, ProviderAuthState, ProviderCredentials, Provider
|
|
|
2
2
|
import { FileCredentialPool } from "@demicodes/provider/credentials-pool";
|
|
3
3
|
import "@demicodes/core";
|
|
4
4
|
//#region src/auth.d.ts
|
|
5
|
+
type CodexAuthMode = 'apiKey' | 'chatgpt' | 'chatgptAuthTokens' | 'agentIdentity' | 'personalAccessToken' | 'bedrockApiKey';
|
|
6
|
+
interface CodexTokenData {
|
|
7
|
+
id_token?: string | Record<string, unknown>;
|
|
8
|
+
access_token?: string;
|
|
9
|
+
refresh_token?: string;
|
|
10
|
+
account_id?: string | null;
|
|
11
|
+
}
|
|
12
|
+
interface CodexAuthDotJson {
|
|
13
|
+
auth_mode?: CodexAuthMode;
|
|
14
|
+
OPENAI_API_KEY?: string | null;
|
|
15
|
+
tokens?: CodexTokenData | null;
|
|
16
|
+
last_refresh?: string | null;
|
|
17
|
+
agent_identity?: unknown;
|
|
18
|
+
personal_access_token?: string | null;
|
|
19
|
+
bedrock_api_key?: unknown;
|
|
20
|
+
[key: string]: unknown;
|
|
21
|
+
}
|
|
5
22
|
type CodexResolvedAuth = {
|
|
6
23
|
kind: 'chatgpt';
|
|
7
24
|
mode: 'chatgpt' | 'chatgptAuthTokens';
|
|
@@ -142,13 +159,29 @@ declare class PoolAwareCodexAuthStore implements CodexAuthStore {
|
|
|
142
159
|
}
|
|
143
160
|
declare function createCodexCredentials(pool: FileCredentialPool, authStore: CodexAuthStore, options?: {
|
|
144
161
|
codexHome?: string;
|
|
145
|
-
loginCommand?: string;
|
|
146
|
-
loginArgs?: string[];
|
|
147
162
|
quota?: ProviderQuota | null;
|
|
148
163
|
onActiveChange?: () => void;
|
|
164
|
+
/** Injectable fetch for the device-code login flow (tests). */
|
|
165
|
+
loginFetch?: typeof fetch;
|
|
149
166
|
}): ProviderCredentials;
|
|
150
167
|
declare function openCodexCredentialPool(options?: {
|
|
151
168
|
stateDir?: string;
|
|
152
169
|
}): FileCredentialPool;
|
|
153
170
|
//#endregion
|
|
154
|
-
|
|
171
|
+
//#region src/device-login.d.ts
|
|
172
|
+
interface CodexDeviceLoginPending {
|
|
173
|
+
verificationUrl: string;
|
|
174
|
+
userCode: string;
|
|
175
|
+
expiresAt: string;
|
|
176
|
+
}
|
|
177
|
+
interface CodexDeviceLoginOptions {
|
|
178
|
+
signal?: AbortSignal;
|
|
179
|
+
/** Fires once with the URL + one-time code the user needs. */
|
|
180
|
+
onPending?: (pending: CodexDeviceLoginPending) => void;
|
|
181
|
+
fetch?: typeof fetch;
|
|
182
|
+
issuer?: string;
|
|
183
|
+
}
|
|
184
|
+
/** Runs the full device-code flow and returns vendor-shaped auth material. */
|
|
185
|
+
declare function runCodexDeviceLogin(options?: CodexDeviceLoginOptions): Promise<CodexAuthDotJson>;
|
|
186
|
+
//#endregion
|
|
187
|
+
export { type CodexDeviceLoginOptions, type CodexDeviceLoginPending, type CodexModelCatalogOptions, type CodexProviderOptions, type CodexQuotaOptions, type CodexResolvedAuth, type CodexTransportMode, type FileCodexAuthStoreOptions, PoolAwareCodexAuthStore, codexAuthStatus, createCodexCredentials, createCodexProvider, createCodexQuota, listCodexModels, mapCodexRateLimitHeaders, openCodexCredentialPool, runCodexDeviceLogin };
|
package/dist/index.mjs
CHANGED
|
@@ -3,8 +3,8 @@ import { Buffer } from "node:buffer";
|
|
|
3
3
|
import { chmod, mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
|
-
import { applyModelPolicy, clampPromptCacheKey, clampUsedPercent, createProviderQuota, defineProvider, httpErrorCode, numberHeader, redactCredentialText, severityFromUsedPercent, unixSecondsToIso } from "@demicodes/provider";
|
|
7
|
-
import { FileCredentialPool, credentialIdFromIdentity
|
|
6
|
+
import { applyModelPolicy, clampPromptCacheKey, clampUsedPercent, createProviderQuota, defineProvider, httpErrorCode, normalizeErrorCode, numberHeader, redactCredentialText, severityFromUsedPercent, unixSecondsToIso } from "@demicodes/provider";
|
|
7
|
+
import { FileCredentialPool, credentialIdFromIdentity } from "@demicodes/provider/credentials-pool";
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
9
9
|
//#region src/auth.ts
|
|
10
10
|
async function codexAuthStatus(options = {}) {
|
|
@@ -15,8 +15,12 @@ const SECRET_FIELD_PATTERNS = ["OPENAI_API_KEY"];
|
|
|
15
15
|
function redactCodexSecretText(text) {
|
|
16
16
|
return redactCredentialText(text, SECRET_FIELD_PATTERNS);
|
|
17
17
|
}
|
|
18
|
-
const
|
|
18
|
+
const CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
19
19
|
const TOKEN_REFRESH_URL = "https://auth.openai.com/oauth/token";
|
|
20
|
+
/** OAuth client id shared by token refresh and device-code login. */
|
|
21
|
+
function codexOauthClientId() {
|
|
22
|
+
return process.env.CODEX_APP_SERVER_LOGIN_CLIENT_ID || CODEX_OAUTH_CLIENT_ID;
|
|
23
|
+
}
|
|
20
24
|
const REFRESH_EXPIRY_SKEW_MS = 300 * 1e3;
|
|
21
25
|
const REFRESH_STALENESS_MS = 11520 * 60 * 1e3;
|
|
22
26
|
var FileCodexAuthStore = class {
|
|
@@ -218,7 +222,7 @@ async function refreshCodexToken(refreshToken, signal) {
|
|
|
218
222
|
method: "POST",
|
|
219
223
|
headers: { "content-type": "application/json" },
|
|
220
224
|
body: JSON.stringify({
|
|
221
|
-
client_id:
|
|
225
|
+
client_id: codexOauthClientId(),
|
|
222
226
|
grant_type: "refresh_token",
|
|
223
227
|
refresh_token: refreshToken
|
|
224
228
|
}),
|
|
@@ -501,6 +505,113 @@ function defaultModelCatalogUserAgent() {
|
|
|
501
505
|
return `demi-provider-codex/0.0.0`;
|
|
502
506
|
}
|
|
503
507
|
//#endregion
|
|
508
|
+
//#region src/device-login.ts
|
|
509
|
+
const DEVICE_LOGIN_ISSUER = "https://auth.openai.com";
|
|
510
|
+
const DEVICE_LOGIN_MAX_WAIT_MS = 900 * 1e3;
|
|
511
|
+
const DEVICE_LOGIN_FALLBACK_INTERVAL_S = 5;
|
|
512
|
+
async function postJson(fetchImpl, url, body, signal) {
|
|
513
|
+
return fetchImpl(url, {
|
|
514
|
+
method: "POST",
|
|
515
|
+
headers: { "content-type": "application/json" },
|
|
516
|
+
body: JSON.stringify(body),
|
|
517
|
+
signal
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
async function jsonBody(response, what) {
|
|
521
|
+
const body = await response.json().catch(() => null);
|
|
522
|
+
if (!isRecord(body)) throw new CodexAuthError("auth_login_failed", `${what} response is not a JSON object`);
|
|
523
|
+
return body;
|
|
524
|
+
}
|
|
525
|
+
async function requestUserCode(fetchImpl, issuer, clientId, signal) {
|
|
526
|
+
const response = await postJson(fetchImpl, `${issuer}/api/accounts/deviceauth/usercode`, { client_id: clientId }, signal);
|
|
527
|
+
if (response.status === 404) throw new CodexAuthError("auth_unsupported", "Device-code login is not enabled for this Codex account");
|
|
528
|
+
if (!response.ok) throw new CodexAuthError("auth_login_failed", `Device code request failed with HTTP ${response.status}`);
|
|
529
|
+
const body = await jsonBody(response, "Device code");
|
|
530
|
+
const deviceAuthId = nonEmptyString(body.device_auth_id);
|
|
531
|
+
const userCode = nonEmptyString(body.user_code) ?? nonEmptyString(body.usercode);
|
|
532
|
+
if (!deviceAuthId || !userCode) throw new CodexAuthError("auth_login_failed", "Device code response is missing device_auth_id or user_code");
|
|
533
|
+
const interval = Number(typeof body.interval === "string" ? body.interval.trim() : body.interval);
|
|
534
|
+
return {
|
|
535
|
+
deviceAuthId,
|
|
536
|
+
userCode,
|
|
537
|
+
intervalSeconds: Number.isFinite(interval) && interval >= 0 ? interval : DEVICE_LOGIN_FALLBACK_INTERVAL_S
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
async function pollForAuthorization(fetchImpl, issuer, userCode, startedAt, signal) {
|
|
541
|
+
for (;;) {
|
|
542
|
+
signal?.throwIfAborted();
|
|
543
|
+
const response = await postJson(fetchImpl, `${issuer}/api/accounts/deviceauth/token`, {
|
|
544
|
+
device_auth_id: userCode.deviceAuthId,
|
|
545
|
+
user_code: userCode.userCode
|
|
546
|
+
}, signal);
|
|
547
|
+
if (response.ok) {
|
|
548
|
+
const body = await jsonBody(response, "Device authorization");
|
|
549
|
+
const authorizationCode = nonEmptyString(body.authorization_code);
|
|
550
|
+
const codeVerifier = nonEmptyString(body.code_verifier);
|
|
551
|
+
if (!authorizationCode || !codeVerifier) throw new CodexAuthError("auth_login_failed", "Device authorization response is missing authorization_code or code_verifier");
|
|
552
|
+
return {
|
|
553
|
+
authorizationCode,
|
|
554
|
+
codeVerifier
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
if (response.status !== 403 && response.status !== 404) throw new CodexAuthError("auth_login_failed", `Device authorization failed with HTTP ${response.status}`);
|
|
558
|
+
if (Date.now() - startedAt >= DEVICE_LOGIN_MAX_WAIT_MS) throw new CodexAuthError("auth_login_failed", "Device-code login timed out after 15 minutes");
|
|
559
|
+
await delay(userCode.intervalSeconds * 1e3);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
async function exchangeAuthorizationCode(fetchImpl, issuer, clientId, authorization, signal) {
|
|
563
|
+
const params = new URLSearchParams({
|
|
564
|
+
grant_type: "authorization_code",
|
|
565
|
+
code: authorization.authorizationCode,
|
|
566
|
+
redirect_uri: `${issuer}/deviceauth/callback`,
|
|
567
|
+
client_id: clientId,
|
|
568
|
+
code_verifier: authorization.codeVerifier
|
|
569
|
+
});
|
|
570
|
+
const response = await fetchImpl(`${issuer}/oauth/token`, {
|
|
571
|
+
method: "POST",
|
|
572
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
573
|
+
body: params.toString(),
|
|
574
|
+
signal
|
|
575
|
+
});
|
|
576
|
+
if (!response.ok) throw new CodexAuthError("auth_login_failed", `Device-code token exchange failed with HTTP ${response.status}`);
|
|
577
|
+
const body = await jsonBody(response, "Token exchange");
|
|
578
|
+
const idToken = nonEmptyString(body.id_token);
|
|
579
|
+
const accessToken = nonEmptyString(body.access_token);
|
|
580
|
+
const refreshToken = nonEmptyString(body.refresh_token);
|
|
581
|
+
if (!idToken || !accessToken || !refreshToken) throw new CodexAuthError("auth_login_failed", "Token exchange response is missing id_token, access_token, or refresh_token");
|
|
582
|
+
return {
|
|
583
|
+
idToken,
|
|
584
|
+
accessToken,
|
|
585
|
+
refreshToken
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
/** Runs the full device-code flow and returns vendor-shaped auth material. */
|
|
589
|
+
async function runCodexDeviceLogin(options = {}) {
|
|
590
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
591
|
+
const issuer = (options.issuer ?? DEVICE_LOGIN_ISSUER).replace(/\/+$/, "");
|
|
592
|
+
const clientId = codexOauthClientId();
|
|
593
|
+
const startedAt = Date.now();
|
|
594
|
+
const userCode = await requestUserCode(fetchImpl, issuer, clientId, options.signal);
|
|
595
|
+
options.onPending?.({
|
|
596
|
+
verificationUrl: `${issuer}/codex/device`,
|
|
597
|
+
userCode: userCode.userCode,
|
|
598
|
+
expiresAt: new Date(startedAt + DEVICE_LOGIN_MAX_WAIT_MS).toISOString()
|
|
599
|
+
});
|
|
600
|
+
const tokens = await exchangeAuthorizationCode(fetchImpl, issuer, clientId, await pollForAuthorization(fetchImpl, issuer, userCode, startedAt, options.signal), options.signal);
|
|
601
|
+
const accountId = parseChatGptClaims(tokens.accessToken).accountId ?? parseIdTokenClaims(tokens.idToken).accountId;
|
|
602
|
+
return {
|
|
603
|
+
auth_mode: "chatgpt",
|
|
604
|
+
OPENAI_API_KEY: null,
|
|
605
|
+
tokens: {
|
|
606
|
+
id_token: tokens.idToken,
|
|
607
|
+
access_token: tokens.accessToken,
|
|
608
|
+
refresh_token: tokens.refreshToken,
|
|
609
|
+
account_id: accountId
|
|
610
|
+
},
|
|
611
|
+
last_refresh: (/* @__PURE__ */ new Date()).toISOString()
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
//#endregion
|
|
504
615
|
//#region src/credentials.ts
|
|
505
616
|
/**
|
|
506
617
|
* Auth store that prefers the demi pool active entry; falls back to vendor ~/.codex.
|
|
@@ -536,8 +647,6 @@ var PoolAwareCodexAuthStore = class {
|
|
|
536
647
|
};
|
|
537
648
|
function createCodexCredentials(pool, authStore, options = {}) {
|
|
538
649
|
const vendorHome = options.codexHome ?? defaultCodexHome();
|
|
539
|
-
const loginCommand = options.loginCommand ?? "codex";
|
|
540
|
-
const loginArgs = options.loginArgs ?? ["login"];
|
|
541
650
|
const capability = () => ({
|
|
542
651
|
mode: "supported",
|
|
543
652
|
canBeginLogin: true,
|
|
@@ -593,17 +702,27 @@ function createCodexCredentials(pool, authStore, options = {}) {
|
|
|
593
702
|
getActive,
|
|
594
703
|
setActive,
|
|
595
704
|
beginLogin: async (loginOptions) => {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
}
|
|
705
|
+
try {
|
|
706
|
+
const auth = await runCodexDeviceLogin({
|
|
707
|
+
signal: loginOptions?.signal,
|
|
708
|
+
onPending: loginOptions?.onPending,
|
|
709
|
+
fetch: options.loginFetch
|
|
710
|
+
});
|
|
711
|
+
return {
|
|
712
|
+
status: "completed",
|
|
713
|
+
credentialId: (await importFromAuthJson(`${JSON.stringify(auth, null, 2)}\n`, "login:device")).id
|
|
714
|
+
};
|
|
715
|
+
} catch (error) {
|
|
716
|
+
if (loginOptions?.signal?.aborted) return { status: "cancelled" };
|
|
717
|
+
if (error instanceof CodexAuthError && error.code === "auth_unsupported") return {
|
|
718
|
+
status: "unavailable",
|
|
719
|
+
message: error.message
|
|
720
|
+
};
|
|
721
|
+
return {
|
|
722
|
+
status: "failed",
|
|
723
|
+
message: errorMessage(error)
|
|
724
|
+
};
|
|
725
|
+
}
|
|
607
726
|
},
|
|
608
727
|
importDefault: async () => {
|
|
609
728
|
const authFile = join(vendorHome, "auth.json");
|
|
@@ -1081,14 +1200,6 @@ function errorEventFromFailedResponse(response) {
|
|
|
1081
1200
|
code: normalizeErrorCode(stringOrNull(error?.code) ?? stringOrNull(error?.type), message)
|
|
1082
1201
|
};
|
|
1083
1202
|
}
|
|
1084
|
-
function normalizeErrorCode(code, message) {
|
|
1085
|
-
const value = `${code ?? ""} ${message}`.toLowerCase();
|
|
1086
|
-
if (/context|maximum context|too long|max.*token/.test(value)) return "context_length_exceeded";
|
|
1087
|
-
if (/rate|quota|usage|limit|billing|balance/.test(value)) return "rate_limit";
|
|
1088
|
-
if (/unauth|auth|expired|invalid.*token/.test(value)) return "auth_expired";
|
|
1089
|
-
if (/overload|unavailable|timeout/.test(value)) return "overloaded";
|
|
1090
|
-
return code;
|
|
1091
|
-
}
|
|
1092
1203
|
function incompleteReason(response) {
|
|
1093
1204
|
if (isRecord(response) && isRecord(response.incomplete_details) && typeof response.incomplete_details.reason === "string") return response.incomplete_details.reason;
|
|
1094
1205
|
return "unknown";
|
|
@@ -1582,10 +1693,11 @@ function providerErrorFromUnknown(error) {
|
|
|
1582
1693
|
message: redactCodexSecretText(error.message),
|
|
1583
1694
|
code: httpErrorCode(error.status, error.message)
|
|
1584
1695
|
};
|
|
1696
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1585
1697
|
return {
|
|
1586
1698
|
type: "error",
|
|
1587
|
-
message: redactCodexSecretText(
|
|
1588
|
-
code: null
|
|
1699
|
+
message: redactCodexSecretText(message),
|
|
1700
|
+
code: normalizeErrorCode(null, message)
|
|
1589
1701
|
};
|
|
1590
1702
|
}
|
|
1591
1703
|
function isRetryableHttpStatus(status) {
|
|
@@ -1605,4 +1717,4 @@ function sleep(ms, signal) {
|
|
|
1605
1717
|
});
|
|
1606
1718
|
}
|
|
1607
1719
|
//#endregion
|
|
1608
|
-
export { PoolAwareCodexAuthStore, codexAuthStatus, createCodexCredentials, createCodexProvider, createCodexQuota, listCodexModels, mapCodexRateLimitHeaders, openCodexCredentialPool };
|
|
1720
|
+
export { PoolAwareCodexAuthStore, codexAuthStatus, createCodexCredentials, createCodexProvider, createCodexQuota, listCodexModels, mapCodexRateLimitHeaders, openCodexCredentialPool, runCodexDeviceLogin };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@demicodes/provider-codex",
|
|
3
3
|
"description": "Codex provider adapter for Demi.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.1",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@demicodes/core": "^0.3.2",
|
|
15
|
-
"@demicodes/provider": "^0.
|
|
15
|
+
"@demicodes/provider": "^0.4.2",
|
|
16
16
|
"@demicodes/utils": "^0.3.2"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"@demicodes/agent": "^0.3.
|
|
19
|
+
"@demicodes/agent": "^0.3.3",
|
|
20
20
|
"@demicodes/shell": "^0.3.2"
|
|
21
21
|
},
|
|
22
22
|
"license": "Apache-2.0",
|