axauth 1.2.0 → 1.4.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,14 @@
1
+ /**
2
+ * JWT token expiry check utility.
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
+ export { isTokenExpired };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * JWT token expiry check utility.
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
+ function isTokenExpired(token, bufferSeconds = 60) {
14
+ const parts = token.split(".");
15
+ if (parts.length !== 3)
16
+ return undefined; // Not a JWT
17
+ const payloadPart = parts[1];
18
+ if (!payloadPart)
19
+ return undefined;
20
+ try {
21
+ // Convert base64url to standard base64 (JWTs use base64url encoding)
22
+ const base64 = payloadPart.replaceAll("-", "+").replaceAll("_", "/");
23
+ const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "=");
24
+ const payload = JSON.parse(atob(padded));
25
+ const exp = payload.exp;
26
+ if (typeof exp !== "number")
27
+ return undefined; // No exp claim
28
+ const nowSeconds = Date.now() / 1000;
29
+ return nowSeconds > exp - bufferSeconds;
30
+ }
31
+ catch {
32
+ return undefined; // Invalid JWT
33
+ }
34
+ }
35
+ export { 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: 10000) */
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,140 @@
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 { mkdtempSync, rmSync, writeFileSync } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import path from "node:path";
10
+ import { buildAgentRuntimeEnvironment, getAgent, } from "axshared";
11
+ import { spawnAgent } from "./spawn-agent.js";
12
+ /** Default timeout for refresh operations in milliseconds */
13
+ const DEFAULT_REFRESH_TIMEOUT_MS = 10_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: handled via GEMINI_FORCE_FILE_STORAGE env var in spawn
35
+ // OpenCode: file-only by design, no action needed
36
+ }
37
+ }
38
+ /**
39
+ * Refresh credentials by spawning the agent briefly.
40
+ *
41
+ * Creates an isolated temp config directory, installs credentials there,
42
+ * and runs the agent with a minimal prompt to trigger its internal refresh.
43
+ *
44
+ * @param creds - The credentials to refresh (must contain refresh_token for OAuth)
45
+ * @param options - Refresh options (timeout, provider for multi-provider agents)
46
+ * @returns RefreshResult with new credentials or error
47
+ */
48
+ async function refreshCredentials(creds, options) {
49
+ const timeout = options?.timeout ?? DEFAULT_REFRESH_TIMEOUT_MS;
50
+ const agent = getAgent(creds.agent);
51
+ // 1. Create temp config directory
52
+ const temporaryDirectory = mkdtempSync(path.join(tmpdir(), `axauth-refresh-${creds.agent}-`));
53
+ try {
54
+ // 2. Write agent-specific config to force file storage
55
+ writeForceFileConfig(creds.agent, temporaryDirectory);
56
+ // 3. Install credentials to temp directory
57
+ // Import dynamically to avoid circular dependency
58
+ const { installCredentials } = await import("./registry.js");
59
+ const installResult = installCredentials(creds, {
60
+ configDir: temporaryDirectory,
61
+ dataDir: temporaryDirectory,
62
+ });
63
+ if (!installResult.ok) {
64
+ return {
65
+ ok: false,
66
+ error: `Failed to install credentials: ${installResult.message}`,
67
+ };
68
+ }
69
+ // 4. Build runtime environment pointing to temp directory
70
+ const runtimeEnvironment = buildAgentRuntimeEnvironment(creds.agent, temporaryDirectory);
71
+ // 5. Add force-file env vars where needed
72
+ const forceFileEnvironment = creds.agent === "gemini" ? { GEMINI_FORCE_FILE_STORAGE: "true" } : {};
73
+ const fullEnvironment = {
74
+ ...process.env,
75
+ ...runtimeEnvironment,
76
+ ...forceFileEnvironment,
77
+ };
78
+ // 6. Build CLI arguments using simplePromptArguments from agent
79
+ const cliArguments = agent.simplePromptArguments("ping", {
80
+ provider: options?.provider,
81
+ });
82
+ // 7. Spawn agent with minimal prompt
83
+ const { timedOut } = await spawnAgent(agent.cli, cliArguments, fullEnvironment, timeout);
84
+ if (timedOut) {
85
+ return { ok: false, error: "Refresh timed out" };
86
+ }
87
+ // 8. Read refreshed credentials from temp directory
88
+ const { extractRawCredentialsFromDirectory } = await import("./registry.js");
89
+ const refreshedCredentials = extractRawCredentialsFromDirectory(creds.agent, temporaryDirectory);
90
+ if (!refreshedCredentials) {
91
+ return { ok: false, error: "No credentials found after refresh" };
92
+ }
93
+ return { ok: true, credentials: refreshedCredentials };
94
+ }
95
+ finally {
96
+ // 9. Clean up temp directory
97
+ try {
98
+ rmSync(temporaryDirectory, { recursive: true, force: true });
99
+ }
100
+ catch (error) {
101
+ const message = error instanceof Error ? error.message : String(error);
102
+ console.error(`Warning: Failed to clean up temp directory '${temporaryDirectory}': ${message}`);
103
+ }
104
+ }
105
+ }
106
+ /**
107
+ * Refresh credentials and persist to original storage.
108
+ *
109
+ * Higher-level function that:
110
+ * 1. Refreshes credentials via agent subprocess
111
+ * 2. Preserves _source marker from original credentials
112
+ * 3. Persists refreshed credentials to original storage location
113
+ *
114
+ * @param creds - Original credentials with _source marker
115
+ * @param installFunction - Function to install credentials (avoids circular import)
116
+ * @param options - Refresh options
117
+ * @returns Refreshed credentials or original credentials on failure
118
+ */
119
+ async function refreshAndPersist(creds, installFunction, options) {
120
+ const result = await refreshCredentials(creds, options);
121
+ if (!result.ok) {
122
+ console.error(`Warning: Token refresh failed: ${result.error}`);
123
+ return { ok: false, staleCredentials: creds };
124
+ }
125
+ // Preserve _source marker so credentials are saved to the same location
126
+ const refreshedCreds = {
127
+ ...result.credentials,
128
+ data: {
129
+ ...result.credentials.data,
130
+ _source: creds.data._source,
131
+ },
132
+ };
133
+ // Persist refreshed credentials back to storage
134
+ const installResult = installFunction(refreshedCreds);
135
+ if (!installResult.ok) {
136
+ console.error(`Warning: Failed to save refreshed credentials: ${installResult.message}`);
137
+ }
138
+ return { ok: true, credentials: refreshedCreds };
139
+ }
140
+ 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
  *
@@ -119,6 +126,13 @@ declare function credentialsToEnvironment(creds: Credentials): Record<string, st
119
126
  * Pattern: AX_<AGENT>_CREDENTIALS (e.g., AX_GEMINI_CREDENTIALS)
120
127
  */
121
128
  declare function getCredentialsEnvironmentVariableName(agent: AgentCli): string;
129
+ /** Options for installing credentials from environment variable */
130
+ interface InstallFromEnvironmentOptions {
131
+ /** Custom config directory */
132
+ configDir?: string;
133
+ /** Custom data directory */
134
+ dataDir?: string;
135
+ }
122
136
  /**
123
137
  * Install credentials from environment variable.
124
138
  *
@@ -132,15 +146,18 @@ declare function getCredentialsEnvironmentVariableName(agent: AgentCli): string;
132
146
  *
133
147
  * @example
134
148
  * // In CI/CD, set: AX_GEMINI_CREDENTIALS=$(cat creds.json)
135
- * const result = await installCredentialsFromEnvironmentVariable("gemini", "/tmp/.gemini");
149
+ * const result = await installCredentialsFromEnvironmentVariable("gemini", {
150
+ * configDir: "/tmp/.gemini",
151
+ * });
136
152
  * if (!result.ok) {
137
153
  * console.error(result.error);
138
154
  * }
139
155
  */
140
- declare function installCredentialsFromEnvironmentVariable(agent: AgentCli, configDirectory: string): Promise<{
156
+ declare function installCredentialsFromEnvironmentVariable(agent: AgentCli, options?: InstallFromEnvironmentOptions): Promise<{
141
157
  ok: true;
142
158
  } | {
143
159
  ok: false;
144
160
  error: string;
145
161
  }>;
146
- export { checkAllAuth, checkAuth, credentialsToEnvironment, extractRawCredentials, getAccessToken, getAdapter, getAgentAccessToken, getAllAdapters, getCapabilities, getCredentialsEnvironmentVariableName, installCredentials, installCredentialsFromEnvironmentVariable, removeCredentials, };
162
+ export type { InstallFromEnvironmentOptions };
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 { 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,52 @@ 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
149
+ const expired = isTokenExpired(token);
150
+ if (expired !== true)
151
+ return token; // Valid or not a JWT
152
+ // Refresh and persist
153
+ const result = await refreshAndPersist(creds, installCredentials, {
154
+ timeout: options?.refreshTimeout,
155
+ provider: options?.provider,
156
+ });
157
+ const finalCreds = result.ok ? result.credentials : result.staleCredentials;
158
+ return ADAPTERS[creds.agent].getAccessToken(finalCreds, options);
127
159
  }
128
160
  /**
129
- * Get access token for an agent (convenience function).
161
+ * Get access token for an agent by ID.
130
162
  *
131
- * Extracts credentials and returns the access token in one call.
163
+ * Convenience function that extracts credentials and gets the token.
164
+ * Automatically refreshes expired OAuth tokens.
132
165
  *
133
166
  * @example
134
- * const token = getAgentAccessToken("claude");
135
- * if (token) { ... }
167
+ * const token = await getAgentAccessToken("claude");
136
168
  */
137
- function getAgentAccessToken(agentId) {
169
+ async function getAgentAccessToken(agentId, options) {
138
170
  const creds = extractRawCredentials(agentId);
139
171
  if (!creds)
140
172
  return undefined;
141
- return getAccessToken(creds);
173
+ return getAccessToken(creds, options);
142
174
  }
143
175
  /**
144
176
  * Convert credentials to environment variables.
@@ -171,12 +203,14 @@ function getCredentialsEnvironmentVariableName(agent) {
171
203
  *
172
204
  * @example
173
205
  * // In CI/CD, set: AX_GEMINI_CREDENTIALS=$(cat creds.json)
174
- * const result = await installCredentialsFromEnvironmentVariable("gemini", "/tmp/.gemini");
206
+ * const result = await installCredentialsFromEnvironmentVariable("gemini", {
207
+ * configDir: "/tmp/.gemini",
208
+ * });
175
209
  * if (!result.ok) {
176
210
  * console.error(result.error);
177
211
  * }
178
212
  */
179
- async function installCredentialsFromEnvironmentVariable(agent, configDirectory) {
213
+ async function installCredentialsFromEnvironmentVariable(agent, options) {
180
214
  const environmentVariableName = getCredentialsEnvironmentVariableName(agent);
181
215
  const environmentValue = process.env[environmentVariableName];
182
216
  if (!environmentValue) {
@@ -218,11 +252,12 @@ async function installCredentialsFromEnvironmentVariable(agent, configDirectory)
218
252
  }
219
253
  // Install credentials to config directory
220
254
  const result = installCredentials(credentials, {
221
- configDir: configDirectory,
255
+ configDir: options?.configDir,
256
+ dataDir: options?.dataDir,
222
257
  });
223
258
  if (!result.ok) {
224
259
  return { ok: false, error: result.message };
225
260
  }
226
261
  return { ok: true };
227
262
  }
228
- export { checkAllAuth, checkAuth, credentialsToEnvironment, extractRawCredentials, getAccessToken, getAdapter, getAgentAccessToken, getAllAdapters, getCapabilities, getCredentialsEnvironmentVariableName, installCredentials, installCredentialsFromEnvironmentVariable, removeCredentials, };
263
+ 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
@@ -34,4 +34,6 @@ export type { AgentCli } from "axshared";
34
34
  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
- export { checkAllAuth, checkAuth, credentialsToEnvironment, extractRawCredentials, getAccessToken, getAgentAccessToken, getCredentialsEnvironmentVariableName, installCredentials, installCredentialsFromEnvironmentVariable, removeCredentials, getAdapter, getAllAdapters, getCapabilities, } from "./auth/registry.js";
37
+ export { checkAllAuth, checkAuth, credentialsToEnvironment, extractRawCredentials, getAccessToken, getAgentAccessToken, getCredentialsEnvironmentVariableName, installCredentials, installCredentialsFromEnvironmentVariable, removeCredentials, getAdapter, getAllAdapters, getCapabilities, type InstallFromEnvironmentOptions, } from "./auth/registry.js";
38
+ export { 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 { 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.2.0",
5
+ "version": "1.4.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.6.0",
74
74
  "commander": "^14.0.2",
75
75
  "zod": "^4.3.4"
76
76
  },