axauth 1.3.0 → 1.5.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.
@@ -73,6 +73,10 @@ interface RemoveOptions {
73
73
  interface TokenOptions {
74
74
  /** Provider ID for multi-provider agents (e.g., "anthropic", "openai") */
75
75
  provider?: string;
76
+ /** Skip auto-refresh even for expired tokens (default: false) */
77
+ skipRefresh?: boolean;
78
+ /** Timeout for refresh operation in ms (default: 10000) */
79
+ refreshTimeout?: number;
76
80
  }
77
81
  /** Capabilities that an adapter supports */
78
82
  interface AdapterCapabilities {
@@ -113,6 +117,13 @@ interface AuthAdapter {
113
117
  * Use {@link getAccessToken} to extract the token in a uniform way.
114
118
  */
115
119
  extractRawCredentials(): Credentials | undefined;
120
+ /**
121
+ * Extract raw credentials from a specific directory.
122
+ *
123
+ * Used by token refresh to read credentials from a temp directory.
124
+ * Returns undefined if no credentials found at that location.
125
+ */
126
+ extractRawCredentialsFromDirectory?(directory: string): Credentials | undefined;
116
127
  /**
117
128
  * Install credentials to storage.
118
129
  *
@@ -4,6 +4,7 @@
4
4
  * Supports OAuth via keychain (macOS) or file, and API key via env var.
5
5
  */
6
6
  import path from "node:path";
7
+ import { extractCredsFromDirectory } from "../extract-creds-from-directory.js";
7
8
  import { isMacOS } from "../keychain.js";
8
9
  import { resolveCustomDirectory } from "../validate-directories.js";
9
10
  import { AGENT_ID, checkAuth, deleteFileCreds, deleteKeychainCreds, extractRawCredentials, getDefaultCredsFilePath, saveFileCreds, saveKeychainCreds, } from "./claude-storage.js";
@@ -27,6 +28,15 @@ const claudeCodeAdapter = {
27
28
  return undefined;
28
29
  return { agent: AGENT_ID, ...result };
29
30
  },
31
+ extractRawCredentialsFromDirectory(directory) {
32
+ // Unwrap claudeAiOauth wrapper (matches loadFileCreds behavior)
33
+ return extractCredsFromDirectory(AGENT_ID, directory, CREDS_FILE_NAME, (file) => {
34
+ const data = file.claudeAiOauth;
35
+ return typeof data === "object" && data !== null
36
+ ? data
37
+ : undefined;
38
+ });
39
+ },
30
40
  installCredentials(creds, options) {
31
41
  // Resolve custom directory with validation
32
42
  const resolved = resolveCustomDirectory(AGENT_ID, options?.configDir, options?.dataDir);
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Supports API key and OAuth via keychain (macOS) or file.
5
5
  */
6
+ import { existsSync, readFileSync } from "node:fs";
6
7
  import path from "node:path";
7
8
  import { ensureDirectory } from "../file-storage.js";
8
9
  import { isMacOS } from "../keychain.js";
@@ -23,6 +24,23 @@ const codexAdapter = {
23
24
  },
24
25
  checkAuth,
25
26
  extractRawCredentials,
27
+ extractRawCredentialsFromDirectory(directory) {
28
+ const credentialsPath = path.join(directory, CREDS_FILE_NAME);
29
+ try {
30
+ if (!existsSync(credentialsPath))
31
+ return undefined;
32
+ const parsed = JSON.parse(readFileSync(credentialsPath, "utf8"));
33
+ if (typeof parsed !== "object" || parsed === null)
34
+ return undefined;
35
+ const data = parsed;
36
+ // Determine type from data structure
37
+ const type = data.api_key ? "api-key" : "oauth";
38
+ return { agent: AGENT_ID, type, data };
39
+ }
40
+ catch {
41
+ return undefined;
42
+ }
43
+ },
26
44
  installCredentials(creds, options) {
27
45
  // Resolve custom directory with validation
28
46
  const resolved = resolveCustomDirectory(AGENT_ID, options?.configDir, options?.dataDir);
@@ -5,6 +5,7 @@
5
5
  * Also supports GitHub CLI (`gh auth login`) as a fallback.
6
6
  */
7
7
  import path from "node:path";
8
+ import { extractCredsFromDirectory } from "../extract-creds-from-directory.js";
8
9
  import { ensureDirectory } from "../file-storage.js";
9
10
  import { isMacOS } from "../keychain.js";
10
11
  import { resolveCustomDirectory } from "../validate-directories.js";
@@ -48,6 +49,9 @@ const copilotAdapter = {
48
49
  data: { accessToken: result.token, _source: result.source },
49
50
  };
50
51
  },
52
+ extractRawCredentialsFromDirectory(directory) {
53
+ return extractCredsFromDirectory(AGENT_ID, directory, CREDS_FILE_NAME);
54
+ },
51
55
  installCredentials(creds, options) {
52
56
  // Resolve custom directory with validation
53
57
  const resolved = resolveCustomDirectory(AGENT_ID, options?.configDir, options?.dataDir);
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Supports OAuth via keychain (macOS) or file. API key auth is env-var only.
5
5
  */
6
+ import { extractCredsFromDirectory } from "../extract-creds-from-directory.js";
6
7
  import { findCredentials } from "./gemini-auth-check.js";
7
8
  import { installOAuthCredentials, removeGeminiCredentials, } from "./gemini-install.js";
8
9
  const AGENT_ID = "gemini";
@@ -48,6 +49,9 @@ const geminiAdapter = {
48
49
  },
49
50
  };
50
51
  },
52
+ extractRawCredentialsFromDirectory(directory) {
53
+ return extractCredsFromDirectory(AGENT_ID, directory, "oauth_creds.json");
54
+ },
51
55
  installCredentials(creds, options) {
52
56
  if (creds.type === "api-key") {
53
57
  return {
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
11
11
  import path from "node:path";
12
+ import { extractCredsFromDirectory } from "../extract-creds-from-directory.js";
12
13
  import { resolveAgentDataDirectory } from "axshared";
13
14
  import { resolveCustomDirectory } from "../validate-directories.js";
14
15
  import { extractTokenFromEntry, OpenCodeAuth } from "./opencode-schema.js";
@@ -82,6 +83,10 @@ const opencodeAdapter = {
82
83
  }
83
84
  return undefined;
84
85
  },
86
+ extractRawCredentialsFromDirectory(directory) {
87
+ // Only return if there's at least one provider
88
+ return extractCredsFromDirectory(AGENT_ID, directory, CREDS_FILE_NAME, (data) => (Object.keys(data).length > 0 ? data : undefined));
89
+ },
85
90
  installCredentials(creds, options) {
86
91
  // Resolve custom directory with validation (OpenCode supports separation)
87
92
  const resolved = resolveCustomDirectory(AGENT_ID, options?.configDir, options?.dataDir);
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Common utility for extracting credentials from a directory.
3
+ */
4
+ import type { AgentCli } from "axshared";
5
+ import type { Credentials } from "./types.js";
6
+ /**
7
+ * Extract credentials from a JSON file in a directory.
8
+ *
9
+ * @param agentId - The agent ID
10
+ * @param directory - Directory to read from
11
+ * @param fileName - Name of the credentials file
12
+ * @param transform - Optional transform to apply to file contents
13
+ * @returns Credentials or undefined if not found
14
+ */
15
+ declare function extractCredsFromDirectory(agentId: AgentCli, directory: string, fileName: string, transform?: (data: Record<string, unknown>) => Record<string, unknown> | undefined): Credentials | undefined;
16
+ export { extractCredsFromDirectory };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Common utility for extracting credentials from a directory.
3
+ */
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import path from "node:path";
6
+ /**
7
+ * Extract credentials from a JSON file in a directory.
8
+ *
9
+ * @param agentId - The agent ID
10
+ * @param directory - Directory to read from
11
+ * @param fileName - Name of the credentials file
12
+ * @param transform - Optional transform to apply to file contents
13
+ * @returns Credentials or undefined if not found
14
+ */
15
+ function extractCredsFromDirectory(agentId, directory, fileName, transform) {
16
+ const credentialsPath = path.join(directory, fileName);
17
+ try {
18
+ if (!existsSync(credentialsPath))
19
+ return undefined;
20
+ const fileContent = JSON.parse(readFileSync(credentialsPath, "utf8"));
21
+ // JSON.parse can return null, primitives, or arrays - we need an object
22
+ if (typeof fileContent !== "object" ||
23
+ fileContent === null ||
24
+ Array.isArray(fileContent)) {
25
+ return undefined;
26
+ }
27
+ const record = fileContent;
28
+ const data = transform ? transform(record) : record;
29
+ if (!data)
30
+ return undefined;
31
+ return { agent: agentId, type: "oauth", data };
32
+ }
33
+ catch {
34
+ return undefined;
35
+ }
36
+ }
37
+ export { extractCredsFromDirectory };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Token and credential expiry check utilities.
3
+ */
4
+ /**
5
+ * Check if a JWT token is expired.
6
+ *
7
+ * Decodes the JWT payload and checks the `exp` claim against current time.
8
+ *
9
+ * @param token - The JWT token string
10
+ * @param bufferSeconds - Buffer before actual expiry (default: 60s)
11
+ * @returns true if expired, false if valid, undefined if not a valid JWT
12
+ */
13
+ declare function isTokenExpired(token: string, bufferSeconds?: number): boolean | undefined;
14
+ /**
15
+ * Check if credentials have expired using explicit timestamp fields.
16
+ *
17
+ * Checks common OAuth credential expiry fields like `expiry_date` (Google)
18
+ * or `expires_at` (generic). Useful for opaque tokens that aren't JWTs.
19
+ *
20
+ * @param data - The credential data object
21
+ * @param bufferSeconds - Buffer before actual expiry (default: 60s)
22
+ * @returns true if expired, false if valid, undefined if no expiry field found
23
+ */
24
+ declare function isCredentialExpired(data: Record<string, unknown>, bufferSeconds?: number): boolean | undefined;
25
+ export { isCredentialExpired, isTokenExpired };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Token and credential expiry check utilities.
3
+ */
4
+ /**
5
+ * Timestamp threshold to distinguish seconds from milliseconds.
6
+ *
7
+ * Values below this are assumed to be Unix timestamps in seconds,
8
+ * values above are assumed to be milliseconds.
9
+ *
10
+ * 10 billion seconds ≈ year 2286, so any current timestamp in
11
+ * seconds will be below this threshold.
12
+ */
13
+ const SECONDS_THRESHOLD = 10_000_000_000;
14
+ /**
15
+ * Check if a JWT token is expired.
16
+ *
17
+ * Decodes the JWT payload and checks the `exp` claim against current time.
18
+ *
19
+ * @param token - The JWT token string
20
+ * @param bufferSeconds - Buffer before actual expiry (default: 60s)
21
+ * @returns true if expired, false if valid, undefined if not a valid JWT
22
+ */
23
+ function isTokenExpired(token, bufferSeconds = 60) {
24
+ const parts = token.split(".");
25
+ if (parts.length !== 3)
26
+ return undefined; // Not a JWT
27
+ const payloadPart = parts[1];
28
+ if (!payloadPart)
29
+ return undefined;
30
+ try {
31
+ // Convert base64url to standard base64 (JWTs use base64url encoding)
32
+ const base64 = payloadPart.replaceAll("-", "+").replaceAll("_", "/");
33
+ const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "=");
34
+ const payload = JSON.parse(atob(padded));
35
+ const exp = payload.exp;
36
+ if (typeof exp !== "number")
37
+ return undefined; // No exp claim
38
+ const nowSeconds = Date.now() / 1000;
39
+ return nowSeconds > exp - bufferSeconds;
40
+ }
41
+ catch {
42
+ return undefined; // Invalid JWT
43
+ }
44
+ }
45
+ /**
46
+ * Check if credentials have expired using explicit timestamp fields.
47
+ *
48
+ * Checks common OAuth credential expiry fields like `expiry_date` (Google)
49
+ * or `expires_at` (generic). Useful for opaque tokens that aren't JWTs.
50
+ *
51
+ * @param data - The credential data object
52
+ * @param bufferSeconds - Buffer before actual expiry (default: 60s)
53
+ * @returns true if expired, false if valid, undefined if no expiry field found
54
+ */
55
+ function isCredentialExpired(data, bufferSeconds = 60) {
56
+ // Check common expiry field names (in order of preference)
57
+ // expiry_date: Google OAuth (milliseconds)
58
+ // expires_at: Generic OAuth (seconds or milliseconds)
59
+ const expiryDate = data.expiry_date;
60
+ const expiresAt = data.expires_at;
61
+ let expiryTimestampMs;
62
+ if (typeof expiryDate === "number") {
63
+ // Google uses milliseconds
64
+ expiryTimestampMs = expiryDate;
65
+ }
66
+ else if (typeof expiresAt === "number") {
67
+ // Could be seconds or milliseconds - heuristic based on SECONDS_THRESHOLD
68
+ expiryTimestampMs =
69
+ expiresAt < SECONDS_THRESHOLD ? expiresAt * 1000 : expiresAt;
70
+ }
71
+ if (expiryTimestampMs === undefined)
72
+ return undefined;
73
+ const nowMs = Date.now();
74
+ const bufferMs = bufferSeconds * 1000;
75
+ return nowMs > expiryTimestampMs - bufferMs;
76
+ }
77
+ export { isCredentialExpired, isTokenExpired };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Token refresh via agent subprocess.
3
+ *
4
+ * Refreshes credentials by running the agent in an isolated temp config,
5
+ * triggering its internal auth refresh mechanism.
6
+ */
7
+ import type { Credentials } from "./types.js";
8
+ /** Options for credential refresh */
9
+ interface RefreshOptions {
10
+ /** Timeout in ms (default: 30000) */
11
+ timeout?: number;
12
+ /** Provider for multi-provider agents (OpenCode) */
13
+ provider?: string;
14
+ }
15
+ /** Result of a credential refresh attempt */
16
+ type RefreshResult = {
17
+ ok: true;
18
+ credentials: Credentials;
19
+ } | {
20
+ ok: false;
21
+ error: string;
22
+ };
23
+ /**
24
+ * Refresh credentials by spawning the agent briefly.
25
+ *
26
+ * Creates an isolated temp config directory, installs credentials there,
27
+ * and runs the agent with a minimal prompt to trigger its internal refresh.
28
+ *
29
+ * @param creds - The credentials to refresh (must contain refresh_token for OAuth)
30
+ * @param options - Refresh options (timeout, provider for multi-provider agents)
31
+ * @returns RefreshResult with new credentials or error
32
+ */
33
+ declare function refreshCredentials(creds: Credentials, options?: RefreshOptions): Promise<RefreshResult>;
34
+ /** Result of refresh and persist operation */
35
+ type RefreshAndPersistResult = {
36
+ ok: true;
37
+ credentials: Credentials;
38
+ } | {
39
+ ok: false;
40
+ staleCredentials: Credentials;
41
+ };
42
+ /**
43
+ * Refresh credentials and persist to original storage.
44
+ *
45
+ * Higher-level function that:
46
+ * 1. Refreshes credentials via agent subprocess
47
+ * 2. Preserves _source marker from original credentials
48
+ * 3. Persists refreshed credentials to original storage location
49
+ *
50
+ * @param creds - Original credentials with _source marker
51
+ * @param installFunction - Function to install credentials (avoids circular import)
52
+ * @param options - Refresh options
53
+ * @returns Refreshed credentials or original credentials on failure
54
+ */
55
+ declare function refreshAndPersist(creds: Credentials, installFunction: (c: Credentials) => {
56
+ ok: boolean;
57
+ message: string;
58
+ }, options?: RefreshOptions): Promise<RefreshAndPersistResult>;
59
+ export { refreshAndPersist, refreshCredentials };
60
+ export type { RefreshAndPersistResult, RefreshOptions, RefreshResult };
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Token refresh via agent subprocess.
3
+ *
4
+ * Refreshes credentials by running the agent in an isolated temp config,
5
+ * triggering its internal auth refresh mechanism.
6
+ */
7
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import path from "node:path";
10
+ import { buildAgentRuntimeEnvironment, getAgent, getAgentConfigSubdirectory, } from "axshared";
11
+ import { spawnAgent } from "./spawn-agent.js";
12
+ /** Default timeout for refresh operations in milliseconds */
13
+ const DEFAULT_REFRESH_TIMEOUT_MS = 30_000;
14
+ /**
15
+ * Write agent-specific config to force file-based storage.
16
+ *
17
+ * This prevents the agent from using keychain credentials,
18
+ * ensuring it reads/writes from our temp directory.
19
+ */
20
+ function writeForceFileConfig(agentId, temporaryDirectory) {
21
+ switch (agentId) {
22
+ case "codex": {
23
+ // Codex uses config.toml with cli_auth_credentials_store setting
24
+ writeFileSync(path.join(temporaryDirectory, "config.toml"), 'cli_auth_credentials_store = "file"\nmcp_oauth_credentials_store = "file"\n');
25
+ break;
26
+ }
27
+ case "copilot": {
28
+ // Copilot uses JSON config with store_token_plaintext flag
29
+ writeFileSync(path.join(temporaryDirectory, "config.json"), JSON.stringify({ store_token_plaintext: true }, undefined, 2));
30
+ break;
31
+ }
32
+ // Claude: keychain service name includes hash of CLAUDE_CONFIG_DIR,
33
+ // so unique temp dir = unique keychain entry that won't exist
34
+ // Gemini: file storage behavior is controlled via the GEMINI_FORCE_FILE_STORAGE
35
+ // environment variable, which is set by buildAgentRuntimeEnvironment and
36
+ // later included in fullEnvironment when spawning the agent.
37
+ // OpenCode: file-only by design, no action needed
38
+ }
39
+ }
40
+ /**
41
+ * Refresh credentials by spawning the agent briefly.
42
+ *
43
+ * Creates an isolated temp config directory, installs credentials there,
44
+ * and runs the agent with a minimal prompt to trigger its internal refresh.
45
+ *
46
+ * @param creds - The credentials to refresh (must contain refresh_token for OAuth)
47
+ * @param options - Refresh options (timeout, provider for multi-provider agents)
48
+ * @returns RefreshResult with new credentials or error
49
+ */
50
+ async function refreshCredentials(creds, options) {
51
+ const timeout = options?.timeout ?? DEFAULT_REFRESH_TIMEOUT_MS;
52
+ const agent = getAgent(creds.agent);
53
+ // 1. Create temp config directory
54
+ const temporaryDirectory = mkdtempSync(path.join(tmpdir(), `axauth-refresh-${creds.agent}-`));
55
+ try {
56
+ // 2. Determine where credentials should be installed
57
+ // Some agents (Gemini, Copilot, OpenCode) require a subdirectory structure
58
+ const subdirectory = getAgentConfigSubdirectory(creds.agent);
59
+ const credentialsDirectory = subdirectory
60
+ ? path.join(temporaryDirectory, subdirectory)
61
+ : temporaryDirectory;
62
+ // 3. Ensure credentials directory exists before writing config
63
+ mkdirSync(credentialsDirectory, { recursive: true });
64
+ // 4. Write agent-specific config to force file storage
65
+ writeForceFileConfig(creds.agent, credentialsDirectory);
66
+ // 5. Install credentials to the appropriate directory
67
+ // Import dynamically to avoid circular dependency
68
+ const { installCredentials } = await import("./registry.js");
69
+ const installResult = installCredentials(creds, {
70
+ configDir: credentialsDirectory,
71
+ dataDir: credentialsDirectory,
72
+ });
73
+ if (!installResult.ok) {
74
+ return {
75
+ ok: false,
76
+ error: `Failed to install credentials: ${installResult.message}`,
77
+ };
78
+ }
79
+ // 6. Build runtime environment
80
+ // Pass the full credentialsDirectory (which may include an agent-specific
81
+ // subdirectory, e.g. "~/.gemini/"); buildAgentRuntimeEnvironment will
82
+ // derive the parent directory internally for agents that use subdirectories.
83
+ const runtimeEnvironment = buildAgentRuntimeEnvironment(creds.agent, credentialsDirectory);
84
+ const fullEnvironment = {
85
+ ...process.env,
86
+ ...runtimeEnvironment,
87
+ };
88
+ // 7. Build CLI arguments using simplePromptArguments from agent
89
+ const cliArguments = agent.simplePromptArguments("ping", {
90
+ provider: options?.provider,
91
+ });
92
+ // 8. Spawn agent with minimal prompt
93
+ const { timedOut } = await spawnAgent(agent.cli, cliArguments, fullEnvironment, timeout);
94
+ if (timedOut) {
95
+ return { ok: false, error: "Refresh timed out" };
96
+ }
97
+ // 9. Read refreshed credentials from temp directory
98
+ const { extractRawCredentialsFromDirectory } = await import("./registry.js");
99
+ const refreshedCredentials = extractRawCredentialsFromDirectory(creds.agent, credentialsDirectory);
100
+ if (!refreshedCredentials) {
101
+ return { ok: false, error: "No credentials found after refresh" };
102
+ }
103
+ return { ok: true, credentials: refreshedCredentials };
104
+ }
105
+ finally {
106
+ // 10. Clean up temp directory
107
+ try {
108
+ rmSync(temporaryDirectory, { recursive: true, force: true });
109
+ }
110
+ catch (error) {
111
+ const message = error instanceof Error ? error.message : String(error);
112
+ console.error(`Warning: Failed to clean up temp directory '${temporaryDirectory}': ${message}`);
113
+ }
114
+ }
115
+ }
116
+ /**
117
+ * Refresh credentials and persist to original storage.
118
+ *
119
+ * Higher-level function that:
120
+ * 1. Refreshes credentials via agent subprocess
121
+ * 2. Preserves _source marker from original credentials
122
+ * 3. Persists refreshed credentials to original storage location
123
+ *
124
+ * @param creds - Original credentials with _source marker
125
+ * @param installFunction - Function to install credentials (avoids circular import)
126
+ * @param options - Refresh options
127
+ * @returns Refreshed credentials or original credentials on failure
128
+ */
129
+ async function refreshAndPersist(creds, installFunction, options) {
130
+ const result = await refreshCredentials(creds, options);
131
+ if (!result.ok) {
132
+ console.error(`Warning: Token refresh failed: ${result.error}`);
133
+ return { ok: false, staleCredentials: creds };
134
+ }
135
+ // Preserve _source marker so credentials are saved to the same location
136
+ const refreshedCreds = {
137
+ ...result.credentials,
138
+ data: {
139
+ ...result.credentials.data,
140
+ _source: creds.data._source,
141
+ },
142
+ };
143
+ // Persist refreshed credentials back to storage
144
+ const installResult = installFunction(refreshedCreds);
145
+ if (!installResult.ok) {
146
+ console.error(`Warning: Failed to save refreshed credentials: ${installResult.message}`);
147
+ }
148
+ return { ok: true, credentials: refreshedCreds };
149
+ }
150
+ export { refreshAndPersist, refreshCredentials };
@@ -59,6 +59,16 @@ declare function checkAllAuth(): AuthStatus[];
59
59
  * if (creds) { await exportCredentials(creds); }
60
60
  */
61
61
  declare function extractRawCredentials(agentId: AgentCli): Credentials | undefined;
62
+ /**
63
+ * Extract raw credentials from a specific directory.
64
+ *
65
+ * Used by token refresh to read credentials from a temp directory.
66
+ * Returns undefined if no credentials found or adapter doesn't support it.
67
+ *
68
+ * @example
69
+ * const creds = extractRawCredentialsFromDirectory("claude", "/tmp/refresh-123");
70
+ */
71
+ declare function extractRawCredentialsFromDirectory(agentId: AgentCli, directory: string): Credentials | undefined;
62
72
  /**
63
73
  * Install credentials for an agent.
64
74
  *
@@ -82,29 +92,26 @@ declare function installCredentials(creds: Credentials, options?: InstallOptions
82
92
  */
83
93
  declare function removeCredentials(agentId: AgentCli, options?: RemoveOptions): OperationResult;
84
94
  /**
85
- * Get access token from credentials.
95
+ * Extract access token from credentials.
86
96
  *
87
- * For multi-provider agents (like opencode), use `options.provider`
88
- * to specify which provider's token to extract.
97
+ * For OAuth tokens, automatically refreshes if expired.
98
+ * For API keys, returns immediately (no expiry).
89
99
  *
90
100
  * @example
91
- * const token = getAccessToken(creds);
92
- * if (token) { headers.Authorization = `Bearer ${token}`; }
93
- *
94
- * // For multi-provider agents
95
- * const token = getAccessToken(creds, { provider: "anthropic" });
101
+ * const token = await getAccessToken(creds);
102
+ * const token = await getAccessToken(creds, { skipRefresh: true });
96
103
  */
97
- declare function getAccessToken(creds: Credentials, options?: TokenOptions): string | undefined;
104
+ declare function getAccessToken(creds: Credentials, options?: TokenOptions): Promise<string | undefined>;
98
105
  /**
99
- * Get access token for an agent (convenience function).
106
+ * Get access token for an agent by ID.
100
107
  *
101
- * Extracts credentials and returns the access token in one call.
108
+ * Convenience function that extracts credentials and gets the token.
109
+ * Automatically refreshes expired OAuth tokens.
102
110
  *
103
111
  * @example
104
- * const token = getAgentAccessToken("claude");
105
- * if (token) { ... }
112
+ * const token = await getAgentAccessToken("claude");
106
113
  */
107
- declare function getAgentAccessToken(agentId: AgentCli): string | undefined;
114
+ declare function getAgentAccessToken(agentId: AgentCli, options?: TokenOptions): Promise<string | undefined>;
108
115
  /**
109
116
  * Convert credentials to environment variables.
110
117
  *
@@ -153,4 +160,4 @@ declare function installCredentialsFromEnvironmentVariable(agent: AgentCli, opti
153
160
  error: string;
154
161
  }>;
155
162
  export type { InstallFromEnvironmentOptions };
156
- export { checkAllAuth, checkAuth, credentialsToEnvironment, extractRawCredentials, getAccessToken, getAdapter, getAgentAccessToken, getAllAdapters, getCapabilities, getCredentialsEnvironmentVariableName, installCredentials, installCredentialsFromEnvironmentVariable, removeCredentials, };
163
+ export { checkAllAuth, checkAuth, credentialsToEnvironment, extractRawCredentials, extractRawCredentialsFromDirectory, getAccessToken, getAdapter, getAgentAccessToken, getAllAdapters, getCapabilities, getCredentialsEnvironmentVariableName, installCredentials, installCredentialsFromEnvironmentVariable, removeCredentials, };
@@ -10,6 +10,8 @@ import { codexAdapter } from "./agents/codex.js";
10
10
  import { copilotAdapter } from "./agents/copilot.js";
11
11
  import { geminiAdapter } from "./agents/gemini.js";
12
12
  import { opencodeAdapter } from "./agents/opencode.js";
13
+ import { isCredentialExpired, isTokenExpired } from "./is-token-expired.js";
14
+ import { refreshAndPersist } from "./refresh-credentials.js";
13
15
  import { parseCredentials } from "./types.js";
14
16
  /** Registry of all adapters by agent ID */
15
17
  const ADAPTERS = {
@@ -83,6 +85,19 @@ function checkAllAuth() {
83
85
  function extractRawCredentials(agentId) {
84
86
  return ADAPTERS[agentId].extractRawCredentials();
85
87
  }
88
+ /**
89
+ * Extract raw credentials from a specific directory.
90
+ *
91
+ * Used by token refresh to read credentials from a temp directory.
92
+ * Returns undefined if no credentials found or adapter doesn't support it.
93
+ *
94
+ * @example
95
+ * const creds = extractRawCredentialsFromDirectory("claude", "/tmp/refresh-123");
96
+ */
97
+ function extractRawCredentialsFromDirectory(agentId, directory) {
98
+ const adapter = ADAPTERS[agentId];
99
+ return adapter.extractRawCredentialsFromDirectory?.(directory);
100
+ }
86
101
  /**
87
102
  * Install credentials for an agent.
88
103
  *
@@ -110,35 +125,56 @@ function removeCredentials(agentId, options) {
110
125
  return ADAPTERS[agentId].removeCredentials(options);
111
126
  }
112
127
  /**
113
- * Get access token from credentials.
128
+ * Extract access token from credentials.
114
129
  *
115
- * For multi-provider agents (like opencode), use `options.provider`
116
- * to specify which provider's token to extract.
130
+ * For OAuth tokens, automatically refreshes if expired.
131
+ * For API keys, returns immediately (no expiry).
117
132
  *
118
133
  * @example
119
- * const token = getAccessToken(creds);
120
- * if (token) { headers.Authorization = `Bearer ${token}`; }
121
- *
122
- * // For multi-provider agents
123
- * const token = getAccessToken(creds, { provider: "anthropic" });
134
+ * const token = await getAccessToken(creds);
135
+ * const token = await getAccessToken(creds, { skipRefresh: true });
124
136
  */
125
- function getAccessToken(creds, options) {
126
- return ADAPTERS[creds.agent].getAccessToken(creds, options);
137
+ async function getAccessToken(creds, options) {
138
+ const token = ADAPTERS[creds.agent].getAccessToken(creds, options);
139
+ // No token found
140
+ if (!token)
141
+ return undefined;
142
+ // API keys don't expire
143
+ if (creds.type === "api-key")
144
+ return token;
145
+ // Skip refresh if requested
146
+ if (options?.skipRefresh)
147
+ return token;
148
+ // Check if token is expired (try JWT first, then credential fields)
149
+ let expired = isTokenExpired(token);
150
+ if (expired === undefined) {
151
+ // Token isn't a JWT - check credential's explicit expiry fields
152
+ expired = isCredentialExpired(creds.data);
153
+ }
154
+ if (expired !== true)
155
+ return token; // Valid or no expiry info
156
+ // Refresh and persist
157
+ const result = await refreshAndPersist(creds, installCredentials, {
158
+ timeout: options?.refreshTimeout,
159
+ provider: options?.provider,
160
+ });
161
+ const finalCreds = result.ok ? result.credentials : result.staleCredentials;
162
+ return ADAPTERS[creds.agent].getAccessToken(finalCreds, options);
127
163
  }
128
164
  /**
129
- * Get access token for an agent (convenience function).
165
+ * Get access token for an agent by ID.
130
166
  *
131
- * Extracts credentials and returns the access token in one call.
167
+ * Convenience function that extracts credentials and gets the token.
168
+ * Automatically refreshes expired OAuth tokens.
132
169
  *
133
170
  * @example
134
- * const token = getAgentAccessToken("claude");
135
- * if (token) { ... }
171
+ * const token = await getAgentAccessToken("claude");
136
172
  */
137
- function getAgentAccessToken(agentId) {
173
+ async function getAgentAccessToken(agentId, options) {
138
174
  const creds = extractRawCredentials(agentId);
139
175
  if (!creds)
140
176
  return undefined;
141
- return getAccessToken(creds);
177
+ return getAccessToken(creds, options);
142
178
  }
143
179
  /**
144
180
  * Convert credentials to environment variables.
@@ -228,4 +264,4 @@ async function installCredentialsFromEnvironmentVariable(agent, options) {
228
264
  }
229
265
  return { ok: true };
230
266
  }
231
- export { checkAllAuth, checkAuth, credentialsToEnvironment, extractRawCredentials, getAccessToken, getAdapter, getAgentAccessToken, getAllAdapters, getCapabilities, getCredentialsEnvironmentVariableName, installCredentials, installCredentialsFromEnvironmentVariable, removeCredentials, };
267
+ export { checkAllAuth, checkAuth, credentialsToEnvironment, extractRawCredentials, extractRawCredentialsFromDirectory, getAccessToken, getAdapter, getAgentAccessToken, getAllAdapters, getCapabilities, getCredentialsEnvironmentVariableName, installCredentials, installCredentialsFromEnvironmentVariable, removeCredentials, };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Agent subprocess spawning utility.
3
+ */
4
+ /**
5
+ * Spawn agent and wait for completion.
6
+ *
7
+ * @returns Object with timedOut flag and exit code
8
+ */
9
+ declare function spawnAgent(binary: string, arguments_: string[], environment: NodeJS.ProcessEnv, timeout: number): Promise<{
10
+ timedOut: boolean;
11
+ exitCode: number | null;
12
+ }>;
13
+ export { spawnAgent };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Agent subprocess spawning utility.
3
+ */
4
+ import { spawn } from "node:child_process";
5
+ /**
6
+ * Spawn agent and wait for completion.
7
+ *
8
+ * @returns Object with timedOut flag and exit code
9
+ */
10
+ async function spawnAgent(binary, arguments_, environment, timeout) {
11
+ return new Promise((resolve) => {
12
+ const child = spawn(binary, arguments_, {
13
+ env: environment,
14
+ stdio: ["ignore", "pipe", "pipe"],
15
+ });
16
+ let timedOut = false;
17
+ let resolved = false;
18
+ const timer = setTimeout(() => {
19
+ if (!resolved) {
20
+ timedOut = true;
21
+ child.kill("SIGTERM");
22
+ }
23
+ }, timeout);
24
+ child.on("error", () => {
25
+ if (!resolved) {
26
+ resolved = true;
27
+ clearTimeout(timer);
28
+ resolve({ timedOut: false, exitCode: 1 });
29
+ }
30
+ });
31
+ child.on("close", (code) => {
32
+ if (!resolved) {
33
+ resolved = true;
34
+ clearTimeout(timer);
35
+ resolve({ timedOut, exitCode: code });
36
+ }
37
+ });
38
+ });
39
+ }
40
+ export { spawnAgent };
@@ -10,7 +10,7 @@ declare function handleAuthList(options: {
10
10
  declare function handleAuthToken(options: {
11
11
  agent: string;
12
12
  provider?: string;
13
- }): void;
13
+ }): Promise<void>;
14
14
  interface AuthExportOptions {
15
15
  agent: string;
16
16
  output: string;
@@ -18,7 +18,7 @@ function handleAuthList(options) {
18
18
  }
19
19
  }
20
20
  /** Handle auth token command */
21
- function handleAuthToken(options) {
21
+ async function handleAuthToken(options) {
22
22
  const agentId = validateAgent(options.agent);
23
23
  if (!agentId)
24
24
  return;
@@ -29,7 +29,7 @@ function handleAuthToken(options) {
29
29
  return;
30
30
  }
31
31
  // Extract the token based on credential type
32
- const token = getAccessToken(creds, { provider: options.provider });
32
+ const token = await getAccessToken(creds, { provider: options.provider });
33
33
  if (!token) {
34
34
  const providerHint = options.provider
35
35
  ? ` for provider '${options.provider}'`
package/dist/index.d.ts CHANGED
@@ -35,3 +35,5 @@ export { AGENT_CLIS } from "axshared";
35
35
  export type { AdapterCapabilities, AuthAdapter, InstallOptions, OperationResult, RemoveOptions, } from "./auth/adapter.js";
36
36
  export type { AuthStatus, Credentials } from "./auth/types.js";
37
37
  export { checkAllAuth, checkAuth, credentialsToEnvironment, extractRawCredentials, getAccessToken, getAgentAccessToken, getCredentialsEnvironmentVariableName, installCredentials, installCredentialsFromEnvironmentVariable, removeCredentials, getAdapter, getAllAdapters, getCapabilities, type InstallFromEnvironmentOptions, } from "./auth/registry.js";
38
+ export { isCredentialExpired, isTokenExpired, } from "./auth/is-token-expired.js";
39
+ export { refreshAndPersist, refreshCredentials, type RefreshAndPersistResult, type RefreshOptions, type RefreshResult, } from "./auth/refresh-credentials.js";
package/dist/index.js CHANGED
@@ -37,3 +37,6 @@ export {
37
37
  checkAllAuth, checkAuth, credentialsToEnvironment, extractRawCredentials, getAccessToken, getAgentAccessToken, getCredentialsEnvironmentVariableName, installCredentials, installCredentialsFromEnvironmentVariable, removeCredentials,
38
38
  // Adapter access
39
39
  getAdapter, getAllAdapters, getCapabilities, } from "./auth/registry.js";
40
+ // Token refresh utilities
41
+ export { isCredentialExpired, isTokenExpired, } from "./auth/is-token-expired.js";
42
+ export { refreshAndPersist, refreshCredentials, } from "./auth/refresh-credentials.js";
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "axauth",
3
3
  "author": "Łukasz Jerciński",
4
4
  "license": "MIT",
5
- "version": "1.3.0",
5
+ "version": "1.5.0",
6
6
  "description": "Authentication management library and CLI for AI coding agents",
7
7
  "repository": {
8
8
  "type": "git",
@@ -70,7 +70,7 @@
70
70
  "@commander-js/extra-typings": "^14.0.0",
71
71
  "@inquirer/password": "^5.0.3",
72
72
  "axconfig": "^3.2.0",
73
- "axshared": "^1.5.0",
73
+ "axshared": "^1.7.0",
74
74
  "commander": "^14.0.2",
75
75
  "zod": "^4.3.4"
76
76
  },