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.
- package/dist/auth/adapter.d.ts +11 -0
- package/dist/auth/agents/claude.js +10 -0
- package/dist/auth/agents/codex.js +18 -0
- package/dist/auth/agents/copilot.js +4 -0
- package/dist/auth/agents/gemini.js +4 -0
- package/dist/auth/agents/opencode.js +5 -0
- package/dist/auth/extract-creds-from-directory.d.ts +16 -0
- package/dist/auth/extract-creds-from-directory.js +37 -0
- package/dist/auth/is-token-expired.d.ts +14 -0
- package/dist/auth/is-token-expired.js +35 -0
- package/dist/auth/refresh-credentials.d.ts +60 -0
- package/dist/auth/refresh-credentials.js +140 -0
- package/dist/auth/registry.d.ts +34 -17
- package/dist/auth/registry.js +55 -20
- package/dist/auth/spawn-agent.d.ts +13 -0
- package/dist/auth/spawn-agent.js +40 -0
- package/dist/commands/auth.d.ts +1 -1
- package/dist/commands/auth.js +2 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -0
- package/package.json +2 -2
package/dist/auth/adapter.d.ts
CHANGED
|
@@ -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 };
|
package/dist/auth/registry.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
95
|
+
* Extract access token from credentials.
|
|
86
96
|
*
|
|
87
|
-
* For
|
|
88
|
-
*
|
|
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
|
-
*
|
|
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
|
|
106
|
+
* Get access token for an agent by ID.
|
|
100
107
|
*
|
|
101
|
-
*
|
|
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",
|
|
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,
|
|
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 {
|
|
162
|
+
export type { InstallFromEnvironmentOptions };
|
|
163
|
+
export { checkAllAuth, checkAuth, credentialsToEnvironment, extractRawCredentials, extractRawCredentialsFromDirectory, getAccessToken, getAdapter, getAgentAccessToken, getAllAdapters, getCapabilities, getCredentialsEnvironmentVariableName, installCredentials, installCredentialsFromEnvironmentVariable, removeCredentials, };
|
package/dist/auth/registry.js
CHANGED
|
@@ -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
|
-
*
|
|
128
|
+
* Extract access token from credentials.
|
|
114
129
|
*
|
|
115
|
-
* For
|
|
116
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
161
|
+
* Get access token for an agent by ID.
|
|
130
162
|
*
|
|
131
|
-
*
|
|
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",
|
|
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,
|
|
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:
|
|
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 };
|
package/dist/commands/auth.d.ts
CHANGED
package/dist/commands/auth.js
CHANGED
|
@@ -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.
|
|
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.
|
|
73
|
+
"axshared": "^1.6.0",
|
|
74
74
|
"commander": "^14.0.2",
|
|
75
75
|
"zod": "^4.3.4"
|
|
76
76
|
},
|