offrouter-core 0.1.0 → 0.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.
@@ -10,6 +10,8 @@
10
10
  import { randomBytes } from "node:crypto";
11
11
  import type { SecretStore } from "../secrets.js";
12
12
  import { Redacted } from "./credential-chain.js";
13
+ import { setOAuthTokens } from "./keychain.js";
14
+ import { startCallbackServer } from "./oauth-server.js";
13
15
 
14
16
  // ---------------------------------------------------------------------------
15
17
  // PKCE code generation
@@ -312,15 +314,22 @@ export async function exchangeCodeForTokens(
312
314
  );
313
315
  }
314
316
 
315
- // Store tokens in secret store
317
+ // Store tokens in secret store (structured)
316
318
  if (options.store) {
317
- await options.store.set(`${provider}_access_token`, json.access_token);
318
- if (json.refresh_token) {
319
- await options.store.set(
320
- `${provider}_refresh_token`,
321
- json.refresh_token,
322
- );
323
- }
319
+ const obtainedAt = Date.now();
320
+ await setOAuthTokens(
321
+ provider,
322
+ {
323
+ accessToken: json.access_token,
324
+ refreshToken: json.refresh_token,
325
+ expiresAt: json.expires_in
326
+ ? obtainedAt + json.expires_in * 1000
327
+ : undefined,
328
+ scope: json.scope,
329
+ obtainedAt,
330
+ },
331
+ options.store,
332
+ );
324
333
  }
325
334
 
326
335
  return {
@@ -333,6 +342,149 @@ export async function exchangeCodeForTokens(
333
342
  };
334
343
  }
335
344
 
345
+ // ---------------------------------------------------------------------------
346
+ // Login orchestration (callback server + browser + token exchange)
347
+ // ---------------------------------------------------------------------------
348
+
349
+ /** Default loopback ports for supported providers. */
350
+ const DEFAULT_CALLBACK_PORTS: Record<string, number> = {
351
+ anthropic: 1456,
352
+ openai: 1455,
353
+ };
354
+
355
+ /**
356
+ * Return the default callback port for a provider, or 0 (OS-assigned).
357
+ */
358
+ export function defaultCallbackPort(provider: string): number {
359
+ return DEFAULT_CALLBACK_PORTS[provider] ?? 0;
360
+ }
361
+
362
+ /**
363
+ * Resolve the client id for a provider from process env (or a provided env
364
+ * override). Returns undefined if the env var is not set.
365
+ */
366
+ export function resolveClientId(
367
+ provider: string,
368
+ env: NodeJS.ProcessEnv = process.env,
369
+ ): string | undefined {
370
+ const config = PKCE_CONFIGS[provider];
371
+ if (!config?.clientIdEnvVar) return undefined;
372
+ return env[config.clientIdEnvVar] ?? undefined;
373
+ }
374
+
375
+ export interface RunOAuthLoginOptions {
376
+ provider: string;
377
+ store: SecretStore;
378
+ /** Override client id (else resolved from env / PKCE_CONFIGS). */
379
+ clientId?: string;
380
+ /** Loopback port (defaults to provider default or 0). */
381
+ port?: number;
382
+ /** Wait timeout in ms (default 5 min). */
383
+ timeoutMs?: number;
384
+ /** Called with the authorize URL once built (test/display). */
385
+ onAuthorizeUrl?: (url: string) => void;
386
+ /** Best-effort browser open (non-throwing). */
387
+ openBrowser?: (url: string) => Promise<boolean> | boolean;
388
+ /** Injectable fetch (tests). */
389
+ fetch?: typeof globalThis.fetch;
390
+ /** Optional abort signal. */
391
+ signal?: AbortSignal;
392
+ }
393
+
394
+ export interface RunOAuthLoginResult {
395
+ provider: string;
396
+ redirectUri: string;
397
+ expiresAt?: number;
398
+ scope?: string;
399
+ }
400
+
401
+ /**
402
+ * Run the full OAuth PKCE login flow:
403
+ * 1. Start a local callback server on 127.0.0.1
404
+ * 2. Build the authorize URL with PKCE challenge + state
405
+ * 3. Notify caller (onAuthorizeUrl) and attempt browser open
406
+ * 4. Wait for the provider callback (code + state)
407
+ * 5. Exchange code for tokens
408
+ * 6. Store structured tokens in the secret store
409
+ * 7. Clean up the server
410
+ */
411
+ export async function runOAuthLogin(
412
+ opts: RunOAuthLoginOptions,
413
+ ): Promise<RunOAuthLoginResult> {
414
+ const config = PKCE_CONFIGS[opts.provider];
415
+ if (!config) {
416
+ throw new Error(
417
+ `OAuth not supported for provider: ${opts.provider}. ` +
418
+ `OAuth is only available for anthropic and openai providers.`,
419
+ );
420
+ }
421
+
422
+ const clientId =
423
+ opts.clientId ?? resolveClientId(opts.provider) ?? undefined;
424
+ if (!clientId) {
425
+ const envHint =
426
+ config.clientIdEnvVar
427
+ ? `Set the ${config.clientIdEnvVar} environment variable or pass it explicitly.`
428
+ : "A client id is required for this provider.";
429
+ throw new Error(
430
+ `Client ID required for ${opts.provider} OAuth. ${envHint}`,
431
+ );
432
+ }
433
+
434
+ // 1. Start callback server
435
+ const port = opts.port ?? defaultCallbackPort(opts.provider);
436
+ const server = await startCallbackServer(port);
437
+
438
+ try {
439
+ const redirectUri = server.url;
440
+
441
+ // 2. Build the authorize URL with PKCE
442
+ const flow = await startOAuthFlow(opts.provider, {
443
+ redirectUri,
444
+ clientId,
445
+ });
446
+
447
+ // 3. Notify and attempt to open browser
448
+ opts.onAuthorizeUrl?.(flow.authorizeUrl);
449
+ if (opts.openBrowser) {
450
+ try {
451
+ await opts.openBrowser(flow.authorizeUrl);
452
+ } catch {
453
+ // Best-effort only.
454
+ }
455
+ }
456
+
457
+ // 4. Wait for callback (validates expected state)
458
+ const { code } = await server.waitForCode({
459
+ expectedState: flow.state,
460
+ timeoutMs: opts.timeoutMs,
461
+ signal: opts.signal,
462
+ });
463
+
464
+ // 5. Exchange code for tokens (also stores them)
465
+ const tokens = await exchangeCodeForTokens(opts.provider, code, flow.state, {
466
+ store: opts.store,
467
+ fetch: opts.fetch,
468
+ });
469
+
470
+ // 6. Tokens are already stored by exchangeCodeForTokens.
471
+ // Compute expiresAt from expiresIn for the result.
472
+ const expiresAt = tokens.expiresIn
473
+ ? Date.now() + tokens.expiresIn * 1000
474
+ : undefined;
475
+
476
+ return {
477
+ provider: opts.provider,
478
+ redirectUri,
479
+ expiresAt,
480
+ scope: tokens.scope,
481
+ };
482
+ } finally {
483
+ // 7. Clean up
484
+ await server.close();
485
+ }
486
+ }
487
+
336
488
  /**
337
489
  * Refresh an OAuth token using a refresh token.
338
490
  * Updates the stored tokens on success.
@@ -392,11 +544,21 @@ export async function refreshTokens(
392
544
  );
393
545
  }
394
546
 
395
- // Update stored tokens
396
- await store.set(`${provider}_access_token`, json.access_token);
397
- if (json.refresh_token) {
398
- await store.set(`${provider}_refresh_token`, json.refresh_token);
399
- }
547
+ // Update stored tokens (structured)
548
+ const obtainedAt = Date.now();
549
+ await setOAuthTokens(
550
+ provider,
551
+ {
552
+ accessToken: json.access_token,
553
+ refreshToken: json.refresh_token,
554
+ expiresAt: json.expires_in
555
+ ? obtainedAt + json.expires_in * 1000
556
+ : undefined,
557
+ scope: json.scope,
558
+ obtainedAt,
559
+ },
560
+ store,
561
+ );
400
562
 
401
563
  return {
402
564
  accessToken: new Redacted(json.access_token),
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Tests for the local OAuth callback server.
3
+ * No real OAuth: we hit the callback URL directly over HTTP.
4
+ */
5
+ import { describe, expect, it } from "vitest";
6
+ import { startCallbackServer } from "./oauth-server.js";
7
+
8
+ describe("startCallbackServer", () => {
9
+ it("binds to 127.0.0.1 and exposes a localhost redirect URL", async () => {
10
+ const server = await startCallbackServer(0);
11
+ try {
12
+ expect(server.url).toContain("127.0.0.1");
13
+ expect(server.url).toMatch(/^http:\/\/127\.0\.0\.1:\d+\/callback$/);
14
+ expect(server.port).toBeGreaterThan(0);
15
+ } finally {
16
+ await server.close();
17
+ }
18
+ });
19
+
20
+ it("captures the authorization code from a matching callback", async () => {
21
+ const server = await startCallbackServer(0);
22
+ try {
23
+ const waitPromise = server.waitForCode({
24
+ expectedState: "state-abc",
25
+ timeoutMs: 2000,
26
+ });
27
+ const res = await fetch(`${server.url}?code=AUTHCODE123&state=state-abc`);
28
+ expect(res.status).toBe(200);
29
+ const body = await res.text();
30
+ expect(body).toMatch(/success|complete|authorized/i);
31
+
32
+ const result = await waitPromise;
33
+ expect(result.code).toBe("AUTHCODE123");
34
+ expect(result.state).toBe("state-abc");
35
+ } finally {
36
+ await server.close();
37
+ }
38
+ });
39
+
40
+ it("rejects a callback with a mismatched state", async () => {
41
+ const server = await startCallbackServer(0);
42
+ try {
43
+ const waitPromise = server.waitForCode({
44
+ expectedState: "expected-state",
45
+ timeoutMs: 2000,
46
+ });
47
+ // Attach the rejection handler before triggering the callback so the
48
+ // rejection is never momentarily unhandled.
49
+ const assertion = expect(waitPromise).rejects.toThrow(/state/i);
50
+ await fetch(`${server.url}?code=CODE&state=wrong-state`);
51
+ await assertion;
52
+ } finally {
53
+ await server.close();
54
+ }
55
+ });
56
+
57
+ it("rejects when no callback arrives before the timeout", async () => {
58
+ const server = await startCallbackServer(0);
59
+ try {
60
+ await expect(
61
+ server.waitForCode({ timeoutMs: 50 }),
62
+ ).rejects.toThrow(/timed out|timeout/i);
63
+ } finally {
64
+ await server.close();
65
+ }
66
+ });
67
+
68
+ it("returns an error page for callbacks missing the code", async () => {
69
+ const server = await startCallbackServer(0);
70
+ try {
71
+ const res = await fetch(`${server.url}?state=only-state`);
72
+ expect(res.status).toBe(400);
73
+ } finally {
74
+ await server.close();
75
+ }
76
+ });
77
+
78
+ it("close is idempotent and releases the port", async () => {
79
+ const server = await startCallbackServer(0);
80
+ await server.close();
81
+ await expect(server.close()).resolves.toBeUndefined();
82
+ });
83
+ });
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Local OAuth callback server for OffRouter's live PKCE login flow.
3
+ *
4
+ * Binds to 127.0.0.1 ONLY (never 0.0.0.0) so the provider can redirect back
5
+ * to this machine without exposing the callback to the network. The server
6
+ * captures the authorization `code` + `state` from the redirect and hands them
7
+ * to the caller; it never touches tokens, secrets, or the network beyond the
8
+ * single loopback HTTP socket.
9
+ */
10
+ import { createServer, type Server, type IncomingMessage, type ServerResponse } from "node:http";
11
+ import { URL } from "node:url";
12
+
13
+ /** Loopback-only hostname. Hardcoded to prevent accidental 0.0.0.0 binds. */
14
+ const LOOPBACK_HOST = "127.0.0.1";
15
+
16
+ /** Path the provider redirects to (http://127.0.0.1:PORT/callback). */
17
+ const CALLBACK_PATH = "/callback";
18
+
19
+ export interface CallbackServerOptions {
20
+ /**
21
+ * State value expected from the provider. Validated in waitForCode against
22
+ * the state returned by the redirect; a mismatch rejects the promise.
23
+ * May also be supplied per-call to waitForCode.
24
+ */
25
+ expectedState?: string;
26
+ }
27
+
28
+ export interface WaitForCodeOptions {
29
+ /** Overrides the server-level expectedState for this call. */
30
+ expectedState?: string;
31
+ /** How long to wait for a callback before rejecting (default 5 minutes). */
32
+ timeoutMs?: number;
33
+ /** Optional abort signal to cancel the wait early. */
34
+ signal?: AbortSignal;
35
+ }
36
+
37
+ export interface CallbackCode {
38
+ code: string;
39
+ state: string;
40
+ }
41
+
42
+ export interface CallbackServerHandle {
43
+ /** Full redirect_uri to register with the provider (http://127.0.0.1:PORT/callback). */
44
+ url: string;
45
+ /** Actual bound port (useful when 0 was requested). */
46
+ port: number;
47
+ /** Resolve with {code, state} on a valid callback; reject on timeout/state mismatch. */
48
+ waitForCode(options?: WaitForCodeOptions): Promise<CallbackCode>;
49
+ /** Stop listening and free the port. Idempotent. */
50
+ close(): Promise<void>;
51
+ }
52
+
53
+ /**
54
+ * Start a loopback HTTP server that captures an OAuth authorization code.
55
+ * Pass port 0 to let the OS choose a free port (read handle.port afterwards).
56
+ */
57
+ export function startCallbackServer(
58
+ port: number,
59
+ options: CallbackServerOptions = {},
60
+ ): Promise<CallbackServerHandle> {
61
+ return new Promise((resolve, reject) => {
62
+ const serverLevelState = options.expectedState;
63
+
64
+ // Deferred for the first valid callback (has a `code` param).
65
+ let captured: CallbackCode | null = null;
66
+ const waiters: Array<{
67
+ resolve: (value: CallbackCode) => void;
68
+ reject: (error: Error) => void;
69
+ }> = [];
70
+
71
+ const httpServer: Server = createServer(
72
+ (req: IncomingMessage, res: ServerResponse) => {
73
+ handleCallback(req, res);
74
+ },
75
+ );
76
+
77
+ function handleCallback(req: IncomingMessage, res: ServerResponse): void {
78
+ // Only respond to GET on the callback path; everything else 404s.
79
+ if (req.method !== "GET") {
80
+ res.writeHead(405, { "content-type": "text/plain; charset=utf-8" });
81
+ res.end("Method Not Allowed");
82
+ return;
83
+ }
84
+
85
+ let parsedUrl: URL;
86
+ try {
87
+ parsedUrl = new URL(req.url ?? "/", `http://${LOOPBACK_HOST}`);
88
+ } catch {
89
+ res.writeHead(400, { "content-type": "text/plain; charset=utf-8" });
90
+ res.end("Bad Request");
91
+ return;
92
+ }
93
+
94
+ const pathname = parsedUrl.pathname;
95
+ const code = parsedUrl.searchParams.get("code");
96
+ const state = parsedUrl.searchParams.get("state");
97
+ const error = parsedUrl.searchParams.get("error");
98
+
99
+ // OAuth error from the provider (e.g. user denied consent).
100
+ if (error) {
101
+ res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
102
+ res.end(authorizationDeniedPage(error));
103
+ return;
104
+ }
105
+
106
+ if (pathname !== CALLBACK_PATH) {
107
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
108
+ res.end("Not Found");
109
+ return;
110
+ }
111
+
112
+ // Missing code -> provider returned nothing usable.
113
+ if (!code) {
114
+ res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
115
+ res.end(missingCodePage());
116
+ return;
117
+ }
118
+
119
+ // Capture the first valid callback exactly once.
120
+ const callback: CallbackCode = { code, state: state ?? "" };
121
+ if (!captured) {
122
+ captured = callback;
123
+ // Resolve any pending waiters that accept this callback.
124
+ const pending = waiters.splice(0, waiters.length);
125
+ for (const w of pending) {
126
+ w.resolve(callback);
127
+ }
128
+ }
129
+
130
+ // Always show the user a friendly page; security validation happens in
131
+ // waitForCode (state may not be known at request time).
132
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
133
+ res.end(successPage());
134
+ }
135
+
136
+ httpServer.on("error", (err: NodeJS.ErrnoException) => {
137
+ reject(
138
+ new Error(
139
+ `Failed to start OAuth callback server on 127.0.0.1:${port}: ${err.message}`,
140
+ { cause: err },
141
+ ),
142
+ );
143
+ });
144
+
145
+ httpServer.listen(port, LOOPBACK_HOST, () => {
146
+ const address = httpServer.address();
147
+ const boundPort =
148
+ address && typeof address === "object" ? address.port : port;
149
+ const redirectUrl = `http://${LOOPBACK_HOST}:${boundPort}${CALLBACK_PATH}`;
150
+
151
+ const handle: CallbackServerHandle = {
152
+ url: redirectUrl,
153
+ port: boundPort,
154
+ waitForCode(waitOpts: WaitForCodeOptions = {}): Promise<CallbackCode> {
155
+ const expected =
156
+ waitOpts.expectedState !== undefined
157
+ ? waitOpts.expectedState
158
+ : serverLevelState;
159
+ const timeoutMs = waitOpts.timeoutMs ?? 5 * 60 * 1000;
160
+
161
+ return new Promise<CallbackCode>((resolveWait, rejectWait) => {
162
+ let settled = false;
163
+
164
+ const settle = (
165
+ outcome: CallbackCode | Error,
166
+ isReject: boolean,
167
+ ): void => {
168
+ if (settled) return;
169
+ settled = true;
170
+ clearTimeout(timer);
171
+ waitOpts.signal?.removeEventListener("abort", onAbort);
172
+ if (isReject) {
173
+ rejectWait(outcome as Error);
174
+ } else {
175
+ resolveWait(outcome as CallbackCode);
176
+ }
177
+ };
178
+
179
+ const onAbort = (): void => {
180
+ settle(
181
+ new Error("OAuth callback wait was aborted."),
182
+ true,
183
+ );
184
+ };
185
+
186
+ const timer = setTimeout(() => {
187
+ settle(new Error("OAuth login timed out waiting for callback."), true);
188
+ }, timeoutMs);
189
+
190
+ if (waitOpts.signal?.aborted) {
191
+ settle(new Error("OAuth callback wait was aborted."), true);
192
+ return;
193
+ }
194
+ waitOpts.signal?.addEventListener("abort", onAbort);
195
+
196
+ const resolveWith = (callback: CallbackCode): void => {
197
+ if (expected !== undefined && callback.state !== expected) {
198
+ settle(
199
+ new Error(
200
+ `OAuth state mismatch: expected "${redactState(expected)}" ` +
201
+ `but received "${redactState(callback.state)}". ` +
202
+ `This may indicate a CSRF attempt or a stale login tab.`,
203
+ ),
204
+ true,
205
+ );
206
+ return;
207
+ }
208
+ settle(callback, false);
209
+ };
210
+
211
+ if (captured) {
212
+ resolveWith(captured);
213
+ return;
214
+ }
215
+
216
+ waiters.push({ resolve: resolveWith, reject: rejectWait });
217
+ });
218
+ },
219
+ close(): Promise<void> {
220
+ return new Promise<void>((resolveClose) => {
221
+ httpServer.close(() => resolveClose());
222
+ // close() waits for in-flight connections; closeAllConnections is
223
+ // available on Node 18+ and speeds shutdown without dropping the
224
+ // response the user's browser is mid-receiving.
225
+ // (Not called here to keep the success page delivery intact.)
226
+ });
227
+ },
228
+ };
229
+
230
+ resolve(handle);
231
+ });
232
+ });
233
+ }
234
+
235
+ // ---------------------------------------------------------------------------
236
+ // Static HTML pages (ASCII-only, no secret material)
237
+ // ---------------------------------------------------------------------------
238
+
239
+ function successPage(): string {
240
+ return [
241
+ "<!doctype html>",
242
+ "<html lang=\"en\">",
243
+ "<head><meta charset=\"utf-8\"><title>OffRouter login</title></head>",
244
+ "<body>",
245
+ "<h1>OffRouter login authorized</h1>",
246
+ "<p>You can close this tab and return to your terminal.</p>",
247
+ "</body>",
248
+ "</html>",
249
+ "",
250
+ ].join("\n");
251
+ }
252
+
253
+ function authorizationDeniedPage(error: string): string {
254
+ const safe = escapeHtml(error);
255
+ return [
256
+ "<!doctype html>",
257
+ "<html lang=\"en\">",
258
+ "<head><meta charset=\"utf-8\"><title>OffRouter login</title></head>",
259
+ "<body>",
260
+ "<h1>OffRouter login not completed</h1>",
261
+ `<p>The provider reported: ${safe}</p>`,
262
+ "<p>You can close this tab and try again from your terminal.</p>",
263
+ "</body>",
264
+ "</html>",
265
+ "",
266
+ ].join("\n");
267
+ }
268
+
269
+ function missingCodePage(): string {
270
+ return [
271
+ "<!doctype html>",
272
+ "<html lang=\"en\">",
273
+ "<head><meta charset=\"utf-8\"><title>OffRouter login</title></head>",
274
+ "<body>",
275
+ "<h1>OffRouter login incomplete</h1>",
276
+ "<p>The callback did not include an authorization code.</p>",
277
+ "</body>",
278
+ "</html>",
279
+ "",
280
+ ].join("\n");
281
+ }
282
+
283
+ function escapeHtml(value: string): string {
284
+ return value
285
+ .replace(/&/g, "&amp;")
286
+ .replace(/</g, "&lt;")
287
+ .replace(/>/g, "&gt;")
288
+ .replace(/"/g, "&quot;")
289
+ .replace(/'/g, "&#39;");
290
+ }
291
+
292
+ /** Truncate a state token for error messages so it is never echoed in full. */
293
+ function redactState(state: string): string {
294
+ if (state.length <= 8) return "***";
295
+ return `${state.slice(0, 4)}...${state.slice(-4)}`;
296
+ }
@@ -168,6 +168,22 @@ enabled = true
168
168
  expect(allowed.allowed).toHaveLength(1);
169
169
  });
170
170
 
171
+ it("parses policy.near_limit_threshold into policy.nearLimitThreshold", async () => {
172
+ const home = await trackDir(makeHome);
173
+ await writeFile(
174
+ join(home, "config.toml"),
175
+ `allowlisted_profiles = ["claude-personal"]
176
+
177
+ [policy]
178
+ near_limit_threshold = 0.15
179
+ `,
180
+ "utf8",
181
+ );
182
+
183
+ const config = await loadConfig({ env: { OFFROUTER_HOME: home } });
184
+ expect(config.policy.nearLimitThreshold).toBe(0.15);
185
+ });
186
+
171
187
  it("loads named profile overlays from profiles/*.toml", async () => {
172
188
  const home = await trackDir(makeHome);
173
189
  await writeFile(
package/src/config.ts CHANGED
@@ -111,6 +111,7 @@ const PolicySectionShape = {
111
111
  allow_api_key_fallback: z.boolean().optional(),
112
112
  allow_third_party_subscription_adapters: z.boolean().optional(),
113
113
  denied_providers: z.array(z.string().min(1)).optional(),
114
+ near_limit_threshold: z.number().min(0).max(1).optional(),
114
115
  } satisfies z.ZodRawShape;
115
116
 
116
117
  const RootConfigShape = {
@@ -368,6 +369,9 @@ function mergeRootInto(
368
369
  target.policy.allowThirdPartySubscriptionAdapters =
369
370
  root.policy.allow_third_party_subscription_adapters;
370
371
  }
372
+ if (root.policy.near_limit_threshold !== undefined) {
373
+ target.policy.nearLimitThreshold = root.policy.near_limit_threshold;
374
+ }
371
375
  if (root.policy.denied_providers) {
372
376
  target.policy.deniedProviders = uniqueStrings([
373
377
  ...(target.policy.deniedProviders ?? []),
package/src/index.ts CHANGED
@@ -119,6 +119,13 @@ export type {
119
119
  } from "./audit.js";
120
120
 
121
121
  export { DEFAULT_NEAR_LIMIT_THRESHOLD, InMemoryStateStore, InMemoryUsageStore } from "./usage.js";
122
+ export {
123
+ FileUsageStore,
124
+ } from "./usage-file.js";
125
+ export type {
126
+ FileUsageStoreFileSystem,
127
+ FileUsageStoreOptions,
128
+ } from "./usage-file.js";
122
129
  export type {
123
130
  AccountUsageSnapshot,
124
131
  InMemoryUsageStoreOptions,
@@ -198,14 +205,21 @@ export type {
198
205
  // Auth module (credential chain, OAuth PKCE, keychain helpers)
199
206
  export {
200
207
  Redacted,
208
+ defaultCallbackPort,
209
+ deleteOAuthTokens,
201
210
  exchangeCodeForTokens,
202
211
  generateCodeChallenge,
203
212
  generateCodeVerifier,
213
+ getOAuthTokens,
204
214
  getProviderToken,
205
215
  isOAuthProvider,
216
+ isOAuthTokenExpired,
206
217
  PKCE_CONFIGS,
207
218
  refreshTokens,
219
+ resolveClientId,
208
220
  resolveCredential,
221
+ runOAuthLogin,
222
+ setOAuthTokens,
209
223
  setProviderToken,
210
224
  startOAuthFlow,
211
225
  } from "./auth/index.js";
@@ -215,10 +229,13 @@ export type {
215
229
  OAuthFlowResult,
216
230
  OAuthFlowState,
217
231
  OAuthProviderConfig,
232
+ OAuthStoredTokens,
218
233
  OAuthStartOptions,
219
234
  OAuthTokenOptions,
220
235
  OAuthTokenResponse,
221
236
  ResolveCredentialOptions,
237
+ RunOAuthLoginOptions,
238
+ RunOAuthLoginResult,
222
239
  } from "./auth/index.js";
223
240
 
224
241
  // OpenAI Responses-compatible local proxy