axauth 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,33 +16,53 @@ interface OperationResult {
16
16
  /**
17
17
  * Options for credential installation.
18
18
  *
19
- * When `path` is provided, credentials are always written as a file to that
20
- * path (keychain is only for default location). The `storage` option is ignored.
19
+ * When custom directories are provided, credentials are always written as files
20
+ * (keychain is only for default location). The `storage` option is ignored.
21
21
  *
22
- * When `path` is not provided, `storage` controls where credentials go:
22
+ * When no directories are provided, `storage` controls where credentials go:
23
23
  * - `"keychain"`: Store in macOS Keychain (if supported)
24
24
  * - `"file"`: Store in agent's default file location
25
25
  * - `undefined`: Use the `_source` marker in credentials, or default to file
26
+ *
27
+ * **Agents without separation** (Claude, Codex, Gemini, Copilot):
28
+ * - Config and data are stored in the same directory
29
+ * - `configDir` and `dataDir` are interchangeable (either works)
30
+ * - Providing different values for both will result in an error
31
+ *
32
+ * **Agents with separation** (OpenCode):
33
+ * - Config and data are stored in separate directories
34
+ * - `dataDir` controls where credentials go; `configDir` is for settings
35
+ * - If only `configDir` provided, credentials use default data location
36
+ * - If only `dataDir` provided, config uses default location
26
37
  */
27
38
  interface InstallOptions {
28
- /** Storage type for default location (ignored if configDir is set) */
39
+ /** Storage type for default location (ignored if configDir/dataDir is set) */
29
40
  storage?: StorageType;
30
- /** Custom config directory (forces file storage, keychain not available) */
41
+ /** Custom config directory for settings/preferences */
31
42
  configDir?: string;
43
+ /** Custom data directory for credentials */
44
+ dataDir?: string;
32
45
  }
33
46
  /**
34
47
  * Options for credential removal.
35
48
  *
36
- * When `configDir` is provided, only the credentials file in that directory
49
+ * When `dataDir` is provided, only the credentials file in that directory
37
50
  * is removed. Keychain credentials are not affected (keychain is only for
38
51
  * default location).
39
52
  *
40
- * When `configDir` is not provided, credentials are removed from all default
41
- * locations (keychain and/or default file path).
53
+ * **Agents without separation** (Claude, Codex, Gemini, Copilot):
54
+ * - `configDir` and `dataDir` are interchangeable
55
+ * - Either option specifies where to remove credentials from
56
+ *
57
+ * **Agents with separation** (OpenCode):
58
+ * - `dataDir` specifies where to remove credentials from
59
+ * - If only `configDir` provided, credentials are removed from default location
42
60
  */
43
61
  interface RemoveOptions {
44
- /** Custom config directory (removes only this location, not keychain) */
62
+ /** Custom config directory for settings/preferences */
45
63
  configDir?: string;
64
+ /** Custom data directory for credentials */
65
+ dataDir?: string;
46
66
  }
47
67
  /**
48
68
  * Options for token extraction.
@@ -3,10 +3,6 @@
3
3
  */
4
4
  /** Get default credentials file path */
5
5
  declare function getDefaultCredsFilePath(): string;
6
- /** Load OAuth credentials from keychain */
7
- declare function loadKeychainCreds(): Record<string, unknown> | undefined;
8
- /** Load OAuth credentials from file */
9
- declare function loadFileCreds(): Record<string, unknown> | undefined;
10
6
  /** Save credentials to keychain */
11
7
  declare function saveKeychainCreds(oauthData: Record<string, unknown>): boolean;
12
8
  /** Save credentials to file */
@@ -15,4 +11,16 @@ declare function saveFileCreds(oauthData: Record<string, unknown>, filePath: str
15
11
  declare function deleteKeychainCreds(): boolean;
16
12
  /** Delete credentials from file */
17
13
  declare function deleteFileCreds(filePath?: string): boolean;
18
- export { deleteFileCreds, deleteKeychainCreds, getDefaultCredsFilePath, loadFileCreds, loadKeychainCreds, saveFileCreds, saveKeychainCreds, };
14
+ declare const AGENT_ID: "claude";
15
+ /** Check if Claude is authenticated and return status */
16
+ declare function checkAuth(): {
17
+ authenticated: boolean;
18
+ method?: string;
19
+ details?: Record<string, unknown>;
20
+ };
21
+ /** Extract raw credentials from available sources */
22
+ declare function extractRawCredentials(): {
23
+ type: "oauth" | "api-key";
24
+ data: Record<string, unknown>;
25
+ } | undefined;
26
+ export { AGENT_ID, checkAuth, deleteFileCreds, deleteKeychainCreds, extractRawCredentials, getDefaultCredsFilePath, saveFileCreds, saveKeychainCreds, };
@@ -62,4 +62,53 @@ function deleteKeychainCreds() {
62
62
  function deleteFileCreds(filePath) {
63
63
  return deleteFile(filePath ?? getDefaultCredsFilePath());
64
64
  }
65
- export { deleteFileCreds, deleteKeychainCreds, getDefaultCredsFilePath, loadFileCreds, loadKeychainCreds, saveFileCreds, saveKeychainCreds, };
65
+ const AGENT_ID = "claude";
66
+ /** Check if Claude is authenticated and return status */
67
+ function checkAuth() {
68
+ if (process.env.CLAUDE_CODE_OAUTH_TOKEN) {
69
+ return { authenticated: true, method: "OAuth (env)" };
70
+ }
71
+ const keychainCreds = loadKeychainCreds();
72
+ if (keychainCreds) {
73
+ const subType = keychainCreds.subscriptionType;
74
+ return { authenticated: true, method: `OAuth (${subType ?? "keychain"})` };
75
+ }
76
+ if (loadFileCreds()) {
77
+ return { authenticated: true, method: "OAuth (file)" };
78
+ }
79
+ if (process.env.ANTHROPIC_API_KEY) {
80
+ return { authenticated: true, method: "API key" };
81
+ }
82
+ return { authenticated: false };
83
+ }
84
+ /** Extract raw credentials from available sources */
85
+ function extractRawCredentials() {
86
+ if (process.env.CLAUDE_CODE_OAUTH_TOKEN) {
87
+ return {
88
+ type: "oauth",
89
+ data: { accessToken: process.env.CLAUDE_CODE_OAUTH_TOKEN },
90
+ };
91
+ }
92
+ const keychainCreds = loadKeychainCreds();
93
+ if (keychainCreds) {
94
+ return {
95
+ type: "oauth",
96
+ data: { ...keychainCreds, _source: "keychain" },
97
+ };
98
+ }
99
+ const fileCreds = loadFileCreds();
100
+ if (fileCreds) {
101
+ return {
102
+ type: "oauth",
103
+ data: { ...fileCreds, _source: "file" },
104
+ };
105
+ }
106
+ if (process.env.ANTHROPIC_API_KEY) {
107
+ return {
108
+ type: "api-key",
109
+ data: { apiKey: process.env.ANTHROPIC_API_KEY },
110
+ };
111
+ }
112
+ return undefined;
113
+ }
114
+ export { AGENT_ID, checkAuth, deleteFileCreds, deleteKeychainCreds, extractRawCredentials, getDefaultCredsFilePath, saveFileCreds, saveKeychainCreds, };
@@ -5,8 +5,8 @@
5
5
  */
6
6
  import path from "node:path";
7
7
  import { isMacOS } from "../keychain.js";
8
- import { deleteFileCreds, deleteKeychainCreds, getDefaultCredsFilePath, loadFileCreds, loadKeychainCreds, saveFileCreds, saveKeychainCreds, } from "./claude-code-storage.js";
9
- const AGENT_ID = "claude";
8
+ import { resolveCustomDirectory } from "../validate-directories.js";
9
+ import { AGENT_ID, checkAuth, deleteFileCreds, deleteKeychainCreds, extractRawCredentials, getDefaultCredsFilePath, saveFileCreds, saveKeychainCreds, } from "./claude-storage.js";
10
10
  const CREDS_FILE_NAME = ".credentials.json";
11
11
  /** Claude Code authentication adapter */
12
12
  const claudeCodeAdapter = {
@@ -18,60 +18,20 @@ const claudeCodeAdapter = {
18
18
  installApiKey: false,
19
19
  },
20
20
  checkAuth() {
21
- if (process.env.CLAUDE_CODE_OAUTH_TOKEN) {
22
- return { agentId: AGENT_ID, authenticated: true, method: "OAuth (env)" };
23
- }
24
- const keychainCreds = loadKeychainCreds();
25
- if (keychainCreds) {
26
- const subType = keychainCreds.subscriptionType;
27
- return {
28
- agentId: AGENT_ID,
29
- authenticated: true,
30
- method: `OAuth (${subType ?? "keychain"})`,
31
- };
32
- }
33
- if (loadFileCreds()) {
34
- return { agentId: AGENT_ID, authenticated: true, method: "OAuth (file)" };
35
- }
36
- if (process.env.ANTHROPIC_API_KEY) {
37
- return { agentId: AGENT_ID, authenticated: true, method: "API key" };
38
- }
39
- return { agentId: AGENT_ID, authenticated: false };
21
+ const result = checkAuth();
22
+ return { agentId: AGENT_ID, ...result };
40
23
  },
41
24
  extractRawCredentials() {
42
- if (process.env.CLAUDE_CODE_OAUTH_TOKEN) {
43
- return {
44
- agent: AGENT_ID,
45
- type: "oauth",
46
- data: { accessToken: process.env.CLAUDE_CODE_OAUTH_TOKEN },
47
- };
48
- }
49
- const keychainCreds = loadKeychainCreds();
50
- if (keychainCreds) {
51
- return {
52
- agent: AGENT_ID,
53
- type: "oauth",
54
- data: { ...keychainCreds, _source: "keychain" },
55
- };
56
- }
57
- const fileCreds = loadFileCreds();
58
- if (fileCreds) {
59
- return {
60
- agent: AGENT_ID,
61
- type: "oauth",
62
- data: { ...fileCreds, _source: "file" },
63
- };
64
- }
65
- if (process.env.ANTHROPIC_API_KEY) {
66
- return {
67
- agent: AGENT_ID,
68
- type: "api-key",
69
- data: { apiKey: process.env.ANTHROPIC_API_KEY },
70
- };
71
- }
72
- return undefined;
25
+ const result = extractRawCredentials();
26
+ if (!result)
27
+ return undefined;
28
+ return { agent: AGENT_ID, ...result };
73
29
  },
74
30
  installCredentials(creds, options) {
31
+ // Resolve custom directory with validation
32
+ const resolved = resolveCustomDirectory(AGENT_ID, options?.configDir, options?.dataDir);
33
+ if (!resolved.ok)
34
+ return resolved.error;
75
35
  if (creds.type === "api-key") {
76
36
  return {
77
37
  ok: false,
@@ -79,9 +39,9 @@ const claudeCodeAdapter = {
79
39
  };
80
40
  }
81
41
  const { _source, ...oauthData } = creds.data;
82
- // Custom config directory forces file storage (keychain only for default location)
83
- if (options?.configDir) {
84
- const targetPath = path.join(options.configDir, CREDS_FILE_NAME);
42
+ // Custom directory forces file storage (keychain only for default location)
43
+ if (resolved.customDir) {
44
+ const targetPath = path.join(resolved.customDir, CREDS_FILE_NAME);
85
45
  if (saveFileCreds(oauthData, targetPath)) {
86
46
  return {
87
47
  ok: true,
@@ -117,9 +77,13 @@ const claudeCodeAdapter = {
117
77
  };
118
78
  },
119
79
  removeCredentials(options) {
120
- // Custom config directory: only remove that specific file (no keychain)
121
- if (options?.configDir) {
122
- const targetPath = path.join(options.configDir, CREDS_FILE_NAME);
80
+ // Resolve custom directory with validation
81
+ const resolved = resolveCustomDirectory(AGENT_ID, options?.configDir, options?.dataDir);
82
+ if (!resolved.ok)
83
+ return resolved.error;
84
+ // Custom directory: only remove that specific file (no keychain)
85
+ if (resolved.customDir) {
86
+ const targetPath = path.join(resolved.customDir, CREDS_FILE_NAME);
123
87
  if (deleteFileCreds(targetPath)) {
124
88
  return { ok: true, message: `Removed ${targetPath}` };
125
89
  }
@@ -6,6 +6,7 @@
6
6
  import path from "node:path";
7
7
  import { ensureDirectory } from "../file-storage.js";
8
8
  import { isMacOS } from "../keychain.js";
9
+ import { resolveCustomDirectory } from "../validate-directories.js";
9
10
  import { checkAuth, extractRawCredentials } from "./codex-auth-check.js";
10
11
  import { getAuthFilePath, getCodexHome, updateConfigStorage, } from "./codex-config.js";
11
12
  import { buildAuthContent, deleteFileCreds, deleteKeychainCreds, saveFileCreds, saveKeychainCreds, } from "./codex-storage.js";
@@ -23,16 +24,20 @@ const codexAdapter = {
23
24
  checkAuth,
24
25
  extractRawCredentials,
25
26
  installCredentials(creds, options) {
27
+ // Resolve custom directory with validation
28
+ const resolved = resolveCustomDirectory(AGENT_ID, options?.configDir, options?.dataDir);
29
+ if (!resolved.ok)
30
+ return resolved.error;
26
31
  const authContent = buildAuthContent(creds.type, creds.data);
27
- // Custom config directory forces file storage (keychain only for default location)
28
- if (options?.configDir) {
29
- if (!ensureDirectory(options.configDir)) {
32
+ // Custom directory forces file storage (keychain only for default location)
33
+ if (resolved.customDir) {
34
+ if (!ensureDirectory(resolved.customDir)) {
30
35
  return {
31
36
  ok: false,
32
- message: `Failed to create directory ${options.configDir}`,
37
+ message: `Failed to create directory ${resolved.customDir}`,
33
38
  };
34
39
  }
35
- const targetPath = path.join(options.configDir, CREDS_FILE_NAME);
40
+ const targetPath = path.join(resolved.customDir, CREDS_FILE_NAME);
36
41
  if (saveFileCreds(authContent, targetPath)) {
37
42
  return {
38
43
  ok: true,
@@ -73,9 +78,13 @@ const codexAdapter = {
73
78
  return { ok: false, message: "Failed to install credentials to file" };
74
79
  },
75
80
  removeCredentials(options) {
76
- // Custom config directory: only remove that specific file (no keychain)
77
- if (options?.configDir) {
78
- const targetPath = path.join(options.configDir, CREDS_FILE_NAME);
81
+ // Resolve custom directory with validation
82
+ const resolved = resolveCustomDirectory(AGENT_ID, options?.configDir, options?.dataDir);
83
+ if (!resolved.ok)
84
+ return resolved.error;
85
+ // Custom directory: only remove that specific file (no keychain)
86
+ if (resolved.customDir) {
87
+ const targetPath = path.join(resolved.customDir, CREDS_FILE_NAME);
79
88
  if (deleteFileCreds(targetPath)) {
80
89
  return { ok: true, message: `Removed ${targetPath}` };
81
90
  }
@@ -7,6 +7,7 @@
7
7
  import path from "node:path";
8
8
  import { ensureDirectory } from "../file-storage.js";
9
9
  import { isMacOS } from "../keychain.js";
10
+ import { resolveCustomDirectory } from "../validate-directories.js";
10
11
  import { findFirstAvailableToken } from "./copilot-auth-check.js";
11
12
  import { deleteFileToken, deleteKeychainToken, deleteTokenFromPath, getConfigDirectory, getConfigFilePath, saveFileToken, saveKeychainToken, saveTokenToPath, } from "./copilot-storage.js";
12
13
  const AGENT_ID = "copilot";
@@ -48,6 +49,10 @@ const copilotAdapter = {
48
49
  };
49
50
  },
50
51
  installCredentials(creds, options) {
52
+ // Resolve custom directory with validation
53
+ const resolved = resolveCustomDirectory(AGENT_ID, options?.configDir, options?.dataDir);
54
+ if (!resolved.ok)
55
+ return resolved.error;
51
56
  if (creds.type === "api-key") {
52
57
  return {
53
58
  ok: false,
@@ -60,9 +65,9 @@ const copilotAdapter = {
60
65
  if (!token) {
61
66
  return { ok: false, message: "No access token found in credentials" };
62
67
  }
63
- // Custom config directory forces file storage
64
- if (options?.configDir) {
65
- const targetPath = path.join(options.configDir, CREDS_FILE_NAME);
68
+ // Custom directory forces file storage
69
+ if (resolved.customDir) {
70
+ const targetPath = path.join(resolved.customDir, CREDS_FILE_NAME);
66
71
  if (saveTokenToPath(token, targetPath)) {
67
72
  return {
68
73
  ok: true,
@@ -105,9 +110,13 @@ const copilotAdapter = {
105
110
  return { ok: false, message: "Failed to install credentials to file" };
106
111
  },
107
112
  removeCredentials(options) {
108
- // Custom config directory: only remove that specific file
109
- if (options?.configDir) {
110
- const targetPath = path.join(options.configDir, CREDS_FILE_NAME);
113
+ // Resolve custom directory with validation
114
+ const resolved = resolveCustomDirectory(AGENT_ID, options?.configDir, options?.dataDir);
115
+ if (!resolved.ok)
116
+ return resolved.error;
117
+ // Custom directory: only remove that specific file
118
+ if (resolved.customDir) {
119
+ const targetPath = path.join(resolved.customDir, CREDS_FILE_NAME);
111
120
  if (deleteTokenFromPath(targetPath)) {
112
121
  return { ok: true, message: `Removed ${targetPath}` };
113
122
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Gemini credential installation operations.
3
+ *
4
+ * Extracted from gemini.ts to reduce file complexity.
5
+ */
6
+ import type { InstallOptions, OperationResult, RemoveOptions } from "../adapter.js";
7
+ /** Install OAuth credentials to storage */
8
+ declare function installOAuthCredentials(oauthData: Record<string, unknown>, source: string | undefined, options?: InstallOptions): OperationResult;
9
+ /** Remove credentials from storage */
10
+ declare function removeGeminiCredentials(options?: RemoveOptions): OperationResult;
11
+ export { installOAuthCredentials, removeGeminiCredentials };
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Gemini credential installation operations.
3
+ *
4
+ * Extracted from gemini.ts to reduce file complexity.
5
+ */
6
+ import path from "node:path";
7
+ import { ensureDirectory } from "../file-storage.js";
8
+ import { isMacOS } from "../keychain.js";
9
+ import { resolveCustomDirectory } from "../validate-directories.js";
10
+ import { clearAuthTypeFromSettings, deleteKeychainCreds, deleteOAuthCreds, getDefaultOAuthPath, saveKeychainCreds, saveOAuthCreds, setAuthTypeInSettings, } from "./gemini-storage.js";
11
+ const AGENT_ID = "gemini";
12
+ const CREDS_FILE_NAME = "oauth_creds.json";
13
+ /** Install OAuth credentials to storage */
14
+ function installOAuthCredentials(oauthData, source, options) {
15
+ // Resolve custom directory with validation
16
+ const resolved = resolveCustomDirectory(AGENT_ID, options?.configDir, options?.dataDir);
17
+ if (!resolved.ok)
18
+ return resolved.error;
19
+ // Custom directory forces file storage
20
+ if (resolved.customDir) {
21
+ return installToCustomDirectory(oauthData, resolved.customDir);
22
+ }
23
+ // Default location: use storage option or _source marker
24
+ const targetStorage = options?.storage ?? (source === "keychain" ? "keychain" : "file");
25
+ if (targetStorage === "keychain") {
26
+ return installToKeychain(oauthData);
27
+ }
28
+ return installToDefaultFile(oauthData);
29
+ }
30
+ /** Install to custom directory */
31
+ function installToCustomDirectory(oauthData, customDirectory) {
32
+ if (!ensureDirectory(customDirectory)) {
33
+ return {
34
+ ok: false,
35
+ message: `Failed to create directory ${customDirectory}`,
36
+ };
37
+ }
38
+ const targetPath = path.join(customDirectory, CREDS_FILE_NAME);
39
+ if (!saveOAuthCreds(oauthData, targetPath)) {
40
+ return {
41
+ ok: false,
42
+ message: `Failed to install credentials to ${targetPath}`,
43
+ };
44
+ }
45
+ if (!setAuthTypeInSettings("oauth-personal", customDirectory)) {
46
+ return { ok: false, message: "Failed to update settings" };
47
+ }
48
+ return { ok: true, message: `Installed credentials to ${targetPath}` };
49
+ }
50
+ /** Install to macOS Keychain */
51
+ function installToKeychain(oauthData) {
52
+ if (!isMacOS()) {
53
+ return {
54
+ ok: false,
55
+ message: "Keychain storage is only available on macOS",
56
+ };
57
+ }
58
+ if (!saveKeychainCreds(oauthData)) {
59
+ return { ok: false, message: "Failed to save to macOS Keychain" };
60
+ }
61
+ if (!setAuthTypeInSettings("oauth-personal")) {
62
+ return { ok: false, message: "Failed to update settings" };
63
+ }
64
+ return { ok: true, message: "Installed credentials to macOS Keychain" };
65
+ }
66
+ /** Install to default file location */
67
+ function installToDefaultFile(oauthData) {
68
+ const targetPath = getDefaultOAuthPath();
69
+ const targetDirectory = path.dirname(targetPath);
70
+ if (!ensureDirectory(targetDirectory)) {
71
+ return {
72
+ ok: false,
73
+ message: `Failed to create directory ${targetDirectory}`,
74
+ };
75
+ }
76
+ if (!saveOAuthCreds(oauthData, targetPath)) {
77
+ return { ok: false, message: "Failed to write OAuth credentials" };
78
+ }
79
+ if (!setAuthTypeInSettings("oauth-personal")) {
80
+ return { ok: false, message: "Failed to update settings" };
81
+ }
82
+ return { ok: true, message: `Installed credentials to ${targetPath}` };
83
+ }
84
+ /** Remove credentials from storage */
85
+ function removeGeminiCredentials(options) {
86
+ // Resolve custom directory with validation
87
+ const resolved = resolveCustomDirectory(AGENT_ID, options?.configDir, options?.dataDir);
88
+ if (!resolved.ok)
89
+ return resolved.error;
90
+ // Custom directory: only remove that specific file
91
+ if (resolved.customDir) {
92
+ const targetPath = path.join(resolved.customDir, CREDS_FILE_NAME);
93
+ if (deleteOAuthCreds(targetPath)) {
94
+ return { ok: true, message: `Removed ${targetPath}` };
95
+ }
96
+ return { ok: true, message: "No credentials file found at specified path" };
97
+ }
98
+ // Default location: remove from keychain, file, and clear settings
99
+ const removedFrom = [];
100
+ if (deleteKeychainCreds()) {
101
+ removedFrom.push("macOS Keychain");
102
+ }
103
+ if (deleteOAuthCreds()) {
104
+ removedFrom.push(getDefaultOAuthPath());
105
+ }
106
+ // Always clear auth type setting to avoid stale configuration
107
+ if (clearAuthTypeFromSettings()) {
108
+ removedFrom.push("auth type from settings");
109
+ }
110
+ if (removedFrom.length === 0) {
111
+ return { ok: true, message: "No credentials found" };
112
+ }
113
+ return { ok: true, message: `Removed from ${removedFrom.join(" and ")}` };
114
+ }
115
+ export { installOAuthCredentials, removeGeminiCredentials };
@@ -3,13 +3,9 @@
3
3
  *
4
4
  * Supports OAuth via keychain (macOS) or file. API key auth is env-var only.
5
5
  */
6
- import path from "node:path";
7
- import { ensureDirectory } from "../file-storage.js";
8
- import { isMacOS } from "../keychain.js";
9
6
  import { findCredentials } from "./gemini-auth-check.js";
10
- import { clearAuthTypeFromSettings, deleteKeychainCreds, deleteOAuthCreds, getDefaultOAuthPath, saveKeychainCreds, saveOAuthCreds, setAuthTypeInSettings, } from "./gemini-storage.js";
7
+ import { installOAuthCredentials, removeGeminiCredentials, } from "./gemini-install.js";
11
8
  const AGENT_ID = "gemini";
12
- const CREDS_FILE_NAME = "oauth_creds.json";
13
9
  /** Gemini authentication adapter */
14
10
  const geminiAdapter = {
15
11
  agentId: AGENT_ID,
@@ -60,91 +56,10 @@ const geminiAdapter = {
60
56
  };
61
57
  }
62
58
  const { _source, ...oauthData } = creds.data;
63
- // Custom config directory forces file storage (keychain only for default location)
64
- if (options?.configDir) {
65
- if (!ensureDirectory(options.configDir)) {
66
- return {
67
- ok: false,
68
- message: `Failed to create directory ${options.configDir}`,
69
- };
70
- }
71
- const targetPath = path.join(options.configDir, CREDS_FILE_NAME);
72
- if (saveOAuthCreds(oauthData, targetPath)) {
73
- // Also set auth type in settings.json at the target directory
74
- if (!setAuthTypeInSettings("oauth-personal", options.configDir)) {
75
- return { ok: false, message: "Failed to update settings" };
76
- }
77
- return {
78
- ok: true,
79
- message: `Installed credentials to ${targetPath}`,
80
- };
81
- }
82
- return {
83
- ok: false,
84
- message: `Failed to install credentials to ${targetPath}`,
85
- };
86
- }
87
- // Default location: use storage option or _source marker
88
- const targetStorage = options?.storage ?? (_source === "keychain" ? "keychain" : "file");
89
- if (targetStorage === "keychain") {
90
- if (!isMacOS()) {
91
- return {
92
- ok: false,
93
- message: "Keychain storage is only available on macOS",
94
- };
95
- }
96
- if (saveKeychainCreds(oauthData)) {
97
- if (!setAuthTypeInSettings("oauth-personal")) {
98
- return { ok: false, message: "Failed to update settings" };
99
- }
100
- return { ok: true, message: "Installed credentials to macOS Keychain" };
101
- }
102
- return { ok: false, message: "Failed to save to macOS Keychain" };
103
- }
104
- // File storage
105
- const targetPath = getDefaultOAuthPath();
106
- const targetDirectory = path.dirname(targetPath);
107
- if (!ensureDirectory(targetDirectory)) {
108
- return {
109
- ok: false,
110
- message: `Failed to create directory ${targetDirectory}`,
111
- };
112
- }
113
- if (!saveOAuthCreds(oauthData, targetPath)) {
114
- return { ok: false, message: "Failed to write OAuth credentials" };
115
- }
116
- if (!setAuthTypeInSettings("oauth-personal")) {
117
- return { ok: false, message: "Failed to update settings" };
118
- }
119
- return { ok: true, message: `Installed credentials to ${targetPath}` };
59
+ return installOAuthCredentials(oauthData, _source, options);
120
60
  },
121
61
  removeCredentials(options) {
122
- // Custom config directory: only remove that specific file (no settings update)
123
- if (options?.configDir) {
124
- const targetPath = path.join(options.configDir, CREDS_FILE_NAME);
125
- if (deleteOAuthCreds(targetPath)) {
126
- return { ok: true, message: `Removed ${targetPath}` };
127
- }
128
- return {
129
- ok: true,
130
- message: "No credentials file found at specified path",
131
- };
132
- }
133
- // Default location: remove from keychain, file, and clear settings
134
- const removedFrom = [];
135
- if (deleteKeychainCreds()) {
136
- removedFrom.push("macOS Keychain");
137
- }
138
- if (deleteOAuthCreds()) {
139
- removedFrom.push(getDefaultOAuthPath());
140
- }
141
- if (clearAuthTypeFromSettings()) {
142
- removedFrom.push("auth type from settings.json");
143
- }
144
- if (removedFrom.length === 0) {
145
- return { ok: true, message: "No credentials found" };
146
- }
147
- return { ok: true, message: `Removed from ${removedFrom.join(", ")}` };
62
+ return removeGeminiCredentials(options);
148
63
  },
149
64
  getAccessToken(creds) {
150
65
  const data = creds.data;
@@ -2,6 +2,10 @@
2
2
  * OpenCode auth adapter.
3
3
  *
4
4
  * Supports multi-provider auth via file only. No keychain or env var support.
5
+ *
6
+ * OpenCode follows XDG Base Directory Specification:
7
+ * - Config: ~/.config/opencode (XDG_CONFIG_HOME/opencode)
8
+ * - Data (auth): ~/.local/share/opencode (XDG_DATA_HOME/opencode)
5
9
  */
6
10
  import type { AuthAdapter } from "../adapter.js";
7
11
  /** OpenCode authentication adapter */
@@ -2,16 +2,28 @@
2
2
  * OpenCode auth adapter.
3
3
  *
4
4
  * Supports multi-provider auth via file only. No keychain or env var support.
5
+ *
6
+ * OpenCode follows XDG Base Directory Specification:
7
+ * - Config: ~/.config/opencode (XDG_CONFIG_HOME/opencode)
8
+ * - Data (auth): ~/.local/share/opencode (XDG_DATA_HOME/opencode)
5
9
  */
6
10
  import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
7
- import { homedir } from "node:os";
8
11
  import path from "node:path";
12
+ import { resolveAgentDataDirectory } from "axshared";
13
+ import { resolveCustomDirectory } from "../validate-directories.js";
9
14
  import { extractTokenFromEntry, OpenCodeAuth } from "./opencode-schema.js";
10
15
  const AGENT_ID = "opencode";
11
16
  const CREDS_FILE_NAME = "auth.json";
17
+ /**
18
+ * Gets the default auth file path for OpenCode.
19
+ *
20
+ * Uses axshared's resolveAgentDataDirectory which respects:
21
+ * 1. OPENCODE_DATA_DIR environment variable
22
+ * 2. XDG_DATA_HOME/opencode (defaults to ~/.local/share/opencode)
23
+ */
12
24
  function getDefaultAuthFilePath() {
13
- const xdgData = process.env.XDG_DATA_HOME ?? path.join(homedir(), ".local/share");
14
- return path.join(xdgData, "opencode", "auth.json");
25
+ const dataDirectory = resolveAgentDataDirectory(AGENT_ID);
26
+ return path.join(dataDirectory, CREDS_FILE_NAME);
15
27
  }
16
28
  /** OpenCode authentication adapter */
17
29
  const opencodeAdapter = {
@@ -71,11 +83,13 @@ const opencodeAdapter = {
71
83
  return undefined;
72
84
  },
73
85
  installCredentials(creds, options) {
86
+ // Resolve custom directory with validation (OpenCode supports separation)
87
+ const resolved = resolveCustomDirectory(AGENT_ID, options?.configDir, options?.dataDir);
88
+ if (!resolved.ok)
89
+ return resolved.error;
74
90
  // OpenCode only supports file storage (no keychain)
75
- // When configDir is provided, it should be the final opencode directory
76
- // (e.g., /tmp/test/opencode) - credentials go directly there
77
- const targetPath = options?.configDir
78
- ? path.join(options.configDir, CREDS_FILE_NAME)
91
+ const targetPath = resolved.customDir
92
+ ? path.join(resolved.customDir, CREDS_FILE_NAME)
79
93
  : getDefaultAuthFilePath();
80
94
  const targetDirectory = path.dirname(targetPath);
81
95
  try {
@@ -100,10 +114,13 @@ const opencodeAdapter = {
100
114
  }
101
115
  },
102
116
  removeCredentials(options) {
103
- // When configDir is provided, it should be the final opencode directory
104
- // (e.g., /tmp/test/opencode) - credentials are at configDir/auth.json
105
- const targetPath = options?.configDir
106
- ? path.join(options.configDir, CREDS_FILE_NAME)
117
+ // Resolve custom directory with validation (OpenCode supports separation)
118
+ const resolved = resolveCustomDirectory(AGENT_ID, options?.configDir, options?.dataDir);
119
+ if (!resolved.ok)
120
+ return resolved.error;
121
+ // Use resolved custom directory or fall back to default
122
+ const targetPath = resolved.customDir
123
+ ? path.join(resolved.customDir, CREDS_FILE_NAME)
107
124
  : getDefaultAuthFilePath();
108
125
  if (!existsSync(targetPath)) {
109
126
  return { ok: true, message: "No credentials file found" };
@@ -5,7 +5,7 @@
5
5
  * delegating to the appropriate adapter based on agent ID.
6
6
  */
7
7
  import { fromBase64, tryDecrypt } from "../crypto.js";
8
- import { claudeCodeAdapter } from "./agents/claude-code.js";
8
+ import { claudeCodeAdapter } from "./agents/claude.js";
9
9
  import { codexAdapter } from "./agents/codex.js";
10
10
  import { copilotAdapter } from "./agents/copilot.js";
11
11
  import { geminiAdapter } from "./agents/gemini.js";
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Directory validation for auth adapters.
3
+ *
4
+ * Wraps axshared's resolveCustomDirectories for use in auth adapters.
5
+ */
6
+ import type { AgentCli } from "axshared";
7
+ import type { OperationResult } from "./adapter.js";
8
+ /** Result of resolving custom directory with validation */
9
+ type ResolveResult = {
10
+ ok: true;
11
+ customDir: string | undefined;
12
+ } | {
13
+ ok: false;
14
+ error: OperationResult;
15
+ };
16
+ /**
17
+ * Validate directory options and resolve the custom data directory to use.
18
+ *
19
+ * Wraps axshared's resolveCustomDirectories, adapting the result for auth
20
+ * adapters and printing warnings to stderr.
21
+ *
22
+ * @see resolveCustomDirectories in axshared for behavior details
23
+ */
24
+ declare function resolveCustomDirectory(agentId: AgentCli, configDirectory: string | undefined, dataDirectory: string | undefined): ResolveResult;
25
+ export { resolveCustomDirectory };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Directory validation for auth adapters.
3
+ *
4
+ * Wraps axshared's resolveCustomDirectories for use in auth adapters.
5
+ */
6
+ import { resolveCustomDirectories } from "axshared";
7
+ /**
8
+ * Validate directory options and resolve the custom data directory to use.
9
+ *
10
+ * Wraps axshared's resolveCustomDirectories, adapting the result for auth
11
+ * adapters and printing warnings to stderr.
12
+ *
13
+ * @see resolveCustomDirectories in axshared for behavior details
14
+ */
15
+ function resolveCustomDirectory(agentId, configDirectory, dataDirectory) {
16
+ const result = resolveCustomDirectories(agentId, configDirectory, dataDirectory);
17
+ if (!result.ok) {
18
+ return {
19
+ ok: false,
20
+ error: { ok: false, message: result.error },
21
+ };
22
+ }
23
+ // Print warning to stderr if present
24
+ if (result.warning) {
25
+ console.error(`Warning: ${result.warning}`);
26
+ }
27
+ return { ok: true, customDir: result.dataDir };
28
+ }
29
+ export { resolveCustomDirectory };
package/dist/cli.js CHANGED
@@ -61,12 +61,13 @@ program
61
61
  .command("remove-credentials")
62
62
  .description("Remove credentials from storage")
63
63
  .requiredOption("-a, --agent <agent>", `Agent to remove credentials from (${AGENT_CLIS.join(", ")})`)
64
- .option("--config-dir <dir>", "Custom config directory (removes only this location, not keychain)")
65
- .option("--path <file>", "[DEPRECATED: use --config-dir] Custom file path")
64
+ .option("--config-dir <dir>", "Custom config directory (for agents without separation, also sets data location)")
65
+ .option("--data-dir <dir>", "Custom data directory for credentials")
66
66
  .action((options) => {
67
67
  handleAuthRemove({
68
68
  ...options,
69
- configDir: options.configDir ?? options.path,
69
+ configDir: options.configDir,
70
+ dataDir: options.dataDir,
70
71
  });
71
72
  });
72
73
  program
@@ -75,13 +76,14 @@ program
75
76
  .requiredOption("-a, --agent <agent>", `Agent to install credentials for (${AGENT_CLIS.join(", ")})`)
76
77
  .requiredOption("--input <file>", "Encrypted credentials file path")
77
78
  .option("--no-password", "Use default password (no prompt)")
78
- .option("--config-dir <dir>", "Custom config directory (forces file storage, ignores --storage)")
79
- .option("--path <file>", "[DEPRECATED: use --config-dir] Custom file path")
79
+ .option("--config-dir <dir>", "Custom config directory (for agents without separation, also sets data location)")
80
+ .option("--data-dir <dir>", "Custom data directory for credentials")
80
81
  .option("--storage <type>", "Storage type: keychain (macOS only) or file (for default location)")
81
82
  .action((options) => {
82
83
  void handleAuthInstall({
83
84
  ...options,
84
- configDir: options.configDir ?? options.path,
85
+ configDir: options.configDir,
86
+ dataDir: options.dataDir,
85
87
  });
86
88
  });
87
89
  await program.parseAsync(process.argv);
@@ -21,6 +21,7 @@ declare function handleAuthExport(options: AuthExportOptions): Promise<void>;
21
21
  interface AuthRemoveOptions {
22
22
  agent: string;
23
23
  configDir?: string;
24
+ dataDir?: string;
24
25
  }
25
26
  /** Handle auth remove-credentials command */
26
27
  declare function handleAuthRemove(options: AuthRemoveOptions): void;
@@ -81,7 +81,10 @@ function handleAuthRemove(options) {
81
81
  const agentId = validateAgent(options.agent);
82
82
  if (!agentId)
83
83
  return;
84
- const result = removeCredentials(agentId, { configDir: options.configDir });
84
+ const result = removeCredentials(agentId, {
85
+ configDir: options.configDir,
86
+ dataDir: options.dataDir,
87
+ });
85
88
  if (result.ok) {
86
89
  console.error(result.message);
87
90
  }
@@ -8,6 +8,7 @@ interface AuthInstallOptions {
8
8
  input: string;
9
9
  password: boolean;
10
10
  configDir?: string;
11
+ dataDir?: string;
11
12
  storage?: string;
12
13
  }
13
14
  /** Handle auth install-credentials command */
@@ -57,6 +57,7 @@ async function handleAuthInstall(options) {
57
57
  // Install credentials
58
58
  const result = installCredentials(creds, {
59
59
  configDir: options.configDir,
60
+ dataDir: options.dataDir,
60
61
  storage,
61
62
  });
62
63
  if (result.ok) {
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.1.0",
5
+ "version": "1.2.0",
6
6
  "description": "Authentication management library and CLI for AI coding agents",
7
7
  "repository": {
8
8
  "type": "git",
@@ -69,8 +69,8 @@
69
69
  "dependencies": {
70
70
  "@commander-js/extra-typings": "^14.0.0",
71
71
  "@inquirer/password": "^5.0.3",
72
- "axconfig": "^3.0.0",
73
- "axshared": "^1.2.0",
72
+ "axconfig": "^3.2.0",
73
+ "axshared": "^1.5.0",
74
74
  "commander": "^14.0.2",
75
75
  "zod": "^4.3.4"
76
76
  },