codex-multi-auth 2.11.0 → 2.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.
Files changed (63) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/README.md +7 -1
  3. package/dist/lib/accounts.d.ts +75 -0
  4. package/dist/lib/accounts.d.ts.map +1 -1
  5. package/dist/lib/accounts.js +213 -42
  6. package/dist/lib/capability-policy.d.ts.map +1 -1
  7. package/dist/lib/capability-policy.js +2 -1
  8. package/dist/lib/codex-manager/account-manager-commands.d.ts.map +1 -1
  9. package/dist/lib/codex-manager/account-manager-commands.js +1 -0
  10. package/dist/lib/codex-manager/commands/limits.d.ts +21 -0
  11. package/dist/lib/codex-manager/commands/limits.d.ts.map +1 -0
  12. package/dist/lib/codex-manager/commands/limits.js +158 -0
  13. package/dist/lib/codex-manager/help.d.ts.map +1 -1
  14. package/dist/lib/codex-manager/help.js +1 -0
  15. package/dist/lib/codex-manager/persist-selected-account.d.ts.map +1 -1
  16. package/dist/lib/codex-manager/persist-selected-account.js +14 -0
  17. package/dist/lib/codex-manager.d.ts.map +1 -1
  18. package/dist/lib/codex-manager.js +12 -0
  19. package/dist/lib/config.d.ts.map +1 -1
  20. package/dist/lib/config.js +2 -1
  21. package/dist/lib/constants.d.ts +9 -0
  22. package/dist/lib/constants.d.ts.map +1 -1
  23. package/dist/lib/constants.js +29 -0
  24. package/dist/lib/entitlement-cache.d.ts.map +1 -1
  25. package/dist/lib/entitlement-cache.js +2 -1
  26. package/dist/lib/forecast.d.ts.map +1 -1
  27. package/dist/lib/forecast.js +1 -14
  28. package/dist/lib/policy/runtime-policy.d.ts +9 -1
  29. package/dist/lib/policy/runtime-policy.d.ts.map +1 -1
  30. package/dist/lib/policy/runtime-policy.js +1 -0
  31. package/dist/lib/preemptive-quota-scheduler.d.ts +12 -0
  32. package/dist/lib/preemptive-quota-scheduler.d.ts.map +1 -1
  33. package/dist/lib/preemptive-quota-scheduler.js +21 -0
  34. package/dist/lib/redaction.d.ts +17 -0
  35. package/dist/lib/redaction.d.ts.map +1 -0
  36. package/dist/lib/redaction.js +28 -0
  37. package/dist/lib/request/error-classification.js +2 -2
  38. package/dist/lib/runtime/rotation-storage-meta.d.ts.map +1 -1
  39. package/dist/lib/runtime/rotation-storage-meta.js +4 -1
  40. package/dist/lib/runtime-rotation-proxy.d.ts +13 -0
  41. package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -1
  42. package/dist/lib/runtime-rotation-proxy.js +47 -7
  43. package/dist/lib/storage.d.ts +20 -2
  44. package/dist/lib/storage.d.ts.map +1 -1
  45. package/dist/lib/storage.js +69 -25
  46. package/dist/lib/usage/ledger.d.ts +1 -1
  47. package/dist/lib/usage/ledger.d.ts.map +1 -1
  48. package/dist/lib/usage/ledger.js +9 -0
  49. package/dist/lib/usage/pricing.d.ts +16 -1
  50. package/dist/lib/usage/pricing.d.ts.map +1 -1
  51. package/dist/lib/usage/pricing.js +46 -8
  52. package/dist/lib/usage/redaction.d.ts +1 -1
  53. package/dist/lib/usage/redaction.d.ts.map +1 -1
  54. package/dist/lib/usage/redaction.js +13 -0
  55. package/dist/lib/usage/types.d.ts +33 -0
  56. package/dist/lib/usage/types.d.ts.map +1 -1
  57. package/dist/lib/usage/types.js +28 -1
  58. package/dist/lib/usage/usage-extraction.d.ts +2 -1
  59. package/dist/lib/usage/usage-extraction.d.ts.map +1 -1
  60. package/dist/lib/usage/usage-extraction.js +44 -4
  61. package/package.json +1 -1
  62. package/scripts/codex-routing.js +1 -0
  63. package/scripts/codex.js +157 -2
@@ -66,18 +66,58 @@ export function extractUsageTokenCounts(usage) {
66
66
  totalTokens: providedTotal,
67
67
  };
68
68
  }
69
+ /**
70
+ * Read the billed service tier off a Responses payload.
71
+ *
72
+ * `service_tier` sits on the response object, not inside `usage`, and a stream
73
+ * event wraps that object one level down. An unrecognised value becomes
74
+ * `unknown` rather than being dropped, because dropping it would price the row
75
+ * at standard rates and under-count a more expensive tier.
76
+ */
77
+ function extractServiceTier(payload) {
78
+ const raw = payload.service_tier ??
79
+ (isRecord(payload.response) ? payload.response.service_tier : undefined);
80
+ if (typeof raw !== "string")
81
+ return undefined;
82
+ const normalized = raw.trim().toLowerCase();
83
+ if (normalized.length === 0)
84
+ return undefined;
85
+ // `default` is what the API calls the standard tier on the wire.
86
+ if (normalized === "default" || normalized === "standard")
87
+ return "standard";
88
+ // The upstream catalog names this tier `priority` with the display name
89
+ // "Fast", and lists `fast` in `additional_speed_tiers`, so a response can
90
+ // report either spelling. Without this it becomes `unknown`, which prices
91
+ // as unpriced and fails a cost budget closed on a tier we DO have a rate
92
+ // for.
93
+ if (normalized === "fast")
94
+ return "priority";
95
+ if (normalized === "priority" ||
96
+ normalized === "flex" ||
97
+ normalized === "batch" ||
98
+ normalized === "scale") {
99
+ return normalized;
100
+ }
101
+ return "unknown";
102
+ }
69
103
  /**
70
104
  * Pull the `usage` object out of a Responses payload, whether it arrived as a
71
- * bare response object (non-streaming) or wrapped in a stream event envelope.
105
+ * bare response object (non-streaming) or wrapped in a stream event envelope,
106
+ * and attach the tier it was billed at.
72
107
  */
73
108
  export function extractResponsesUsage(payload) {
74
109
  if (!isRecord(payload))
75
110
  return null;
111
+ const serviceTier = extractServiceTier(payload);
76
112
  const direct = extractUsageTokenCounts(payload.usage);
77
- if (direct)
78
- return direct;
113
+ if (direct) {
114
+ return serviceTier ? { ...direct, serviceTier } : direct;
115
+ }
79
116
  if (isRecord(payload.response)) {
80
- return extractUsageTokenCounts(payload.response.usage);
117
+ const nested = extractUsageTokenCounts(payload.response.usage);
118
+ if (!nested)
119
+ return null;
120
+ return serviceTier ? { ...nested, serviceTier } : nested;
81
121
  }
82
122
  return null;
83
123
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-multi-auth",
3
- "version": "2.11.0",
3
+ "version": "2.13.0",
4
4
  "description": "Codex CLI multi-account OAuth manager with account switching, health checks, runtime rotation, diagnostics, and recovery tools for @openai/codex",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -2,6 +2,7 @@ const AUTH_SUBCOMMANDS = new Set([
2
2
  "login",
3
3
  "list",
4
4
  "status",
5
+ "limits",
5
6
  "switch",
6
7
  "unpin",
7
8
  "workspace",
package/scripts/codex.js CHANGED
@@ -78,6 +78,8 @@ const APP_RUNTIME_HELPER_USE_CANONICAL_HOME_ENV =
78
78
  "CODEX_MULTI_AUTH_APP_ROTATION_USE_CANONICAL_HOME";
79
79
  const APP_RUNTIME_HELPER_INSTALL_APP_SERVER_SHIM_ENV =
80
80
  "CODEX_MULTI_AUTH_APP_ROTATION_INSTALL_APP_SERVER_SHIM";
81
+ const RUNTIME_ROTATION_PROXY_UPSTREAM_BASE_URL_ENV =
82
+ "CODEX_MULTI_AUTH_RUNTIME_PROXY_UPSTREAM_BASE_URL";
81
83
  const APP_SERVER_CONFIG_ARGS_ENV =
82
84
  "CODEX_MULTI_AUTH_APP_SERVER_CONFIG_ARGS_JSON";
83
85
  const APP_RUNTIME_HELPER_STATUS_FILE =
@@ -168,6 +170,95 @@ let warnedShadowHomeSqliteLinkFailure = false;
168
170
  const warnedShadowHomeLinkOnlyDirectoryFailures = new Set();
169
171
  const warnedShadowHomeSqliteSidecarPlaceholderFailures = new Set();
170
172
 
173
+ /**
174
+ * True only for a NUMERIC loopback literal, as `new URL().hostname` reports it.
175
+ *
176
+ * A name is deliberately not enough. `localhost` is resolved by the OS at
177
+ * connect time, so an /etc/hosts or Windows hosts entry can point it at a
178
+ * routable address, and the upstream request carries the managed OAuth bearer
179
+ * token and the request body. WHATWG URL always reports an IPv6 host in its
180
+ * bracketed form, so `::1` is only ever seen here as `[::1]`.
181
+ */
182
+ function isLoopbackUrlHostname(hostname) {
183
+ if (hostname === "[::1]") {
184
+ return true;
185
+ }
186
+ const ipv4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
187
+ if (!ipv4) {
188
+ return false;
189
+ }
190
+ const octets = ipv4.slice(1).map((part) => Number(part));
191
+ if (octets.some((octet) => !Number.isInteger(octet) || octet > 255)) {
192
+ return false;
193
+ }
194
+ // The whole 127.0.0.0/8 block is loopback, not just 127.0.0.1.
195
+ return octets[0] === 127;
196
+ }
197
+
198
+ /**
199
+ * Whether the raw URL text carries an explicit `:port`.
200
+ *
201
+ * `new URL()` erases a port that matches the scheme default, so
202
+ * `http://127.0.0.1:80/x` reports `parsed.port === ""` and a `!parsed.port`
203
+ * check would reject a perfectly valid, explicitly-ported loopback URL.
204
+ */
205
+ function hasExplicitUrlPort(raw) {
206
+ const withoutScheme = raw.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//, "");
207
+ const authority = withoutScheme.split(/[/?#]/, 1)[0] ?? "";
208
+ // Strip userinfo first, or a password containing ':' reads as a port.
209
+ const hostAndPort = authority.slice(authority.lastIndexOf("@") + 1);
210
+ if (hostAndPort.startsWith("[")) {
211
+ const close = hostAndPort.indexOf("]");
212
+ return close !== -1 && /^:\d+$/.test(hostAndPort.slice(close + 1));
213
+ }
214
+ const colon = hostAndPort.indexOf(":");
215
+ return colon !== -1 && /^:\d+$/.test(hostAndPort.slice(colon));
216
+ }
217
+
218
+ function resolveRuntimeRotationProxyUpstreamBaseUrl(env = process.env) {
219
+ const raw = (env[RUNTIME_ROTATION_PROXY_UPSTREAM_BASE_URL_ENV] ?? "").trim();
220
+ if (!raw) {
221
+ return undefined;
222
+ }
223
+
224
+ let parsed;
225
+ try {
226
+ parsed = new URL(raw);
227
+ } catch {
228
+ throw new Error(
229
+ `${RUNTIME_ROTATION_PROXY_UPSTREAM_BASE_URL_ENV} must be an absolute loopback HTTP URL.`,
230
+ );
231
+ }
232
+
233
+ if (
234
+ parsed.protocol !== "http:" ||
235
+ !isLoopbackUrlHostname(parsed.hostname.toLowerCase()) ||
236
+ !(parsed.port || hasExplicitUrlPort(raw)) ||
237
+ parsed.username ||
238
+ parsed.password ||
239
+ parsed.search ||
240
+ parsed.hash
241
+ ) {
242
+ throw new Error(
243
+ `${RUNTIME_ROTATION_PROXY_UPSTREAM_BASE_URL_ENV} must use HTTP with a numeric loopback host (127.0.0.0/8 or [::1]; a name such as "localhost" is not accepted because it can resolve off-host), an explicit port, and no credentials, query, or fragment.`,
244
+ );
245
+ }
246
+
247
+ return parsed.toString().replace(/\/$/, "");
248
+ }
249
+
250
+ /**
251
+ * @param clientApiKey Shared secret the wrapper and proxy authenticate with.
252
+ * @param upstreamBaseUrl Already-resolved upstream, or undefined for the
253
+ * default backend. Callers pass the value they resolved rather than the env, so
254
+ * one launch never parses (and never re-reports) the same variable twice.
255
+ */
256
+ function createRuntimeRotationProxyOptions(clientApiKey, upstreamBaseUrl) {
257
+ return upstreamBaseUrl
258
+ ? { clientApiKey, upstreamBaseUrl }
259
+ : { clientApiKey };
260
+ }
261
+
171
262
  async function loadRuntimeConstants() {
172
263
  const fallback = {
173
264
  RUNTIME_ROTATION_PROXY_PROVIDER_ID: `${APP_SERVER_ACCOUNT_DISPLAY_NAME}-runtime-proxy`,
@@ -4711,7 +4802,12 @@ async function runRuntimeRotationAppHelper(identityToken = "") {
4711
4802
  throw new Error("runtime rotation config helpers are unavailable");
4712
4803
  }
4713
4804
  const clientApiKey = createRuntimeRotationProxyClientApiKey();
4714
- proxyServer = await proxyModule.startRuntimeRotationProxy({ clientApiKey });
4805
+ proxyServer = await proxyModule.startRuntimeRotationProxy(
4806
+ createRuntimeRotationProxyOptions(
4807
+ clientApiKey,
4808
+ resolveRuntimeRotationProxyUpstreamBaseUrl(),
4809
+ ),
4810
+ );
4715
4811
  const useCanonicalHome =
4716
4812
  (process.env[APP_RUNTIME_HELPER_USE_CANONICAL_HOME_ENV] ?? "").trim() ===
4717
4813
  "1";
@@ -5112,13 +5208,52 @@ async function createRuntimeRotationProxyContextIfEnabled(
5112
5208
  baseContext,
5113
5209
  rawArgs,
5114
5210
  ) {
5211
+ // Resolve the configured upstream BEFORE the enabled gate. Every path that
5212
+ // leaves this function with `baseContext` forwards Responses traffic, and its
5213
+ // managed OAuth bearer token, straight to the real backend. An explicit
5214
+ // upstream is a routing requirement rather than a rotation preference, so
5215
+ // `CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY=0`, `CODEX_MULTI_AUTH_BYPASS=1`, a
5216
+ // missing `dist/lib/config.js` or a disabled setting must all fail closed
5217
+ // here instead of silently reaching chatgpt.com.
5218
+ let configuredUpstreamBaseUrl;
5219
+ try {
5220
+ configuredUpstreamBaseUrl =
5221
+ resolveRuntimeRotationProxyUpstreamBaseUrl(baseContext.env);
5222
+ } catch (error) {
5223
+ baseContext.cleanup?.();
5224
+ return {
5225
+ startupError: `codex-multi-auth runtime rotation upstream is invalid: ${
5226
+ error instanceof Error ? error.message : String(error)
5227
+ }`,
5228
+ };
5229
+ }
5230
+ // The one exception is a subcommand that never reaches the backend at all
5231
+ // (`--version`, `login`, ...): there is no traffic to misroute, so it passes
5232
+ // through with the upstream simply unused.
5233
+ const requireConfiguredUpstream =
5234
+ configuredUpstreamBaseUrl !== undefined &&
5235
+ shouldUseRuntimeRoutingForForwardedArgs(rawArgs);
5236
+
5115
5237
  const enabled = await isRuntimeRotationProxyEnabled(rawArgs, baseContext.env);
5116
5238
  if (!enabled) {
5239
+ if (requireConfiguredUpstream) {
5240
+ baseContext.cleanup?.();
5241
+ return {
5242
+ startupError: `codex-multi-auth runtime rotation is disabled, so ${RUNTIME_ROTATION_PROXY_UPSTREAM_BASE_URL_ENV} cannot be honored and requests would go to the direct backend instead. Enable runtime rotation or unset that variable.`,
5243
+ };
5244
+ }
5117
5245
  return baseContext;
5118
5246
  }
5119
5247
 
5120
5248
  const configTomlModule = await loadRuntimeConfigTomlModule();
5121
5249
  if (!configTomlModule) {
5250
+ if (requireConfiguredUpstream) {
5251
+ baseContext.cleanup?.();
5252
+ return {
5253
+ startupError:
5254
+ "codex-multi-auth runtime rotation config helpers are unavailable; configured upstream routing cannot continue.",
5255
+ };
5256
+ }
5122
5257
  console.error(
5123
5258
  "codex-multi-auth runtime rotation config helpers are unavailable; continuing without runtime rotation.",
5124
5259
  );
@@ -5188,6 +5323,13 @@ async function createRuntimeRotationProxyContextIfEnabled(
5188
5323
 
5189
5324
  const proxyModule = await loadRuntimeRotationProxyModule();
5190
5325
  if (!proxyModule) {
5326
+ if (requireConfiguredUpstream) {
5327
+ baseContext.cleanup?.();
5328
+ return {
5329
+ startupError:
5330
+ "codex-multi-auth runtime rotation proxy is unavailable; configured upstream routing cannot continue.",
5331
+ };
5332
+ }
5191
5333
  console.error(
5192
5334
  "codex-multi-auth runtime rotation proxy is unavailable; continuing without runtime rotation.",
5193
5335
  );
@@ -5198,7 +5340,12 @@ async function createRuntimeRotationProxyContextIfEnabled(
5198
5340
  let shadowContext;
5199
5341
  try {
5200
5342
  const clientApiKey = createRuntimeRotationProxyClientApiKey();
5201
- proxyServer = await proxyModule.startRuntimeRotationProxy({ clientApiKey });
5343
+ proxyServer = await proxyModule.startRuntimeRotationProxy(
5344
+ createRuntimeRotationProxyOptions(
5345
+ clientApiKey,
5346
+ configuredUpstreamBaseUrl,
5347
+ ),
5348
+ );
5202
5349
  shadowContext = createRuntimeRotationProxyCodexHome(
5203
5350
  baseContext.env,
5204
5351
  proxyServer.baseUrl,
@@ -5211,6 +5358,14 @@ async function createRuntimeRotationProxyContextIfEnabled(
5211
5358
  } catch {
5212
5359
  // Best-effort cleanup only.
5213
5360
  }
5361
+ if (requireConfiguredUpstream) {
5362
+ baseContext.cleanup?.();
5363
+ return {
5364
+ startupError: `codex-multi-auth runtime rotation proxy failed to start with configured upstream: ${
5365
+ error instanceof Error ? error.message : String(error)
5366
+ }`,
5367
+ };
5368
+ }
5214
5369
  console.error(
5215
5370
  `codex-multi-auth runtime rotation proxy failed to start; continuing without runtime rotation: ${error instanceof Error ? error.message : String(error)}`,
5216
5371
  );