@juspay/neurolink 11.1.1 → 11.2.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.
Files changed (74) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/auth/codexOAuth.d.ts +67 -0
  3. package/dist/auth/codexOAuth.js +202 -0
  4. package/dist/auth/index.d.ts +1 -0
  5. package/dist/auth/index.js +4 -0
  6. package/dist/browser/neurolink.min.js +401 -401
  7. package/dist/cli/commands/auth.d.ts +27 -8
  8. package/dist/cli/commands/auth.js +425 -6
  9. package/dist/cli/commands/proxy.js +230 -5
  10. package/dist/cli/factories/authCommandFactory.d.ts +8 -0
  11. package/dist/cli/factories/authCommandFactory.js +74 -1
  12. package/dist/lib/auth/codexOAuth.d.ts +67 -0
  13. package/dist/lib/auth/codexOAuth.js +203 -0
  14. package/dist/lib/auth/index.d.ts +1 -0
  15. package/dist/lib/auth/index.js +4 -0
  16. package/dist/lib/providers/configuredOpenAICompat.d.ts +24 -0
  17. package/dist/lib/providers/configuredOpenAICompat.js +60 -0
  18. package/dist/lib/providers/openaiCompatCatalog.d.ts +24 -0
  19. package/dist/lib/providers/openaiCompatCatalog.js +272 -0
  20. package/dist/lib/proxy/accountCooldown.js +35 -2
  21. package/dist/lib/proxy/accountQuota.d.ts +29 -3
  22. package/dist/lib/proxy/accountQuota.js +203 -12
  23. package/dist/lib/proxy/accountUsage.js +15 -2
  24. package/dist/lib/proxy/codexAccountUsage.d.ts +26 -0
  25. package/dist/lib/proxy/codexAccountUsage.js +174 -0
  26. package/dist/lib/proxy/proxyAnalysis.js +12 -1
  27. package/dist/lib/proxy/proxyConfig.js +24 -0
  28. package/dist/lib/proxy/routingEvidence.d.ts +12 -1
  29. package/dist/lib/proxy/routingEvidence.js +23 -0
  30. package/dist/lib/proxy/runtimeConfig.js +3 -0
  31. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +79 -5
  32. package/dist/lib/server/routes/claudeProxyRoutes.js +653 -72
  33. package/dist/lib/server/routes/codexProxyRoutes.d.ts +64 -0
  34. package/dist/lib/server/routes/codexProxyRoutes.js +454 -0
  35. package/dist/lib/types/cli.d.ts +7 -1
  36. package/dist/lib/types/codex.d.ts +95 -0
  37. package/dist/lib/types/codex.js +15 -0
  38. package/dist/lib/types/index.d.ts +1 -0
  39. package/dist/lib/types/index.js +1 -0
  40. package/dist/lib/types/providers.d.ts +99 -0
  41. package/dist/lib/types/proxy.d.ts +83 -0
  42. package/dist/lib/types/subscription.d.ts +13 -0
  43. package/dist/lib/utils/providerConfig.d.ts +23 -1
  44. package/dist/lib/utils/providerConfig.js +60 -0
  45. package/dist/providers/configuredOpenAICompat.d.ts +24 -0
  46. package/dist/providers/configuredOpenAICompat.js +59 -0
  47. package/dist/providers/openaiCompatCatalog.d.ts +24 -0
  48. package/dist/providers/openaiCompatCatalog.js +271 -0
  49. package/dist/proxy/accountCooldown.js +35 -2
  50. package/dist/proxy/accountQuota.d.ts +29 -3
  51. package/dist/proxy/accountQuota.js +203 -12
  52. package/dist/proxy/accountUsage.js +15 -2
  53. package/dist/proxy/codexAccountUsage.d.ts +26 -0
  54. package/dist/proxy/codexAccountUsage.js +173 -0
  55. package/dist/proxy/proxyAnalysis.js +12 -1
  56. package/dist/proxy/proxyConfig.js +24 -0
  57. package/dist/proxy/routingEvidence.d.ts +12 -1
  58. package/dist/proxy/routingEvidence.js +23 -0
  59. package/dist/proxy/runtimeConfig.js +3 -0
  60. package/dist/server/routes/claudeProxyRoutes.d.ts +79 -5
  61. package/dist/server/routes/claudeProxyRoutes.js +653 -72
  62. package/dist/server/routes/codexProxyRoutes.d.ts +64 -0
  63. package/dist/server/routes/codexProxyRoutes.js +453 -0
  64. package/dist/types/cli.d.ts +7 -1
  65. package/dist/types/codex.d.ts +95 -0
  66. package/dist/types/codex.js +14 -0
  67. package/dist/types/index.d.ts +1 -0
  68. package/dist/types/index.js +1 -0
  69. package/dist/types/providers.d.ts +99 -0
  70. package/dist/types/proxy.d.ts +83 -0
  71. package/dist/types/subscription.d.ts +13 -0
  72. package/dist/utils/providerConfig.d.ts +23 -1
  73. package/dist/utils/providerConfig.js +60 -0
  74. package/package.json +3 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [11.2.1](https://github.com/juspay/neurolink/compare/v11.2.0...v11.2.1) (2026-08-18)
2
+
3
+ ### Bug Fixes
4
+
5
+ - **(proxy):** rotate off entitlement-blocked accounts and route model-scoped quota ([a634f8a](https://github.com/juspay/neurolink/commit/a634f8ae5124daec78ea336f5d211f3b960690be))
6
+
7
+ ## [11.2.0](https://github.com/juspay/neurolink/compare/v11.1.1...v11.2.0) (2026-08-18)
8
+
9
+ ### Features
10
+
11
+ - **(providers):** add the catalog foundations for OpenAI-compatible providers ([baf1b2a](https://github.com/juspay/neurolink/commit/baf1b2a7c23bb85286a564e82b34a444e44aa715))
12
+
1
13
  ## [11.1.1](https://github.com/juspay/neurolink/compare/v11.1.0...v11.1.1) (2026-08-18)
2
14
 
3
15
  ### Bug Fixes
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Codex (OpenAI ChatGPT) OAuth + identity helpers.
3
+ *
4
+ * Mirrors the role of `anthropicOAuth.ts` for the Codex pool engine. Codex signs
5
+ * in with a ChatGPT account (PKCE OAuth against auth.openai.com) and talks to the
6
+ * ChatGPT backend Responses API. The proxy pools multiple ChatGPT accounts, so it
7
+ * needs to: import an existing `~/.codex/auth.json`, refresh access tokens, and
8
+ * derive the per-account `chatgpt-account-id` the backend requires.
9
+ *
10
+ * Constants verified against Codex CLI 0.144.4.
11
+ */
12
+ import type { CodexImportedCredential } from "../types/index.js";
13
+ export declare const CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
14
+ export declare const CODEX_AUTH_URL = "https://auth.openai.com/oauth/authorize";
15
+ export declare const CODEX_TOKEN_URL = "https://auth.openai.com/oauth/token";
16
+ export declare const CODEX_REVOKE_URL = "https://auth.openai.com/oauth/revoke";
17
+ export declare const CODEX_REDIRECT_URI = "http://localhost:1455/auth/callback";
18
+ export declare const CODEX_DEFAULT_SCOPES: readonly ["openid", "profile", "email", "offline_access"];
19
+ export declare const CODEX_BACKEND_BASE_URL = "https://chatgpt.com/backend-api/codex";
20
+ export declare const CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
21
+ export declare const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/codex/usage";
22
+ export declare const CODEX_VERSION = "0.144.4";
23
+ export declare const CODEX_USER_AGENT = "codex_cli_rs/0.144.4 (external; neurolink-proxy)";
24
+ export declare const CODEX_ORIGINATOR = "codex_cli_rs";
25
+ /** Decode the ChatGPT account id, plan type, and expiry from an access token. */
26
+ export declare function decodeCodexAccessToken(accessToken: string): {
27
+ accountId?: string;
28
+ planType?: string;
29
+ expiresAt?: number;
30
+ };
31
+ /** Decode the account email from an id token (best-effort, for the label). */
32
+ export declare function decodeCodexEmail(idToken?: string): string | undefined;
33
+ /**
34
+ * The chatgpt-account-id the backend requires for a given access token.
35
+ * Prefer the value stored on the account; fall back to decoding the JWT so a
36
+ * rotated token always carries a coherent account id.
37
+ */
38
+ export declare function resolveCodexAccountId(accessToken: string, storedAccountId?: string): string | undefined;
39
+ /**
40
+ * Import an existing Codex credential from `~/.codex/auth.json` (or a custom
41
+ * path). This is the verified, robust login path: the user logs into Codex
42
+ * normally, then imports the current account into the proxy pool.
43
+ */
44
+ export declare function importCodexAuthFile(authFilePath?: string): Promise<CodexImportedCredential>;
45
+ /**
46
+ * Refresh a Codex access token. Verified: POST form-urlencoded to
47
+ * auth.openai.com/oauth/token with grant_type=refresh_token and the Codex
48
+ * client id. Returns a fresh credential (access + rotated refresh token).
49
+ */
50
+ export declare function refreshCodexToken(refreshToken: string, options?: {
51
+ timeoutMs?: number;
52
+ }): Promise<CodexImportedCredential>;
53
+ /**
54
+ * Whether a {@link refreshCodexToken} failure means the refresh token itself is
55
+ * no longer usable, as opposed to the request never getting a verdict.
56
+ *
57
+ * Only the authorization server rejecting the grant is permanent. A 5xx, a
58
+ * timeout, or a DNS failure says nothing about the credential, and treating
59
+ * those as permanent disables a working account — which `auth cleanup` then
60
+ * deletes.
61
+ */
62
+ export declare function isPermanentCodexRefreshFailure(error: unknown): boolean;
63
+ /**
64
+ * Whether an imported credential's access token is expired (or within the
65
+ * buffer window) and should be refreshed before use.
66
+ */
67
+ export declare function codexTokenNeedsRefresh(expiresAt: number | undefined, bufferMs?: number): boolean;
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Codex (OpenAI ChatGPT) OAuth + identity helpers.
3
+ *
4
+ * Mirrors the role of `anthropicOAuth.ts` for the Codex pool engine. Codex signs
5
+ * in with a ChatGPT account (PKCE OAuth against auth.openai.com) and talks to the
6
+ * ChatGPT backend Responses API. The proxy pools multiple ChatGPT accounts, so it
7
+ * needs to: import an existing `~/.codex/auth.json`, refresh access tokens, and
8
+ * derive the per-account `chatgpt-account-id` the backend requires.
9
+ *
10
+ * Constants verified against Codex CLI 0.144.4.
11
+ */
12
+ import { readFile } from "node:fs/promises";
13
+ import { homedir } from "node:os";
14
+ import { join } from "node:path";
15
+ import { logger } from "../utils/logger.js";
16
+ // OAuth client + endpoints (Codex CLI values).
17
+ export const CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
18
+ export const CODEX_AUTH_URL = "https://auth.openai.com/oauth/authorize";
19
+ export const CODEX_TOKEN_URL = "https://auth.openai.com/oauth/token";
20
+ export const CODEX_REVOKE_URL = "https://auth.openai.com/oauth/revoke";
21
+ export const CODEX_REDIRECT_URI = "http://localhost:1455/auth/callback";
22
+ export const CODEX_DEFAULT_SCOPES = [
23
+ "openid",
24
+ "profile",
25
+ "email",
26
+ "offline_access",
27
+ ];
28
+ // Upstream backend (ChatGPT subscription path).
29
+ export const CODEX_BACKEND_BASE_URL = "https://chatgpt.com/backend-api/codex";
30
+ export const CODEX_RESPONSES_URL = `${CODEX_BACKEND_BASE_URL}/responses`;
31
+ export const CODEX_USAGE_URL = `${CODEX_BACKEND_BASE_URL}/usage`;
32
+ // Client fingerprint. The real Codex CLI sends these; the proxy defaults them
33
+ // only when a non-Codex client omits them.
34
+ export const CODEX_VERSION = "0.144.4";
35
+ export const CODEX_USER_AGENT = `codex_cli_rs/${CODEX_VERSION} (external; neurolink-proxy)`;
36
+ export const CODEX_ORIGINATOR = "codex_cli_rs";
37
+ const CODEX_AUTH_FILE = join(homedir(), ".codex", "auth.json");
38
+ /** Base64url-decode a JWT segment into a JSON object (claims only, no verify). */
39
+ function decodeJwtSegment(segment) {
40
+ try {
41
+ const padded = segment + "=".repeat((4 - (segment.length % 4)) % 4);
42
+ const json = Buffer.from(padded.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8");
43
+ const parsed = JSON.parse(json);
44
+ return parsed && typeof parsed === "object"
45
+ ? parsed
46
+ : null;
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ function getOpenAiAuthClaim(claims) {
53
+ if (!claims) {
54
+ return null;
55
+ }
56
+ const auth = claims["https://api.openai.com/auth"];
57
+ return auth && typeof auth === "object"
58
+ ? auth
59
+ : null;
60
+ }
61
+ /** Decode the ChatGPT account id, plan type, and expiry from an access token. */
62
+ export function decodeCodexAccessToken(accessToken) {
63
+ const parts = accessToken.split(".");
64
+ if (parts.length !== 3) {
65
+ return {};
66
+ }
67
+ const claims = decodeJwtSegment(parts[1]);
68
+ const authClaim = getOpenAiAuthClaim(claims);
69
+ const accountId = typeof authClaim?.chatgpt_account_id === "string"
70
+ ? authClaim.chatgpt_account_id
71
+ : undefined;
72
+ const planType = typeof authClaim?.chatgpt_plan_type === "string"
73
+ ? authClaim.chatgpt_plan_type
74
+ : undefined;
75
+ const exp = typeof claims?.exp === "number" ? claims.exp * 1000 : undefined;
76
+ return { accountId, planType, expiresAt: exp };
77
+ }
78
+ /** Decode the account email from an id token (best-effort, for the label). */
79
+ export function decodeCodexEmail(idToken) {
80
+ if (!idToken) {
81
+ return undefined;
82
+ }
83
+ const parts = idToken.split(".");
84
+ if (parts.length !== 3) {
85
+ return undefined;
86
+ }
87
+ const claims = decodeJwtSegment(parts[1]);
88
+ return typeof claims?.email === "string" ? claims.email : undefined;
89
+ }
90
+ /**
91
+ * The chatgpt-account-id the backend requires for a given access token.
92
+ * Prefer the value stored on the account; fall back to decoding the JWT so a
93
+ * rotated token always carries a coherent account id.
94
+ */
95
+ export function resolveCodexAccountId(accessToken, storedAccountId) {
96
+ if (storedAccountId && storedAccountId.length > 0) {
97
+ return storedAccountId;
98
+ }
99
+ return decodeCodexAccessToken(accessToken).accountId;
100
+ }
101
+ /** Build a CodexImportedCredential from raw token material. */
102
+ function buildImportedCredential(tokens) {
103
+ const decoded = decodeCodexAccessToken(tokens.access_token);
104
+ return {
105
+ accessToken: tokens.access_token,
106
+ refreshToken: tokens.refresh_token,
107
+ idToken: tokens.id_token,
108
+ accountId: tokens.account_id ?? decoded.accountId,
109
+ expiresAt: decoded.expiresAt,
110
+ planType: decoded.planType,
111
+ email: decodeCodexEmail(tokens.id_token),
112
+ };
113
+ }
114
+ /**
115
+ * Import an existing Codex credential from `~/.codex/auth.json` (or a custom
116
+ * path). This is the verified, robust login path: the user logs into Codex
117
+ * normally, then imports the current account into the proxy pool.
118
+ */
119
+ export async function importCodexAuthFile(authFilePath = CODEX_AUTH_FILE) {
120
+ const raw = await readFile(authFilePath, "utf8");
121
+ const parsed = JSON.parse(raw);
122
+ const tokens = parsed.tokens;
123
+ if (!tokens || typeof tokens.access_token !== "string") {
124
+ throw new Error(`No ChatGPT OAuth tokens found in ${authFilePath}. Run \`codex login\` first.`);
125
+ }
126
+ if (parsed.auth_mode && parsed.auth_mode !== "chatgpt") {
127
+ throw new Error(`Codex auth mode is "${parsed.auth_mode}", expected "chatgpt". Only ChatGPT subscription login can be pooled.`);
128
+ }
129
+ return buildImportedCredential({
130
+ access_token: tokens.access_token,
131
+ refresh_token: tokens.refresh_token,
132
+ id_token: tokens.id_token,
133
+ account_id: tokens.account_id,
134
+ });
135
+ }
136
+ /**
137
+ * Refresh a Codex access token. Verified: POST form-urlencoded to
138
+ * auth.openai.com/oauth/token with grant_type=refresh_token and the Codex
139
+ * client id. Returns a fresh credential (access + rotated refresh token).
140
+ */
141
+ export async function refreshCodexToken(refreshToken, options = {}) {
142
+ const body = new URLSearchParams({
143
+ grant_type: "refresh_token",
144
+ refresh_token: refreshToken,
145
+ client_id: CODEX_CLIENT_ID,
146
+ scope: CODEX_DEFAULT_SCOPES.join(" "),
147
+ });
148
+ const response = await fetch(CODEX_TOKEN_URL, {
149
+ method: "POST",
150
+ headers: {
151
+ "Content-Type": "application/x-www-form-urlencoded",
152
+ Accept: "application/json",
153
+ "User-Agent": CODEX_USER_AGENT,
154
+ },
155
+ body: body.toString(),
156
+ signal: AbortSignal.timeout(options.timeoutMs ?? 10_000),
157
+ });
158
+ if (!response.ok) {
159
+ const text = await response.text().catch(() => "");
160
+ throw Object.assign(new Error(`Codex token refresh failed: ${response.status} ${text.slice(0, 200)}`), { status: response.status });
161
+ }
162
+ const data = (await response.json());
163
+ if (!data.access_token) {
164
+ throw new Error("Codex token refresh returned no access_token");
165
+ }
166
+ logger.debug("Codex token refreshed");
167
+ return buildImportedCredential({
168
+ access_token: data.access_token,
169
+ // OpenAI rotates the refresh token; fall back to the old one if omitted.
170
+ refresh_token: data.refresh_token ?? refreshToken,
171
+ id_token: data.id_token,
172
+ });
173
+ }
174
+ /**
175
+ * Whether a {@link refreshCodexToken} failure means the refresh token itself is
176
+ * no longer usable, as opposed to the request never getting a verdict.
177
+ *
178
+ * Only the authorization server rejecting the grant is permanent. A 5xx, a
179
+ * timeout, or a DNS failure says nothing about the credential, and treating
180
+ * those as permanent disables a working account — which `auth cleanup` then
181
+ * deletes.
182
+ */
183
+ export function isPermanentCodexRefreshFailure(error) {
184
+ const status = error?.status;
185
+ if (typeof status !== "number" || status < 400 || status >= 500) {
186
+ return false;
187
+ }
188
+ // 408 and 429 are a timeout and a throttle. Neither is the grant being
189
+ // rejected, and treating them as permanent disables the account — which
190
+ // `auth cleanup` then deletes.
191
+ return status !== 408 && status !== 429;
192
+ }
193
+ /**
194
+ * Whether an imported credential's access token is expired (or within the
195
+ * buffer window) and should be refreshed before use.
196
+ */
197
+ export function codexTokenNeedsRefresh(expiresAt, bufferMs = 5 * 60 * 1000) {
198
+ if (!expiresAt) {
199
+ return false;
200
+ }
201
+ return expiresAt - bufferMs <= Date.now();
202
+ }
@@ -14,6 +14,7 @@ export { ANTHROPIC_OAUTH_BASE_URL, DEFAULT_SCOPES, DEFAULT_REDIRECT_URI, DEFAULT
14
14
  export { AnthropicOAuth } from "./anthropicOAuth.js";
15
15
  export { OAuthError, OAuthConfigurationError, OAuthTokenExchangeError, OAuthTokenRefreshError, OAuthTokenValidationError, OAuthTokenRevocationError, OAuthCallbackServerError, } from "../types/index.js";
16
16
  export { createAnthropicOAuth, createAnthropicOAuthConfig, hasAnthropicOAuthCredentials, startCallbackServer, stopCallbackServer, performOAuthFlow, } from "./anthropicOAuth.js";
17
+ export { CODEX_CLIENT_ID, CODEX_AUTH_URL, CODEX_TOKEN_URL, CODEX_REDIRECT_URI, CODEX_DEFAULT_SCOPES, CODEX_BACKEND_BASE_URL, CODEX_RESPONSES_URL, CODEX_USAGE_URL, CODEX_USER_AGENT, CODEX_ORIGINATOR, decodeCodexAccessToken, decodeCodexEmail, resolveCodexAccountId, importCodexAuthFile, refreshCodexToken, codexTokenNeedsRefresh, } from "./codexOAuth.js";
17
18
  export { TokenStore, tokenStore, defaultTokenStore } from "./tokenStore.js";
18
19
  export { TokenStoreError } from "../types/index.js";
19
20
  export { AccountPool } from "./accountPool.js";
@@ -23,6 +23,10 @@ export { OAuthError, OAuthConfigurationError, OAuthTokenExchangeError, OAuthToke
23
23
  export { createAnthropicOAuth, createAnthropicOAuthConfig, hasAnthropicOAuthCredentials, startCallbackServer, stopCallbackServer, performOAuthFlow, } from "./anthropicOAuth.js";
24
24
  // OAuth types (canonical definitions in types/subscriptionTypes.ts)
25
25
  // =============================================================================
26
+ // CODEX (ChatGPT) OAUTH - subscription pool support
27
+ // =============================================================================
28
+ export { CODEX_CLIENT_ID, CODEX_AUTH_URL, CODEX_TOKEN_URL, CODEX_REDIRECT_URI, CODEX_DEFAULT_SCOPES, CODEX_BACKEND_BASE_URL, CODEX_RESPONSES_URL, CODEX_USAGE_URL, CODEX_USER_AGENT, CODEX_ORIGINATOR, decodeCodexAccessToken, decodeCodexEmail, resolveCodexAccountId, importCodexAuthFile, refreshCodexToken, codexTokenNeedsRefresh, } from "./codexOAuth.js";
29
+ // =============================================================================
26
30
  // TOKEN STORE - Secure Token Storage
27
31
  // =============================================================================
28
32
  // Main TokenStore class and instances