@agentionai/agents 1.11.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/Agent.d.ts +1 -1
- package/dist/agents/AgentConfig.d.ts +23 -0
- package/dist/agents/AgentEvent.d.ts +5 -0
- package/dist/agents/AgentEvent.js +5 -0
- package/dist/agents/BaseAgent.d.ts +107 -1
- package/dist/agents/BaseAgent.js +56 -0
- package/dist/agents/anthropic/ClaudeAgent.js +157 -117
- package/dist/agents/errors/AgentError.d.ts +12 -0
- package/dist/agents/openai/CodexAgent.d.ts +135 -0
- package/dist/agents/openai/CodexAgent.js +179 -0
- package/dist/agents/openai/OpenAiAgent.d.ts +89 -6
- package/dist/agents/openai/OpenAiAgent.js +305 -75
- package/dist/agents/openai/codex-auth.d.ts +166 -0
- package/dist/agents/openai/codex-auth.js +264 -0
- package/dist/agents/openai-compatible/OpenAICompatibleAgent.js +140 -107
- package/dist/agents/openrouter/OpenRouterAgent.js +128 -95
- package/dist/agents/reasoning-text.d.ts +44 -0
- package/dist/agents/reasoning-text.js +43 -0
- package/dist/core.d.ts +1 -0
- package/dist/core.js +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +13 -1
- package/dist/openai.d.ts +4 -1
- package/dist/openai.js +16 -1
- package/package.json +3 -3
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth against a ChatGPT subscription, as used by OpenAI's Codex CLI.
|
|
3
|
+
*
|
|
4
|
+
* This is a different product surface from the platform API: the credentials are
|
|
5
|
+
* a ChatGPT login rather than a `sk-...` platform key, and requests are billed
|
|
6
|
+
* against the subscription instead of an API account. The endpoint differs too —
|
|
7
|
+
* see {@link CODEX_BASE_URL}.
|
|
8
|
+
*
|
|
9
|
+
* None of it is a documented public API. The values here were cross-checked
|
|
10
|
+
* against the Codex CLI's own behaviour and several independent
|
|
11
|
+
* reimplementations, but OpenAI can change them without notice.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Base URL for the ChatGPT-backed Codex Responses API.
|
|
15
|
+
*
|
|
16
|
+
* The SDK appends `/responses`, giving
|
|
17
|
+
* `https://chatgpt.com/backend-api/codex/responses`.
|
|
18
|
+
*/
|
|
19
|
+
export declare const CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex";
|
|
20
|
+
/** Public OAuth client id the Codex CLI uses. Not a secret. */
|
|
21
|
+
export declare const CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
22
|
+
/** Token endpoint used to exchange a refresh token for a fresh access token. */
|
|
23
|
+
export declare const CODEX_TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
24
|
+
/**
|
|
25
|
+
* Default `originator` header value.
|
|
26
|
+
*
|
|
27
|
+
* OpenAI gates parts of the model catalog on this, so an unrecognised value can
|
|
28
|
+
* quietly change which models an account may reach.
|
|
29
|
+
*/
|
|
30
|
+
export declare const CODEX_ORIGINATOR = "codex_cli_rs";
|
|
31
|
+
/**
|
|
32
|
+
* `client_version` for the Codex models endpoint, which 400s without one.
|
|
33
|
+
*
|
|
34
|
+
* Each model also carries a `minimal_client_version`; the backend hides models
|
|
35
|
+
* newer than the version claimed here, so an old value quietly shortens the
|
|
36
|
+
* list rather than erroring.
|
|
37
|
+
*/
|
|
38
|
+
export declare const CODEX_CLIENT_VERSION = "0.153.4";
|
|
39
|
+
/**
|
|
40
|
+
* One entry from the Codex `/models` response.
|
|
41
|
+
*
|
|
42
|
+
* Nothing like the platform API's `/v1/models` — richer, and shaped for the
|
|
43
|
+
* Codex client. Only the fields worth relying on are named; the rest come
|
|
44
|
+
* through on `ModelInfo.raw`.
|
|
45
|
+
*/
|
|
46
|
+
export interface CodexModelCard {
|
|
47
|
+
slug: string;
|
|
48
|
+
display_name?: string;
|
|
49
|
+
description?: string;
|
|
50
|
+
/** Default context window for this account's plan. */
|
|
51
|
+
context_window?: number;
|
|
52
|
+
/** Largest window the model can be driven at. */
|
|
53
|
+
max_context_window?: number;
|
|
54
|
+
input_modalities?: string[];
|
|
55
|
+
supported_reasoning_levels?: {
|
|
56
|
+
effort: string;
|
|
57
|
+
description?: string;
|
|
58
|
+
}[];
|
|
59
|
+
default_reasoning_level?: string;
|
|
60
|
+
/** Subscription plans that may select this model. */
|
|
61
|
+
available_in_plans?: string[];
|
|
62
|
+
/** `"list"` for models meant to be shown in a picker. */
|
|
63
|
+
visibility?: string;
|
|
64
|
+
supported_in_api?: boolean;
|
|
65
|
+
minimal_client_version?: string;
|
|
66
|
+
supports_parallel_tool_calls?: boolean;
|
|
67
|
+
[key: string]: unknown;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Credentials for the ChatGPT/Codex backend.
|
|
71
|
+
*/
|
|
72
|
+
export interface CodexCredentials {
|
|
73
|
+
/** Bearer token sent as `Authorization`. */
|
|
74
|
+
accessToken: string;
|
|
75
|
+
/** Used to mint a new access token once the current one expires. */
|
|
76
|
+
refreshToken?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Workspace/account the request is billed to, sent as the
|
|
79
|
+
* `chatgpt-account-id` header. Read from `auth.json`, or decoded from the
|
|
80
|
+
* `id_token` when absent.
|
|
81
|
+
*/
|
|
82
|
+
accountId?: string;
|
|
83
|
+
/** Account e-mail, when the `id_token` carried one. Informational. */
|
|
84
|
+
email?: string;
|
|
85
|
+
/** Subscription tier (`plus`, `pro`, …), when present. Informational. */
|
|
86
|
+
planType?: string;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Decode a JWT's payload without verifying its signature.
|
|
90
|
+
*
|
|
91
|
+
* Verification is the token endpoint's job — we are only reading claims out of a
|
|
92
|
+
* token we were just handed over TLS, never making a trust decision on it.
|
|
93
|
+
* Returns `undefined` for anything that does not parse, so a malformed or
|
|
94
|
+
* opaque token degrades to "no claims" rather than throwing.
|
|
95
|
+
*/
|
|
96
|
+
export declare function decodeJwtClaims<T = Record<string, unknown>>(token: string): T | undefined;
|
|
97
|
+
/**
|
|
98
|
+
* Seconds-since-epoch expiry of a JWT, or `undefined` if it has no `exp`.
|
|
99
|
+
*/
|
|
100
|
+
export declare function jwtExpiry(token: string): number | undefined;
|
|
101
|
+
/** Default location of Codex's credential file. */
|
|
102
|
+
export declare function codexAuthFilePath(codexHome?: string): string;
|
|
103
|
+
/**
|
|
104
|
+
* Read the credentials the Codex CLI stored at `$CODEX_HOME/auth.json`
|
|
105
|
+
* (`~/.codex/auth.json` by default).
|
|
106
|
+
*
|
|
107
|
+
* Sign in with `codex login` first — this only reads what that wrote, it does
|
|
108
|
+
* not run the OAuth flow itself.
|
|
109
|
+
*
|
|
110
|
+
* @throws if the file is missing, unreadable, not JSON, or holds no access token.
|
|
111
|
+
*/
|
|
112
|
+
export declare function loadCodexCredentials(codexHome?: string): Promise<CodexCredentials>;
|
|
113
|
+
/**
|
|
114
|
+
* Exchange a refresh token for a fresh access token.
|
|
115
|
+
*
|
|
116
|
+
* The returned credentials carry the new `refresh_token` when the server
|
|
117
|
+
* rotated it, and the previous one otherwise.
|
|
118
|
+
*/
|
|
119
|
+
export declare function refreshCodexCredentials(refreshToken: string, options?: {
|
|
120
|
+
clientId?: string;
|
|
121
|
+
tokenUrl?: string;
|
|
122
|
+
signal?: AbortSignal;
|
|
123
|
+
}): Promise<CodexCredentials>;
|
|
124
|
+
/** Options for {@link createCodexTokenProvider}. */
|
|
125
|
+
export interface CodexTokenProviderOptions {
|
|
126
|
+
/** OAuth client id. Defaults to {@link CODEX_CLIENT_ID}. */
|
|
127
|
+
clientId?: string;
|
|
128
|
+
/** Token endpoint. Defaults to {@link CODEX_TOKEN_URL}. */
|
|
129
|
+
tokenUrl?: string;
|
|
130
|
+
/**
|
|
131
|
+
* Refresh this many seconds before the access token actually expires, so a
|
|
132
|
+
* request is never sent with a token that dies in flight.
|
|
133
|
+
*
|
|
134
|
+
* @default 300
|
|
135
|
+
*/
|
|
136
|
+
refreshSkewSeconds?: number;
|
|
137
|
+
/**
|
|
138
|
+
* Called after every successful refresh, e.g. to persist the rotated refresh
|
|
139
|
+
* token. Errors thrown here are ignored — a failed write must not fail the
|
|
140
|
+
* request the token was minted for.
|
|
141
|
+
*/
|
|
142
|
+
onRefresh?: (credentials: CodexCredentials) => void | Promise<void>;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* A token source that hands out an access token and silently refreshes it.
|
|
146
|
+
*
|
|
147
|
+
* The `getToken` function is shaped for the OpenAI SDK's `apiKey` option, which
|
|
148
|
+
* accepts an async function and calls it before *every* request — so a
|
|
149
|
+
* long-running agent keeps working past the ~1h life of an access token without
|
|
150
|
+
* anyone reaching for the credential file again.
|
|
151
|
+
*/
|
|
152
|
+
export interface CodexTokenProvider {
|
|
153
|
+
/** Current access token, refreshed on demand. Pass as the SDK's `apiKey`. */
|
|
154
|
+
getToken: () => Promise<string>;
|
|
155
|
+
/** Latest known credentials, including `accountId`. */
|
|
156
|
+
current: () => CodexCredentials;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Wrap credentials in a self-refreshing token provider.
|
|
160
|
+
*
|
|
161
|
+
* Refreshes lazily — only when a token is actually asked for and the current
|
|
162
|
+
* one is within `refreshSkewSeconds` of expiry. Concurrent callers share a
|
|
163
|
+
* single in-flight refresh rather than each starting their own.
|
|
164
|
+
*/
|
|
165
|
+
export declare function createCodexTokenProvider(credentials: CodexCredentials, options?: CodexTokenProviderOptions): CodexTokenProvider;
|
|
166
|
+
//# sourceMappingURL=codex-auth.d.ts.map
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CODEX_CLIENT_VERSION = exports.CODEX_ORIGINATOR = exports.CODEX_TOKEN_URL = exports.CODEX_CLIENT_ID = exports.CODEX_BASE_URL = void 0;
|
|
37
|
+
exports.decodeJwtClaims = decodeJwtClaims;
|
|
38
|
+
exports.jwtExpiry = jwtExpiry;
|
|
39
|
+
exports.codexAuthFilePath = codexAuthFilePath;
|
|
40
|
+
exports.loadCodexCredentials = loadCodexCredentials;
|
|
41
|
+
exports.refreshCodexCredentials = refreshCodexCredentials;
|
|
42
|
+
exports.createCodexTokenProvider = createCodexTokenProvider;
|
|
43
|
+
const fs_1 = require("fs");
|
|
44
|
+
const os = __importStar(require("os"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
/**
|
|
47
|
+
* OAuth against a ChatGPT subscription, as used by OpenAI's Codex CLI.
|
|
48
|
+
*
|
|
49
|
+
* This is a different product surface from the platform API: the credentials are
|
|
50
|
+
* a ChatGPT login rather than a `sk-...` platform key, and requests are billed
|
|
51
|
+
* against the subscription instead of an API account. The endpoint differs too —
|
|
52
|
+
* see {@link CODEX_BASE_URL}.
|
|
53
|
+
*
|
|
54
|
+
* None of it is a documented public API. The values here were cross-checked
|
|
55
|
+
* against the Codex CLI's own behaviour and several independent
|
|
56
|
+
* reimplementations, but OpenAI can change them without notice.
|
|
57
|
+
*/
|
|
58
|
+
/**
|
|
59
|
+
* Base URL for the ChatGPT-backed Codex Responses API.
|
|
60
|
+
*
|
|
61
|
+
* The SDK appends `/responses`, giving
|
|
62
|
+
* `https://chatgpt.com/backend-api/codex/responses`.
|
|
63
|
+
*/
|
|
64
|
+
exports.CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex";
|
|
65
|
+
/** Public OAuth client id the Codex CLI uses. Not a secret. */
|
|
66
|
+
exports.CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
67
|
+
/** Token endpoint used to exchange a refresh token for a fresh access token. */
|
|
68
|
+
exports.CODEX_TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
69
|
+
/**
|
|
70
|
+
* Default `originator` header value.
|
|
71
|
+
*
|
|
72
|
+
* OpenAI gates parts of the model catalog on this, so an unrecognised value can
|
|
73
|
+
* quietly change which models an account may reach.
|
|
74
|
+
*/
|
|
75
|
+
exports.CODEX_ORIGINATOR = "codex_cli_rs";
|
|
76
|
+
/**
|
|
77
|
+
* `client_version` for the Codex models endpoint, which 400s without one.
|
|
78
|
+
*
|
|
79
|
+
* Each model also carries a `minimal_client_version`; the backend hides models
|
|
80
|
+
* newer than the version claimed here, so an old value quietly shortens the
|
|
81
|
+
* list rather than erroring.
|
|
82
|
+
*/
|
|
83
|
+
exports.CODEX_CLIENT_VERSION = "0.153.4";
|
|
84
|
+
/**
|
|
85
|
+
* Decode a JWT's payload without verifying its signature.
|
|
86
|
+
*
|
|
87
|
+
* Verification is the token endpoint's job — we are only reading claims out of a
|
|
88
|
+
* token we were just handed over TLS, never making a trust decision on it.
|
|
89
|
+
* Returns `undefined` for anything that does not parse, so a malformed or
|
|
90
|
+
* opaque token degrades to "no claims" rather than throwing.
|
|
91
|
+
*/
|
|
92
|
+
function decodeJwtClaims(token) {
|
|
93
|
+
const payload = token.split(".")[1];
|
|
94
|
+
if (!payload)
|
|
95
|
+
return undefined;
|
|
96
|
+
try {
|
|
97
|
+
const json = Buffer.from(payload, "base64url").toString("utf8");
|
|
98
|
+
const claims = JSON.parse(json);
|
|
99
|
+
return typeof claims === "object" && claims !== null
|
|
100
|
+
? claims
|
|
101
|
+
: undefined;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Seconds-since-epoch expiry of a JWT, or `undefined` if it has no `exp`.
|
|
109
|
+
*/
|
|
110
|
+
function jwtExpiry(token) {
|
|
111
|
+
const exp = decodeJwtClaims(token)?.exp;
|
|
112
|
+
return typeof exp === "number" ? exp : undefined;
|
|
113
|
+
}
|
|
114
|
+
/** Default location of Codex's credential file. */
|
|
115
|
+
function codexAuthFilePath(codexHome) {
|
|
116
|
+
const home = codexHome ?? process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
|
|
117
|
+
return path.join(home, "auth.json");
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Read the credentials the Codex CLI stored at `$CODEX_HOME/auth.json`
|
|
121
|
+
* (`~/.codex/auth.json` by default).
|
|
122
|
+
*
|
|
123
|
+
* Sign in with `codex login` first — this only reads what that wrote, it does
|
|
124
|
+
* not run the OAuth flow itself.
|
|
125
|
+
*
|
|
126
|
+
* @throws if the file is missing, unreadable, not JSON, or holds no access token.
|
|
127
|
+
*/
|
|
128
|
+
async function loadCodexCredentials(codexHome) {
|
|
129
|
+
const file = codexAuthFilePath(codexHome);
|
|
130
|
+
let raw;
|
|
131
|
+
try {
|
|
132
|
+
raw = await fs_1.promises.readFile(file, "utf8");
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
const reason = error?.code === "ENOENT"
|
|
136
|
+
? "no such file — run `codex login` to sign in with your ChatGPT account"
|
|
137
|
+
: error instanceof Error
|
|
138
|
+
? error.message
|
|
139
|
+
: "unknown error";
|
|
140
|
+
throw new Error(`Could not read Codex credentials from ${file}: ${reason}`);
|
|
141
|
+
}
|
|
142
|
+
let parsed;
|
|
143
|
+
try {
|
|
144
|
+
parsed = JSON.parse(raw);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
throw new Error(`Codex credentials at ${file} are not valid JSON`);
|
|
148
|
+
}
|
|
149
|
+
const tokens = parsed.tokens;
|
|
150
|
+
if (!tokens?.access_token) {
|
|
151
|
+
throw new Error(`Codex credentials at ${file} contain no OAuth access token` +
|
|
152
|
+
(parsed.OPENAI_API_KEY
|
|
153
|
+
? " — that file holds a platform API key instead, which belongs in `apiKey` with the default `authType: \"apiKey\"`"
|
|
154
|
+
: " — run `codex login` to sign in with your ChatGPT account"));
|
|
155
|
+
}
|
|
156
|
+
return credentialsFromTokens(tokens);
|
|
157
|
+
}
|
|
158
|
+
/** Build {@link CodexCredentials} from an `auth.json` `tokens` object. */
|
|
159
|
+
function credentialsFromTokens(tokens) {
|
|
160
|
+
const claims = tokens.id_token
|
|
161
|
+
? decodeJwtClaims(tokens.id_token)
|
|
162
|
+
: undefined;
|
|
163
|
+
const auth = claims?.["https://api.openai.com/auth"];
|
|
164
|
+
return {
|
|
165
|
+
accessToken: tokens.access_token,
|
|
166
|
+
refreshToken: tokens.refresh_token,
|
|
167
|
+
// `auth.json` usually carries `account_id`, but not always; the same value
|
|
168
|
+
// is a claim on the id_token, so fall back to that before giving up.
|
|
169
|
+
accountId: tokens.account_id ?? auth?.chatgpt_account_id,
|
|
170
|
+
email: claims?.email ?? claims?.["https://api.openai.com/profile"]?.email,
|
|
171
|
+
planType: auth?.chatgpt_plan_type,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Exchange a refresh token for a fresh access token.
|
|
176
|
+
*
|
|
177
|
+
* The returned credentials carry the new `refresh_token` when the server
|
|
178
|
+
* rotated it, and the previous one otherwise.
|
|
179
|
+
*/
|
|
180
|
+
async function refreshCodexCredentials(refreshToken, options = {}) {
|
|
181
|
+
const res = await fetch(options.tokenUrl ?? exports.CODEX_TOKEN_URL, {
|
|
182
|
+
method: "POST",
|
|
183
|
+
headers: { "Content-Type": "application/json" },
|
|
184
|
+
body: JSON.stringify({
|
|
185
|
+
grant_type: "refresh_token",
|
|
186
|
+
refresh_token: refreshToken,
|
|
187
|
+
client_id: options.clientId ?? exports.CODEX_CLIENT_ID,
|
|
188
|
+
}),
|
|
189
|
+
signal: options.signal,
|
|
190
|
+
});
|
|
191
|
+
if (!res.ok) {
|
|
192
|
+
const body = await res.text().catch(() => "");
|
|
193
|
+
throw new Error(`Codex token refresh failed (${res.status} ${res.statusText})${body ? `: ${body.slice(0, 500)}` : ""}`);
|
|
194
|
+
}
|
|
195
|
+
const data = (await res.json());
|
|
196
|
+
if (!data.access_token) {
|
|
197
|
+
throw new Error("Codex token refresh returned no access_token");
|
|
198
|
+
}
|
|
199
|
+
return credentialsFromTokens({
|
|
200
|
+
access_token: data.access_token,
|
|
201
|
+
// The endpoint only returns a refresh token when it rotates one; reuse the
|
|
202
|
+
// current one otherwise, or the next refresh has nothing to present.
|
|
203
|
+
refresh_token: data.refresh_token ?? refreshToken,
|
|
204
|
+
id_token: data.id_token,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Wrap credentials in a self-refreshing token provider.
|
|
209
|
+
*
|
|
210
|
+
* Refreshes lazily — only when a token is actually asked for and the current
|
|
211
|
+
* one is within `refreshSkewSeconds` of expiry. Concurrent callers share a
|
|
212
|
+
* single in-flight refresh rather than each starting their own.
|
|
213
|
+
*/
|
|
214
|
+
function createCodexTokenProvider(credentials, options = {}) {
|
|
215
|
+
const skew = options.refreshSkewSeconds ?? 300;
|
|
216
|
+
let current = credentials;
|
|
217
|
+
let expiresAt = jwtExpiry(credentials.accessToken);
|
|
218
|
+
let inFlight;
|
|
219
|
+
const isFresh = () => {
|
|
220
|
+
// An opaque token with no readable `exp` is assumed good: refreshing on
|
|
221
|
+
// every call would be worse than letting a 401 surface.
|
|
222
|
+
if (expiresAt === undefined)
|
|
223
|
+
return true;
|
|
224
|
+
return Date.now() / 1000 < expiresAt - skew;
|
|
225
|
+
};
|
|
226
|
+
const refresh = async () => {
|
|
227
|
+
if (!current.refreshToken) {
|
|
228
|
+
throw new Error("Codex access token has expired and no refresh token is available — run `codex login` again");
|
|
229
|
+
}
|
|
230
|
+
const next = await refreshCodexCredentials(current.refreshToken, {
|
|
231
|
+
clientId: options.clientId,
|
|
232
|
+
tokenUrl: options.tokenUrl,
|
|
233
|
+
});
|
|
234
|
+
current = {
|
|
235
|
+
...next,
|
|
236
|
+
// A refresh response carries no id_token in some cases, which would drop
|
|
237
|
+
// the account id the `chatgpt-account-id` header needs.
|
|
238
|
+
accountId: next.accountId ?? current.accountId,
|
|
239
|
+
email: next.email ?? current.email,
|
|
240
|
+
planType: next.planType ?? current.planType,
|
|
241
|
+
};
|
|
242
|
+
expiresAt = jwtExpiry(current.accessToken);
|
|
243
|
+
try {
|
|
244
|
+
await options.onRefresh?.(current);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
// Persisting is best-effort; the token in hand is still valid.
|
|
248
|
+
}
|
|
249
|
+
return current.accessToken;
|
|
250
|
+
};
|
|
251
|
+
return {
|
|
252
|
+
getToken: async () => {
|
|
253
|
+
if (isFresh())
|
|
254
|
+
return current.accessToken;
|
|
255
|
+
// Collapse concurrent refreshes: the second caller awaits the first.
|
|
256
|
+
inFlight ?? (inFlight = refresh().finally(() => {
|
|
257
|
+
inFlight = undefined;
|
|
258
|
+
}));
|
|
259
|
+
return inFlight;
|
|
260
|
+
},
|
|
261
|
+
current: () => current,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
//# sourceMappingURL=codex-auth.js.map
|