axauth 2.1.0 → 3.0.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.
Files changed (48) hide show
  1. package/README.md +10 -4
  2. package/dist/auth/adapter.d.ts +60 -20
  3. package/dist/auth/agents/claude-install.d.ts +11 -0
  4. package/dist/auth/agents/claude-install.js +80 -0
  5. package/dist/auth/agents/claude-storage.d.ts +13 -5
  6. package/dist/auth/agents/claude-storage.js +11 -16
  7. package/dist/auth/agents/claude.js +14 -89
  8. package/dist/auth/agents/codex-auth-check.d.ts +6 -5
  9. package/dist/auth/agents/codex-auth-check.js +34 -20
  10. package/dist/auth/agents/codex-install.js +1 -2
  11. package/dist/auth/agents/codex-storage.d.ts +0 -1
  12. package/dist/auth/agents/codex-storage.js +4 -5
  13. package/dist/auth/agents/codex.js +4 -4
  14. package/dist/auth/agents/copilot-install.js +2 -3
  15. package/dist/auth/agents/copilot.js +11 -6
  16. package/dist/auth/agents/gemini-install.d.ts +1 -1
  17. package/dist/auth/agents/gemini-install.js +3 -3
  18. package/dist/auth/agents/gemini.js +19 -17
  19. package/dist/auth/agents/opencode-credentials.d.ts +25 -37
  20. package/dist/auth/agents/opencode-credentials.js +48 -122
  21. package/dist/auth/agents/opencode-remove-provider.d.ts +25 -0
  22. package/dist/auth/agents/opencode-remove-provider.js +86 -0
  23. package/dist/auth/agents/opencode-storage.js +18 -22
  24. package/dist/auth/agents/opencode.d.ts +1 -1
  25. package/dist/auth/agents/opencode.js +59 -41
  26. package/dist/auth/build-refreshed-credentials.d.ts +6 -4
  27. package/dist/auth/build-refreshed-credentials.js +34 -19
  28. package/dist/auth/extract-creds-from-directory.d.ts +7 -3
  29. package/dist/auth/extract-creds-from-directory.js +4 -1
  30. package/dist/auth/refresh-credentials.d.ts +38 -12
  31. package/dist/auth/refresh-credentials.js +49 -18
  32. package/dist/auth/registry.d.ts +18 -15
  33. package/dist/auth/registry.js +35 -18
  34. package/dist/auth/resolve-refresh-credentials.d.ts +11 -8
  35. package/dist/auth/resolve-refresh-credentials.js +12 -48
  36. package/dist/auth/types.d.ts +3 -35
  37. package/dist/auth/types.js +2 -47
  38. package/dist/auth/wait-for-refreshed-credentials.d.ts +5 -1
  39. package/dist/auth/wait-for-refreshed-credentials.js +7 -4
  40. package/dist/commands/auth-export.js +24 -46
  41. package/dist/commands/auth.js +13 -3
  42. package/dist/commands/copy-to-clipboard.d.ts +6 -0
  43. package/dist/commands/copy-to-clipboard.js +48 -0
  44. package/dist/commands/vault.js +16 -15
  45. package/dist/index.d.ts +2 -2
  46. package/dist/index.js +2 -2
  47. package/dist/vault/vault-client.js +21 -2
  48. package/package.json +7 -7
@@ -65,17 +65,16 @@ function buildAuthContent(type, data) {
65
65
  if (type === "api-key") {
66
66
  return { OPENAI_API_KEY: data.apiKey };
67
67
  }
68
- const { _source: _unused, ...rest } = data;
69
- void _unused;
70
- if ("tokens" in rest) {
71
- const auth = rest;
68
+ // source is now at top-level, so data is clean
69
+ if ("tokens" in data) {
70
+ const auth = data;
72
71
  if (!auth.last_refresh) {
73
72
  auth.last_refresh = new Date().toISOString();
74
73
  }
75
74
  return auth;
76
75
  }
77
76
  return {
78
- tokens: rest,
77
+ tokens: data,
79
78
  last_refresh: new Date().toISOString(),
80
79
  };
81
80
  }
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { existsSync, readFileSync } from "node:fs";
7
7
  import path from "node:path";
8
- import { checkAuth, extractEnvironmentCredentials, extractRawCredentials, } from "./codex-auth-check.js";
8
+ import { checkAuth, findStoredCredentials, getEnvironmentCredentials, } from "./codex-auth-check.js";
9
9
  import { CREDS_FILE_NAME, installCodexCredentials, removeCodexCredentials, } from "./codex-install.js";
10
10
  const AGENT_ID = "codex";
11
11
  /** Codex authentication adapter */
@@ -18,9 +18,9 @@ const codexAdapter = {
18
18
  installApiKey: true,
19
19
  },
20
20
  checkAuth,
21
- extractFromEnvironment: extractEnvironmentCredentials,
22
- extractRawCredentials,
23
- extractRawCredentialsFromDirectory(options) {
21
+ getEnvironmentCredentials,
22
+ findStoredCredentials,
23
+ loadCredentialsFromDirectory(options) {
24
24
  // Codex doesn't separate config/data - use either directory
25
25
  const directory = options.dataDir ?? options.configDir;
26
26
  if (!directory)
@@ -40,9 +40,8 @@ function installCopilotCredentials(creds, resolved, options) {
40
40
  message: `Failed to install credentials to ${targetPath}`,
41
41
  };
42
42
  }
43
- // Default location: use storage option or _source marker
44
- const sourceMarker = creds.data._source;
45
- const targetStorage = options?.storage ?? (sourceMarker === "keychain" ? "keychain" : "file");
43
+ // Default location: use storage option or default to file
44
+ const targetStorage = options?.storage ?? "file";
46
45
  if (targetStorage === "keychain") {
47
46
  if (!isMacOS()) {
48
47
  return {
@@ -37,7 +37,7 @@ const copilotAdapter = {
37
37
  method: methodMap[result.source],
38
38
  };
39
39
  },
40
- extractFromEnvironment() {
40
+ getEnvironmentCredentials() {
41
41
  const token = getEnvironmentToken();
42
42
  if (!token)
43
43
  return undefined;
@@ -47,17 +47,22 @@ const copilotAdapter = {
47
47
  data: { accessToken: token },
48
48
  };
49
49
  },
50
- extractRawCredentials() {
50
+ findStoredCredentials() {
51
51
  const result = findFirstAvailableToken();
52
52
  if (!result)
53
53
  return undefined;
54
+ // Map TokenSource to CredentialSource (environment/gh-cli -> file)
55
+ const source = result.source === "keychain" ? "keychain" : "file";
54
56
  return {
55
- agent: AGENT_ID,
56
- type: "oauth-token",
57
- data: { accessToken: result.token, _source: result.source },
57
+ credentials: {
58
+ agent: AGENT_ID,
59
+ type: "oauth-token",
60
+ data: { accessToken: result.token },
61
+ },
62
+ source,
58
63
  };
59
64
  },
60
- extractRawCredentialsFromDirectory(options) {
65
+ loadCredentialsFromDirectory(options) {
61
66
  // Copilot doesn't separate config/data - use either directory
62
67
  const directory = options.dataDir ?? options.configDir;
63
68
  if (!directory)
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import type { InstallOptions, OperationResult, RemoveOptions } from "../adapter.js";
7
7
  /** Install OAuth credentials to storage */
8
- declare function installOAuthCredentials(oauthData: Record<string, unknown>, source: string | undefined, options?: InstallOptions): OperationResult;
8
+ declare function installOAuthCredentials(oauthData: Record<string, unknown>, options?: InstallOptions): OperationResult;
9
9
  /** Remove credentials from storage */
10
10
  declare function removeGeminiCredentials(options?: RemoveOptions): OperationResult;
11
11
  export { installOAuthCredentials, removeGeminiCredentials };
@@ -11,7 +11,7 @@ import { clearAuthTypeFromSettings, deleteKeychainCreds, deleteOAuthCreds, getDe
11
11
  const AGENT_ID = "gemini";
12
12
  const CREDS_FILE_NAME = "oauth_creds.json";
13
13
  /** Install OAuth credentials to storage */
14
- function installOAuthCredentials(oauthData, source, options) {
14
+ function installOAuthCredentials(oauthData, options) {
15
15
  // Resolve custom directory with validation
16
16
  const resolved = resolveCustomDirectory(AGENT_ID, options?.configDir, options?.dataDir);
17
17
  if (!resolved.ok)
@@ -20,8 +20,8 @@ function installOAuthCredentials(oauthData, source, options) {
20
20
  if (resolved.customDir) {
21
21
  return installToCustomDirectory(oauthData, resolved.customDir);
22
22
  }
23
- // Default location: use storage option or _source marker
24
- const targetStorage = options?.storage ?? (source === "keychain" ? "keychain" : "file");
23
+ // Default location: use storage option, default to file
24
+ const targetStorage = options?.storage ?? "file";
25
25
  if (targetStorage === "keychain") {
26
26
  return installToKeychain(oauthData);
27
27
  }
@@ -29,9 +29,9 @@ const geminiAdapter = {
29
29
  method: result.method,
30
30
  };
31
31
  },
32
- extractFromEnvironment() {
32
+ getEnvironmentCredentials() {
33
33
  const result = checkEnvironmentAuth();
34
- if (!result || !result.data)
34
+ if (!result?.data)
35
35
  return undefined;
36
36
  return {
37
37
  agent: AGENT_ID,
@@ -39,29 +39,32 @@ const geminiAdapter = {
39
39
  data: result.data,
40
40
  };
41
41
  },
42
- extractRawCredentials() {
42
+ findStoredCredentials() {
43
43
  const result = findCredentials();
44
- if (!result || !result.data)
44
+ if (!result?.data)
45
45
  return undefined;
46
- // Env-sourced credentials (api-key, vertex-ai) are read-only, no _source marker
46
+ // Env-sourced credentials (api-key, vertex-ai) are read-only, treat as file
47
47
  if (result.source === "api-key" || result.source === "vertex-ai") {
48
48
  return {
49
- agent: AGENT_ID,
50
- type: "api-key",
51
- data: result.data,
49
+ credentials: {
50
+ agent: AGENT_ID,
51
+ type: "api-key",
52
+ data: result.data,
53
+ },
54
+ source: "file",
52
55
  };
53
56
  }
54
- // OAuth credentials from keychain or file include _source for round-tripping
57
+ // OAuth credentials from keychain or file - return source separately
55
58
  return {
56
- agent: AGENT_ID,
57
- type: "oauth-credentials",
58
- data: {
59
- ...result.data,
60
- _source: result.source === "keychain" ? "keychain" : "file",
59
+ credentials: {
60
+ agent: AGENT_ID,
61
+ type: "oauth-credentials",
62
+ data: result.data,
61
63
  },
64
+ source: result.source === "keychain" ? "keychain" : "file",
62
65
  };
63
66
  },
64
- extractRawCredentialsFromDirectory(options) {
67
+ loadCredentialsFromDirectory(options) {
65
68
  // Gemini doesn't separate config/data - use either directory
66
69
  const directory = options.dataDir ?? options.configDir;
67
70
  if (!directory)
@@ -75,8 +78,7 @@ const geminiAdapter = {
75
78
  message: "API key credentials cannot be installed (use GEMINI_API_KEY env var)",
76
79
  };
77
80
  }
78
- const { _source, ...oauthData } = creds.data;
79
- return installOAuthCredentials(oauthData, _source, options);
81
+ return installOAuthCredentials(creds.data, options);
80
82
  },
81
83
  removeCredentials(options) {
82
84
  return removeGeminiCredentials(options);
@@ -1,36 +1,42 @@
1
1
  /**
2
2
  * OpenCode per-provider credential handling.
3
3
  *
4
- * Handles splitting, extracting, and removing individual provider credentials
5
- * from OpenCode's bundled auth.json format.
4
+ * Handles extracting and removing individual provider credentials
5
+ * from OpenCode's auth.json file.
6
6
  */
7
+ import type { CredentialType } from "axshared";
7
8
  import type { Credentials } from "../types.js";
9
+ import { OpenCodeAuth } from "./opencode-schema.js";
8
10
  declare const AGENT_ID: "opencode";
9
11
  declare const CREDS_FILE_NAME = "auth.json";
10
12
  /** Gets the default auth file path for OpenCode. */
11
13
  declare function getDefaultAuthFilePath(): string;
12
14
  /**
13
- * Split bundled OpenCode credentials into per-provider credentials.
15
+ * Maps OpenCode auth entry type to axauth credential type.
14
16
  *
15
- * Takes a bundled credential (all providers in `data`) and returns an array
16
- * of individual credentials, one per provider, with correct credential types.
17
+ * - `oauth` -> `oauth-credentials` (refreshable)
18
+ * - `api` -> `api-key` (static)
19
+ * - `wellknown` -> `api-key` (static, treated as API key for simplicity)
20
+ */
21
+ declare function mapToCredentialType(entry: OpenCodeAuth): CredentialType;
22
+ interface StoredCredentialsOptions {
23
+ /** Custom data directory (overrides default) */
24
+ dataDir?: string;
25
+ }
26
+ /**
27
+ * Get all stored credentials as per-provider entries.
17
28
  *
18
- * @example
19
- * // Input: { agent: "opencode", type: "oauth-credentials", data: { anthropic: {...}, cerebras: {...} } }
20
- * // Output: [
21
- * // { agent: "opencode", provider: "anthropic", type: "oauth-credentials", data: {...} },
22
- * // { agent: "opencode", provider: "cerebras", type: "api-key", data: {...} }
23
- * // ]
29
+ * Reads auth.json from disk and returns an array of per-provider credentials.
30
+ * Used by CLI commands that need to export or list all providers.
24
31
  */
25
- declare function splitToPerProviderCredentials(creds: Credentials): Credentials[];
32
+ declare function getAllStoredCredentials(options?: StoredCredentialsOptions): Credentials[];
26
33
  /**
27
- * Extract a single provider's credentials from bundled OpenCode credentials.
34
+ * Get stored credentials for a specific provider.
28
35
  *
29
- * @param creds - Bundled credentials with all providers
30
- * @param provider - The provider to extract (e.g., "anthropic", "openai")
31
- * @returns Per-provider credential or undefined if provider not found
36
+ * Reads auth.json from disk and returns credentials for the specified provider.
37
+ * Used by CLI commands that need a specific provider's credentials.
32
38
  */
33
- declare function extractProviderCredentials(creds: Credentials, provider: string): Credentials | undefined;
39
+ declare function getStoredCredentialsForProvider(provider: string, options?: StoredCredentialsOptions): Credentials | undefined;
34
40
  type ReadAuthResult = {
35
41
  ok: true;
36
42
  data: Record<string, unknown> | undefined;
@@ -46,23 +52,5 @@ type ReadAuthResult = {
46
52
  * Returns { ok: false, error: "..." } if file exists but cannot be read/parsed.
47
53
  */
48
54
  declare function readAuthFile(authFile: string): ReadAuthResult;
49
- interface RemoveProviderOptions {
50
- provider: string;
51
- configDir?: string;
52
- dataDir?: string;
53
- }
54
- interface RemoveProviderResult {
55
- ok: boolean;
56
- message: string;
57
- }
58
- /**
59
- * Remove a single provider from OpenCode's auth.json.
60
- *
61
- * Unlike removeCredentials which deletes the entire auth.json,
62
- * this function removes only the specified provider while preserving others.
63
- *
64
- * @param options - Options including the provider to remove
65
- * @returns Operation result with success/failure message
66
- */
67
- declare function removeProviderCredentials(options: RemoveProviderOptions): RemoveProviderResult;
68
- export { AGENT_ID, CREDS_FILE_NAME, extractProviderCredentials, getDefaultAuthFilePath, readAuthFile, removeProviderCredentials, splitToPerProviderCredentials, };
55
+ export { AGENT_ID, CREDS_FILE_NAME, getAllStoredCredentials, getDefaultAuthFilePath, getStoredCredentialsForProvider, mapToCredentialType, readAuthFile, };
56
+ export { removeProviderCredentials } from "./opencode-remove-provider.js";
@@ -1,13 +1,12 @@
1
1
  /**
2
2
  * OpenCode per-provider credential handling.
3
3
  *
4
- * Handles splitting, extracting, and removing individual provider credentials
5
- * from OpenCode's bundled auth.json format.
4
+ * Handles extracting and removing individual provider credentials
5
+ * from OpenCode's auth.json file.
6
6
  */
7
- import { readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
7
+ import { readFileSync } from "node:fs";
8
8
  import path from "node:path";
9
9
  import { resolveAgentDataDirectory } from "axshared";
10
- import { resolveCustomDirectory } from "../validate-directories.js";
11
10
  import { OpenCodeAuth } from "./opencode-schema.js";
12
11
  const AGENT_ID = "opencode";
13
12
  const CREDS_FILE_NAME = "auth.json";
@@ -33,61 +32,62 @@ function mapToCredentialType(entry) {
33
32
  }
34
33
  }
35
34
  }
35
+ /** Build auth file path from options */
36
+ function resolveAuthPath(options) {
37
+ return options?.dataDir
38
+ ? path.join(options.dataDir, CREDS_FILE_NAME)
39
+ : getDefaultAuthFilePath();
40
+ }
41
+ /** Build credentials object from provider entry */
42
+ function buildCredentials(provider, entry) {
43
+ const parsed = OpenCodeAuth.safeParse(entry);
44
+ if (!parsed.success)
45
+ return undefined;
46
+ return {
47
+ agent: AGENT_ID,
48
+ provider,
49
+ type: mapToCredentialType(parsed.data),
50
+ data: entry,
51
+ };
52
+ }
36
53
  /**
37
- * Split bundled OpenCode credentials into per-provider credentials.
38
- *
39
- * Takes a bundled credential (all providers in `data`) and returns an array
40
- * of individual credentials, one per provider, with correct credential types.
54
+ * Get all stored credentials as per-provider entries.
41
55
  *
42
- * @example
43
- * // Input: { agent: "opencode", type: "oauth-credentials", data: { anthropic: {...}, cerebras: {...} } }
44
- * // Output: [
45
- * // { agent: "opencode", provider: "anthropic", type: "oauth-credentials", data: {...} },
46
- * // { agent: "opencode", provider: "cerebras", type: "api-key", data: {...} }
47
- * // ]
56
+ * Reads auth.json from disk and returns an array of per-provider credentials.
57
+ * Used by CLI commands that need to export or list all providers.
48
58
  */
49
- function splitToPerProviderCredentials(creds) {
59
+ function getAllStoredCredentials(options) {
60
+ const authResult = readAuthFile(resolveAuthPath(options));
61
+ if (!authResult.ok || !authResult.data)
62
+ return [];
50
63
  const result = [];
51
- for (const [provider, entry] of Object.entries(creds.data)) {
52
- // Skip empty provider keys (could come from malformed auth.json)
53
- const normalizedProvider = provider.trim();
54
- if (normalizedProvider.length === 0)
55
- continue;
56
- const parsed = OpenCodeAuth.safeParse(entry);
57
- if (!parsed.success)
64
+ for (const [provider, entry] of Object.entries(authResult.data)) {
65
+ const normalized = provider.trim();
66
+ if (normalized.length === 0)
58
67
  continue;
59
- result.push({
60
- agent: AGENT_ID,
61
- provider: normalizedProvider,
62
- type: mapToCredentialType(parsed.data),
63
- data: entry,
64
- });
68
+ const creds = buildCredentials(normalized, entry);
69
+ if (creds)
70
+ result.push(creds);
65
71
  }
66
72
  return result;
67
73
  }
68
74
  /**
69
- * Extract a single provider's credentials from bundled OpenCode credentials.
75
+ * Get stored credentials for a specific provider.
70
76
  *
71
- * @param creds - Bundled credentials with all providers
72
- * @param provider - The provider to extract (e.g., "anthropic", "openai")
73
- * @returns Per-provider credential or undefined if provider not found
77
+ * Reads auth.json from disk and returns credentials for the specified provider.
78
+ * Used by CLI commands that need a specific provider's credentials.
74
79
  */
75
- function extractProviderCredentials(creds, provider) {
76
- const normalizedProvider = provider.trim();
77
- if (normalizedProvider.length === 0)
80
+ function getStoredCredentialsForProvider(provider, options) {
81
+ const normalized = provider.trim();
82
+ if (normalized.length === 0)
78
83
  return undefined;
79
- const entry = creds.data[normalizedProvider];
80
- if (!entry)
84
+ const authResult = readAuthFile(resolveAuthPath(options));
85
+ if (!authResult.ok || !authResult.data)
81
86
  return undefined;
82
- const parsed = OpenCodeAuth.safeParse(entry);
83
- if (!parsed.success)
87
+ const entry = authResult.data[normalized];
88
+ if (!entry)
84
89
  return undefined;
85
- return {
86
- agent: AGENT_ID,
87
- provider: normalizedProvider,
88
- type: mapToCredentialType(parsed.data),
89
- data: entry,
90
- };
90
+ return buildCredentials(normalized, entry);
91
91
  }
92
92
  /**
93
93
  * Read and parse the auth file.
@@ -123,80 +123,6 @@ function readAuthFile(authFile) {
123
123
  return { ok: false, error: `Failed to parse auth file: ${message}` };
124
124
  }
125
125
  }
126
- /**
127
- * Remove a single provider from OpenCode's auth.json.
128
- *
129
- * Unlike removeCredentials which deletes the entire auth.json,
130
- * this function removes only the specified provider while preserving others.
131
- *
132
- * @param options - Options including the provider to remove
133
- * @returns Operation result with success/failure message
134
- */
135
- function removeProviderCredentials(options) {
136
- // Normalize and validate provider name
137
- const provider = options.provider.trim();
138
- if (provider.length === 0) {
139
- return { ok: false, message: "Provider name cannot be empty" };
140
- }
141
- const resolved = resolveCustomDirectory(AGENT_ID, options.configDir, options.dataDir);
142
- if (!resolved.ok)
143
- return resolved.error;
144
- const targetPath = resolved.customDir
145
- ? path.join(resolved.customDir, CREDS_FILE_NAME)
146
- : getDefaultAuthFilePath();
147
- const authResult = readAuthFile(targetPath);
148
- if (!authResult.ok) {
149
- return { ok: false, message: authResult.error };
150
- }
151
- if (!authResult.data) {
152
- return { ok: true, message: "No credentials file found" };
153
- }
154
- if (!Object.hasOwn(authResult.data, provider)) {
155
- return {
156
- ok: true,
157
- message: `No credentials found for provider '${provider}'`,
158
- };
159
- }
160
- // Remove the provider by filtering entries
161
- const remaining = Object.fromEntries(Object.entries(authResult.data).filter(([key]) => key !== provider));
162
- try {
163
- if (Object.keys(remaining).length === 0) {
164
- // No providers left, delete the file
165
- unlinkSync(targetPath);
166
- return {
167
- ok: true,
168
- message: `Removed provider '${provider}' (file deleted, no remaining providers)`,
169
- };
170
- }
171
- // Write back without the removed provider
172
- const temporaryFile = `${targetPath}.tmp.${Date.now()}`;
173
- try {
174
- writeFileSync(temporaryFile, JSON.stringify(remaining, undefined, 2), {
175
- mode: 0o600,
176
- });
177
- renameSync(temporaryFile, targetPath);
178
- }
179
- catch (error) {
180
- // Clean up temp file on failure
181
- try {
182
- unlinkSync(temporaryFile);
183
- }
184
- catch {
185
- // Ignore cleanup errors
186
- }
187
- throw error;
188
- }
189
- return {
190
- ok: true,
191
- message: `Removed provider '${provider}' from ${targetPath}`,
192
- };
193
- }
194
- catch (error) {
195
- const message = error instanceof Error ? error.message : String(error);
196
- return {
197
- ok: false,
198
- message: `Failed to remove provider credentials: ${message}`,
199
- };
200
- }
201
- }
202
- export { AGENT_ID, CREDS_FILE_NAME, extractProviderCredentials, getDefaultAuthFilePath, readAuthFile, removeProviderCredentials, splitToPerProviderCredentials, };
126
+ export { AGENT_ID, CREDS_FILE_NAME, getAllStoredCredentials, getDefaultAuthFilePath, getStoredCredentialsForProvider, mapToCredentialType, readAuthFile, };
127
+ // Re-export from extracted module
128
+ export { removeProviderCredentials } from "./opencode-remove-provider.js";
@@ -0,0 +1,25 @@
1
+ /**
2
+ * OpenCode provider removal.
3
+ *
4
+ * Extracted from opencode-credentials.ts for complexity reduction.
5
+ */
6
+ interface RemoveProviderOptions {
7
+ provider: string;
8
+ configDir?: string;
9
+ dataDir?: string;
10
+ }
11
+ interface RemoveProviderResult {
12
+ ok: boolean;
13
+ message: string;
14
+ }
15
+ /**
16
+ * Remove a single provider from OpenCode's auth.json.
17
+ *
18
+ * Unlike removeCredentials which deletes the entire auth.json,
19
+ * this function removes only the specified provider while preserving others.
20
+ *
21
+ * @param options - Options including the provider to remove
22
+ * @returns Operation result with success/failure message
23
+ */
24
+ declare function removeProviderCredentials(options: RemoveProviderOptions): RemoveProviderResult;
25
+ export { removeProviderCredentials };
@@ -0,0 +1,86 @@
1
+ /**
2
+ * OpenCode provider removal.
3
+ *
4
+ * Extracted from opencode-credentials.ts for complexity reduction.
5
+ */
6
+ import { renameSync, unlinkSync, writeFileSync } from "node:fs";
7
+ import path from "node:path";
8
+ import { resolveCustomDirectory } from "../validate-directories.js";
9
+ import { AGENT_ID, CREDS_FILE_NAME, getDefaultAuthFilePath, readAuthFile, } from "./opencode-credentials.js";
10
+ /**
11
+ * Remove a single provider from OpenCode's auth.json.
12
+ *
13
+ * Unlike removeCredentials which deletes the entire auth.json,
14
+ * this function removes only the specified provider while preserving others.
15
+ *
16
+ * @param options - Options including the provider to remove
17
+ * @returns Operation result with success/failure message
18
+ */
19
+ function removeProviderCredentials(options) {
20
+ // Normalize and validate provider name
21
+ const provider = options.provider.trim();
22
+ if (provider.length === 0) {
23
+ return { ok: false, message: "Provider name cannot be empty" };
24
+ }
25
+ const resolved = resolveCustomDirectory(AGENT_ID, options.configDir, options.dataDir);
26
+ if (!resolved.ok)
27
+ return resolved.error;
28
+ const targetPath = resolved.customDir
29
+ ? path.join(resolved.customDir, CREDS_FILE_NAME)
30
+ : getDefaultAuthFilePath();
31
+ const authResult = readAuthFile(targetPath);
32
+ if (!authResult.ok) {
33
+ return { ok: false, message: authResult.error };
34
+ }
35
+ if (!authResult.data) {
36
+ return { ok: true, message: "No credentials file found" };
37
+ }
38
+ if (!Object.hasOwn(authResult.data, provider)) {
39
+ return {
40
+ ok: true,
41
+ message: `No credentials found for provider '${provider}'`,
42
+ };
43
+ }
44
+ // Remove the provider by filtering entries
45
+ const remaining = Object.fromEntries(Object.entries(authResult.data).filter(([key]) => key !== provider));
46
+ try {
47
+ if (Object.keys(remaining).length === 0) {
48
+ // No providers left, delete the file
49
+ unlinkSync(targetPath);
50
+ return {
51
+ ok: true,
52
+ message: `Removed provider '${provider}' (file deleted, no remaining providers)`,
53
+ };
54
+ }
55
+ // Write back without the removed provider
56
+ const temporaryFile = `${targetPath}.tmp.${Date.now()}`;
57
+ try {
58
+ writeFileSync(temporaryFile, JSON.stringify(remaining, undefined, 2), {
59
+ mode: 0o600,
60
+ });
61
+ renameSync(temporaryFile, targetPath);
62
+ }
63
+ catch (error) {
64
+ // Clean up temp file on failure
65
+ try {
66
+ unlinkSync(temporaryFile);
67
+ }
68
+ catch {
69
+ // Ignore cleanup errors
70
+ }
71
+ throw error;
72
+ }
73
+ return {
74
+ ok: true,
75
+ message: `Removed provider '${provider}' from ${targetPath}`,
76
+ };
77
+ }
78
+ catch (error) {
79
+ const message = error instanceof Error ? error.message : String(error);
80
+ return {
81
+ ok: false,
82
+ message: `Failed to remove provider credentials: ${message}`,
83
+ };
84
+ }
85
+ }
86
+ export { removeProviderCredentials };
@@ -21,29 +21,25 @@ function installCredentials(creds, options) {
21
21
  if (!existsSync(targetDirectory)) {
22
22
  mkdirSync(targetDirectory, { recursive: true, mode: 0o700 });
23
23
  }
24
- let authData;
25
- // Strip internal axauth markers before writing to agent file
26
- // _source is used for round-tripping but should not be persisted to OpenCode's auth.json
27
- const cleanData = { ...creds.data };
28
- delete cleanData._source;
29
- if (creds.provider) {
30
- // Validate provider key is non-empty (defensive check for programmatic use)
31
- const provider = creds.provider.trim();
32
- if (provider.length === 0) {
33
- return { ok: false, message: "Provider name cannot be empty" };
34
- }
35
- // Per-provider credential: merge into existing auth.json at provider's slot
36
- const authResult = readAuthFile(targetPath);
37
- if (!authResult.ok) {
38
- return { ok: false, message: authResult.error };
39
- }
40
- const existingAuth = authResult.data ?? {};
41
- authData = { ...existingAuth, [provider]: cleanData };
24
+ // OpenCode requires per-provider credentials
25
+ if (creds.agent !== "opencode" || !creds.provider) {
26
+ return {
27
+ ok: false,
28
+ message: "Bundled credential format is no longer supported for OpenCode; please provide per-provider credentials.",
29
+ };
30
+ }
31
+ // Validate provider key is non-empty (defensive check for programmatic use)
32
+ const provider = creds.provider.trim();
33
+ if (provider.length === 0) {
34
+ return { ok: false, message: "Provider name cannot be empty" };
42
35
  }
43
- else {
44
- // Bundled credential: replace entire auth.json (legacy format)
45
- authData = cleanData;
36
+ // Per-provider credential: merge into existing auth.json at provider's slot
37
+ const authResult = readAuthFile(targetPath);
38
+ if (!authResult.ok) {
39
+ return { ok: false, message: authResult.error };
46
40
  }
41
+ const existingAuth = authResult.data ?? {};
42
+ const authData = { ...existingAuth, [provider]: creds.data };
47
43
  // OpenCode stores auth data directly as the file content
48
44
  const temporaryFile = `${targetPath}.tmp.${Date.now()}`;
49
45
  try {
@@ -62,7 +58,7 @@ function installCredentials(creds, options) {
62
58
  }
63
59
  throw error;
64
60
  }
65
- const providerInfo = creds.provider ? ` (${creds.provider.trim()})` : "";
61
+ const providerInfo = ` (${provider})`;
66
62
  return {
67
63
  ok: true,
68
64
  message: `Installed credentials${providerInfo} to ${targetPath}`,