axauth 3.1.1 → 3.1.4

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 (34) hide show
  1. package/README.md +55 -44
  2. package/dist/auth/adapter.d.ts +2 -0
  3. package/dist/auth/agents/claude.js +2 -2
  4. package/dist/auth/agents/codex-auth-check.js +0 -5
  5. package/dist/auth/agents/codex.js +1 -1
  6. package/dist/auth/agents/copilot-storage.d.ts +15 -4
  7. package/dist/auth/agents/copilot-storage.js +39 -24
  8. package/dist/auth/agents/copilot.js +2 -4
  9. package/dist/auth/agents/gemini.js +0 -3
  10. package/dist/auth/agents/opencode-credentials.js +0 -2
  11. package/dist/auth/agents/opencode-storage.js +5 -4
  12. package/dist/auth/agents/opencode.js +0 -4
  13. package/dist/auth/build-refreshed-credentials.d.ts +6 -6
  14. package/dist/auth/build-refreshed-credentials.js +6 -46
  15. package/dist/auth/extract-creds-from-directory.js +1 -1
  16. package/dist/auth/install-from-environment.js +0 -7
  17. package/dist/auth/keychain.d.ts +5 -1
  18. package/dist/auth/keychain.js +29 -1
  19. package/dist/auth/refresh-credentials.d.ts +13 -6
  20. package/dist/auth/refresh-credentials.js +12 -13
  21. package/dist/auth/registry.d.ts +3 -3
  22. package/dist/auth/registry.js +10 -10
  23. package/dist/auth/wait-for-refreshed-credentials.d.ts +2 -1
  24. package/dist/cli.js +2 -2
  25. package/dist/commands/auth.js +2 -2
  26. package/dist/commands/encrypt.d.ts +1 -0
  27. package/dist/commands/encrypt.js +4 -18
  28. package/dist/commands/install-credentials.js +3 -17
  29. package/dist/commands/vault.js +5 -4
  30. package/dist/vault/vault-client.d.ts +4 -2
  31. package/dist/vault/vault-client.js +9 -40
  32. package/dist/vault/vault-config.d.ts +2 -6
  33. package/dist/vault/vault-config.js +6 -9
  34. package/package.json +7 -7
package/README.md CHANGED
@@ -24,13 +24,27 @@ axauth provides a consistent interface for managing credentials across multiple
24
24
  ## Installation
25
25
 
26
26
  ```bash
27
- npm install axauth
27
+ # CLI (global)
28
+ npm install -g axauth
28
29
  # or
29
- pnpm add axauth
30
+ pnpm add -g axauth
31
+
32
+ # Library usage (non-global)
33
+ npm install axauth
30
34
  ```
31
35
 
36
+ ## Prerequisites
37
+
38
+ - Node.js 22+
39
+ - pnpm or npm
40
+ - jq for JSON examples
41
+ - Agent CLIs installed (as needed): `claude`, `codex`, `gemini`, `opencode`, `copilot`
42
+ - POSIX shell assumed in examples (bash/zsh); PowerShell needs syntax adjustments
43
+
32
44
  ## CLI Usage
33
45
 
46
+ Examples use long-form flags; short flags exist but prefer long for clarity.
47
+
34
48
  ```bash
35
49
  # List agents and their auth status
36
50
  axauth list
@@ -53,18 +67,34 @@ axauth remove-credentials --agent claude
53
67
  axauth remove-credentials --agent claude --config-dir /tmp/config
54
68
  ```
55
69
 
70
+ ## Output Formats
71
+
72
+ - `axauth list` outputs TSV by default; `--json` returns a JSON array
73
+ - `axauth vault fetch` outputs JSON by default; `--json` pretty-prints JSON
74
+ - `axauth token` outputs the raw token for piping
75
+
56
76
  ### Pipeline Examples
57
77
 
58
- The CLI outputs TSV format for easy processing with standard Unix tools:
78
+ The `axauth list` command outputs TSV with a header row:
79
+
80
+ - Columns: `AGENT`, `STATUS`, `METHOD`
81
+ - `STATUS` values: `authenticated` | `not_configured`
59
82
 
60
83
  ```bash
61
84
  # List all agents and their auth status
62
85
  axauth list
63
- # AGENT STATUS METHOD
64
- # claude authenticated OAuth (max)
65
- # codex authenticated ChatGPT OAuth
66
- # ...
86
+ ```
67
87
 
88
+ Output (TSV):
89
+
90
+ ```
91
+ AGENT STATUS METHOD
92
+ claude authenticated OAuth (max)
93
+ codex authenticated ChatGPT OAuth
94
+ ...
95
+ ```
96
+
97
+ ```bash
68
98
  # Filter to show only authenticated agents
69
99
  axauth list | tail -n +2 | awk -F'\t' '$2 == "authenticated"'
70
100
 
@@ -80,6 +110,10 @@ curl -s -H "Authorization: Bearer $(axauth token --agent claude)" \
80
110
  https://api.anthropic.com/api/oauth/usage | jq .
81
111
  ```
82
112
 
113
+ ## Security Note: `--no-password`
114
+
115
+ `--no-password` uses a deterministic “default password” derived from the source code. This is intended for CI/CD convenience (e.g., storing the exported file as a secret), not for protecting credentials at rest.
116
+
83
117
  ## Library API
84
118
 
85
119
  ```typescript
@@ -170,9 +204,9 @@ Each agent adapter declares its storage capabilities:
170
204
  | Agent | Keychain | File | Environment | Install API Key |
171
205
  | -------- | :------: | :--: | :---------: | :-------------: |
172
206
  | claude | macOS | Yes | Yes | No (env-only) |
173
- | codex | macOS | Yes | Yes | No (env-only) |
207
+ | codex | macOS | Yes | Yes | Yes |
174
208
  | gemini | macOS | Yes | Yes | No (env-only) |
175
- | opencode | No | Yes | Yes | No |
209
+ | opencode | No | Yes | No | Yes |
176
210
  | copilot | macOS | Yes | Yes | No (env-only) |
177
211
 
178
212
  **Notes:**
@@ -215,47 +249,24 @@ For CI/CD workflows, credentials can be passed via environment variables:
215
249
 
216
250
  Use `installCredentialsFromEnvironmentVariable()` to install credentials from these variables programmatically.
217
251
 
218
- ## Config Directory Requirements
252
+ ## Custom Directory Behavior
219
253
 
220
- Some agents require specific directory name suffixes:
254
+ Directory handling depends on whether the agent separates config and data:
221
255
 
222
- | Agent | Directory Requirement | Example |
223
- | -------- | ------------------------ | -------------------- |
224
- | claude | Any name | `/tmp/my-config` |
225
- | codex | Any name | `/tmp/my-config` |
226
- | gemini | Must end with `.gemini` | `/tmp/home/.gemini` |
227
- | copilot | Must end with `.copilot` | `/tmp/home/.copilot` |
228
- | opencode | Must end with `opencode` | `/tmp/data/opencode` |
256
+ - **Shared config/data agents** (`claude`, `codex`, `gemini`, `copilot`): `--config-dir` and `--data-dir` are interchangeable and point to the same location.
257
+ - **Separate config/data agent** (`opencode`): `--config-dir` and `--data-dir` are independent.
258
+ If only one is provided, the other uses the default location and axauth emits a warning.
229
259
 
230
260
  ## Architecture
231
261
 
232
- axauth follows the adapter pattern with a functional core:
262
+ axauth follows an adapter architecture with a functional core:
233
263
 
234
- ```
235
- src/
236
- ├── index.ts # Public API exports
237
- ├── cli.ts # CLI entry point
238
- ├── crypto.ts # AES-256-GCM encryption
239
- ├── commands/
240
- │ └── auth.ts # CLI command handlers
241
- └── auth/
242
- ├── adapter.ts # AuthAdapter interface
243
- ├── types.ts # AuthStatus, Credentials types
244
- ├── registry.ts # Adapter registry and unified operations
245
- └── agents/ # Agent-specific adapters
246
- ├── claude-code.ts
247
- ├── claude-code-storage.ts
248
- ├── codex.ts
249
- ├── codex-storage.ts
250
- ├── codex-config.ts
251
- ├── gemini.ts
252
- ├── gemini-storage.ts
253
- ├── gemini-auth-check.ts
254
- ├── copilot.ts
255
- ├── copilot-storage.ts
256
- ├── copilot-auth-check.ts
257
- └── opencode.ts
258
- ```
264
+ - `src/auth/` — core auth domain logic, adapter interfaces, registry, shared utilities
265
+ - `src/auth/agents/` — agent-specific adapter implementations and storage/install/remove flows
266
+ - `src/commands/` CLI command handlers (`list`, `token`, `export`, `install-credentials`, `vault`, etc.)
267
+ - `src/vault/` axvault client/config integration for fetch/push workflows
268
+ - `src/crypto.ts` — credential encryption/decryption primitives (AES-256-GCM + PBKDF2)
269
+ - `src/index.ts` / `src/cli.ts` — library exports and CLI entrypoint
259
270
 
260
271
  ## Related Packages
261
272
 
@@ -51,6 +51,8 @@ interface InstallOptions {
51
51
  configDir?: string;
52
52
  /** Custom data directory for credentials */
53
53
  dataDir?: string;
54
+ /** Provider ID for multi-provider agents (required for OpenCode) */
55
+ provider?: string;
54
56
  }
55
57
  /**
56
58
  * Options for credential removal.
@@ -24,14 +24,14 @@ const claudeCodeAdapter = {
24
24
  const result = getEnvironmentCredentialsInternal();
25
25
  if (!result)
26
26
  return undefined;
27
- return { agent: AGENT_ID, type: result.type, data: result.data };
27
+ return { type: result.type, data: result.data };
28
28
  },
29
29
  findStoredCredentials() {
30
30
  const result = findStoredCredentialsInternal();
31
31
  if (!result)
32
32
  return undefined;
33
33
  return {
34
- credentials: { agent: AGENT_ID, type: result.type, data: result.data },
34
+ credentials: { type: result.type, data: result.data },
35
35
  source: result.source,
36
36
  };
37
37
  },
@@ -51,7 +51,6 @@ function getEnvironmentCredentials() {
51
51
  const environmentKey = getEnvironmentApiKey();
52
52
  if (environmentKey) {
53
53
  return {
54
- agent: AGENT_ID,
55
54
  type: "api-key",
56
55
  data: { apiKey: environmentKey },
57
56
  };
@@ -64,7 +63,6 @@ function extractKeychainCredentials() {
64
63
  if (keychainAuth?.OPENAI_API_KEY) {
65
64
  return {
66
65
  credentials: {
67
- agent: AGENT_ID,
68
66
  type: "api-key",
69
67
  data: { apiKey: keychainAuth.OPENAI_API_KEY },
70
68
  },
@@ -74,7 +72,6 @@ function extractKeychainCredentials() {
74
72
  if (keychainAuth?.tokens) {
75
73
  return {
76
74
  credentials: {
77
- agent: AGENT_ID,
78
75
  type: "oauth-credentials",
79
76
  data: keychainAuth,
80
77
  },
@@ -89,7 +86,6 @@ function extractFileCredentials() {
89
86
  if (fileAuth?.OPENAI_API_KEY) {
90
87
  return {
91
88
  credentials: {
92
- agent: AGENT_ID,
93
89
  type: "api-key",
94
90
  data: { apiKey: fileAuth.OPENAI_API_KEY },
95
91
  },
@@ -99,7 +95,6 @@ function extractFileCredentials() {
99
95
  if (fileAuth?.tokens) {
100
96
  return {
101
97
  credentials: {
102
- agent: AGENT_ID,
103
98
  type: "oauth-credentials",
104
99
  data: fileAuth,
105
100
  },
@@ -35,7 +35,7 @@ const codexAdapter = {
35
35
  const data = parsed;
36
36
  // Determine type from data structure
37
37
  const type = data.api_key ? "api-key" : "oauth-credentials";
38
- return { agent: AGENT_ID, type, data };
38
+ return { type, data };
39
39
  }
40
40
  catch {
41
41
  return undefined;
@@ -9,13 +9,24 @@
9
9
  declare function getConfigDirectory(): string;
10
10
  /** Get the default config file path */
11
11
  declare function getConfigFilePath(): string;
12
- /** Load token from keychain */
13
- declare function loadKeychainToken(host?: string): string | undefined;
12
+ /**
13
+ * Load token from keychain.
14
+ *
15
+ * Searches by service only — the Copilot CLI stores credentials under the
16
+ * GitHub username (from the OAuth response), which may differ in case from
17
+ * the OS username ($USER). Service-only search avoids that mismatch.
18
+ */
19
+ declare function loadKeychainToken(): string | undefined;
14
20
  /** Save token to keychain */
15
21
  declare function saveKeychainToken(token: string, host?: string): boolean;
16
22
  /** Delete token from keychain */
17
- declare function deleteKeychainToken(host?: string): boolean;
18
- /** Load token from config file */
23
+ declare function deleteKeychainToken(): boolean;
24
+ /**
25
+ * Load token from config file.
26
+ *
27
+ * Matches by host prefix — the Copilot CLI stores tokens under the GitHub
28
+ * username (from OAuth), which may differ in case from $USER.
29
+ */
19
30
  declare function loadFileToken(host?: string): string | undefined;
20
31
  /** Save token to config file */
21
32
  declare function saveFileToken(token: string, host?: string): boolean;
@@ -8,7 +8,7 @@
8
8
  import { existsSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
9
9
  import path from "node:path";
10
10
  import { ensureDirectory, loadJsonFile, saveJsonFile, } from "../file-storage.js";
11
- import { deleteFromKeychain, isMacOS, loadFromKeychain, saveToKeychain, } from "../keychain.js";
11
+ import { deleteFromKeychainByService, isMacOS, loadFromKeychainByService, saveToKeychain, } from "../keychain.js";
12
12
  import { getResolvedConfigDirectory } from "../resolve-config-directory.js";
13
13
  const KEYCHAIN_SERVICE = "copilot-cli";
14
14
  const DEFAULT_HOST = "https://github.com";
@@ -24,30 +24,28 @@ function getConfigFilePath() {
24
24
  function getUsername() {
25
25
  return process.env.USER ?? process.env.USERNAME ?? "user";
26
26
  }
27
- /** Build keychain account key */
28
- function getKeychainAccount(host = DEFAULT_HOST) {
29
- return `${host}:${getUsername()}`;
30
- }
31
- /** Load token from keychain */
32
- function loadKeychainToken(host = DEFAULT_HOST) {
33
- if (!isMacOS())
34
- return undefined;
35
- const account = getKeychainAccount(host);
36
- return loadFromKeychain(KEYCHAIN_SERVICE, account);
27
+ /**
28
+ * Load token from keychain.
29
+ *
30
+ * Searches by service only — the Copilot CLI stores credentials under the
31
+ * GitHub username (from the OAuth response), which may differ in case from
32
+ * the OS username ($USER). Service-only search avoids that mismatch.
33
+ */
34
+ function loadKeychainToken() {
35
+ return loadFromKeychainByService(KEYCHAIN_SERVICE);
37
36
  }
38
37
  /** Save token to keychain */
39
38
  function saveKeychainToken(token, host = DEFAULT_HOST) {
40
39
  if (!isMacOS())
41
40
  return false;
42
- const account = getKeychainAccount(host);
41
+ // Remove any existing entry first (handles username case mismatches)
42
+ deleteFromKeychainByService(KEYCHAIN_SERVICE);
43
+ const account = `${host}:${getUsername()}`;
43
44
  return saveToKeychain(KEYCHAIN_SERVICE, account, token);
44
45
  }
45
46
  /** Delete token from keychain */
46
- function deleteKeychainToken(host = DEFAULT_HOST) {
47
- if (!isMacOS())
48
- return false;
49
- const account = getKeychainAccount(host);
50
- return deleteFromKeychain(KEYCHAIN_SERVICE, account);
47
+ function deleteKeychainToken() {
48
+ return deleteFromKeychainByService(KEYCHAIN_SERVICE);
51
49
  }
52
50
  /** Load config file */
53
51
  function loadConfig() {
@@ -57,17 +55,36 @@ function loadConfig() {
57
55
  function saveConfig(config) {
58
56
  return saveJsonFile(getConfigFilePath(), config, { mode: 0o600 });
59
57
  }
60
- /** Load token from config file */
58
+ /** Find the first token key matching a host prefix */
59
+ function findTokenKeyForHost(tokens, host) {
60
+ const prefix = `${host}:`;
61
+ return Object.keys(tokens).find((key) => key.startsWith(prefix));
62
+ }
63
+ /**
64
+ * Load token from config file.
65
+ *
66
+ * Matches by host prefix — the Copilot CLI stores tokens under the GitHub
67
+ * username (from OAuth), which may differ in case from $USER.
68
+ */
61
69
  function loadFileToken(host = DEFAULT_HOST) {
62
70
  const config = loadConfig();
63
- return config?.copilot_tokens?.[`${host}:${getUsername()}`];
71
+ if (!config?.copilot_tokens)
72
+ return undefined;
73
+ const key = findTokenKeyForHost(config.copilot_tokens, host);
74
+ return key ? config.copilot_tokens[key] : undefined;
64
75
  }
65
76
  /** Save token to config file */
66
77
  function saveFileToken(token, host = DEFAULT_HOST) {
67
78
  const config = loadConfig() ?? {};
68
79
  const key = `${host}:${getUsername()}`;
80
+ // Rebuild tokens without any stale entry for this host (handles username case mismatches)
81
+ const existing = config.copilot_tokens ?? {};
82
+ const staleKey = findTokenKeyForHost(existing, host);
83
+ const filtered = staleKey && staleKey !== key
84
+ ? Object.fromEntries(Object.entries(existing).filter(([k]) => k !== staleKey))
85
+ : existing;
69
86
  config.copilot_tokens = {
70
- ...config.copilot_tokens,
87
+ ...filtered,
71
88
  [key]: token,
72
89
  };
73
90
  config.store_token_plaintext = true;
@@ -78,12 +95,10 @@ function deleteFileToken(host = DEFAULT_HOST) {
78
95
  const config = loadConfig();
79
96
  if (!config?.copilot_tokens)
80
97
  return false;
81
- const key = `${host}:${getUsername()}`;
82
- if (!(key in config.copilot_tokens))
98
+ const key = findTokenKeyForHost(config.copilot_tokens, host);
99
+ if (!key)
83
100
  return false;
84
- // Remove the token by filtering out the key
85
101
  const remainingTokens = Object.fromEntries(Object.entries(config.copilot_tokens).filter(([k]) => k !== key));
86
- // If no tokens left, remove the whole section
87
102
  if (Object.keys(remainingTokens).length === 0) {
88
103
  const rest = Object.fromEntries(Object.entries(config).filter(([k]) => k !== "copilot_tokens" && k !== "store_token_plaintext"));
89
104
  return saveConfig(rest);
@@ -27,8 +27,8 @@ const copilotAdapter = {
27
27
  }
28
28
  const methodMap = {
29
29
  environment: `Token (${result.environmentVariable})`,
30
- keychain: "Token (keychain)",
31
- file: "Token (file)",
30
+ keychain: "OAuth (keychain)",
31
+ file: "OAuth (file)",
32
32
  "gh-cli": "GitHub CLI",
33
33
  };
34
34
  return {
@@ -42,7 +42,6 @@ const copilotAdapter = {
42
42
  if (!token)
43
43
  return undefined;
44
44
  return {
45
- agent: AGENT_ID,
46
45
  type: "oauth-token",
47
46
  data: { accessToken: token },
48
47
  };
@@ -55,7 +54,6 @@ const copilotAdapter = {
55
54
  const source = result.source === "keychain" ? "keychain" : "file";
56
55
  return {
57
56
  credentials: {
58
- agent: AGENT_ID,
59
57
  type: "oauth-token",
60
58
  data: { accessToken: result.token },
61
59
  },
@@ -34,7 +34,6 @@ const geminiAdapter = {
34
34
  if (!result?.data)
35
35
  return undefined;
36
36
  return {
37
- agent: AGENT_ID,
38
37
  type: "api-key",
39
38
  data: result.data,
40
39
  };
@@ -47,7 +46,6 @@ const geminiAdapter = {
47
46
  if (result.source === "api-key" || result.source === "vertex-ai") {
48
47
  return {
49
48
  credentials: {
50
- agent: AGENT_ID,
51
49
  type: "api-key",
52
50
  data: result.data,
53
51
  },
@@ -57,7 +55,6 @@ const geminiAdapter = {
57
55
  // OAuth credentials from keychain or file - return source separately
58
56
  return {
59
57
  credentials: {
60
- agent: AGENT_ID,
61
58
  type: "oauth-credentials",
62
59
  data: result.data,
63
60
  },
@@ -44,8 +44,6 @@ function buildCredentials(provider, entry) {
44
44
  if (!parsed.success)
45
45
  return undefined;
46
46
  return {
47
- agent: AGENT_ID,
48
- provider,
49
47
  type: mapToCredentialType(parsed.data),
50
48
  data: entry,
51
49
  };
@@ -21,15 +21,16 @@ function installCredentials(creds, options) {
21
21
  if (!existsSync(targetDirectory)) {
22
22
  mkdirSync(targetDirectory, { recursive: true, mode: 0o700 });
23
23
  }
24
- // OpenCode requires per-provider credentials
25
- if (creds.agent !== "opencode" || !creds.provider) {
24
+ // OpenCode requires per-provider credentials via options
25
+ const rawProvider = options?.provider;
26
+ if (!rawProvider) {
26
27
  return {
27
28
  ok: false,
28
- message: "Bundled credential format is no longer supported for OpenCode; please provide per-provider credentials.",
29
+ message: "OpenCode requires provider option for credential installation.",
29
30
  };
30
31
  }
31
32
  // Validate provider key is non-empty (defensive check for programmatic use)
32
- const provider = creds.provider.trim();
33
+ const provider = rawProvider.trim();
33
34
  if (provider.length === 0) {
34
35
  return { ok: false, message: "Provider name cannot be empty" };
35
36
  }
@@ -76,9 +76,7 @@ const opencodeAdapter = {
76
76
  }
77
77
  return {
78
78
  credentials: {
79
- agent: AGENT_ID,
80
79
  type: mapToCredentialType(parsed.data),
81
- provider: normalizedProvider,
82
80
  data: entry,
83
81
  },
84
82
  source: "file",
@@ -108,9 +106,7 @@ const opencodeAdapter = {
108
106
  if (!parsed.success)
109
107
  return undefined;
110
108
  return {
111
- agent: AGENT_ID,
112
109
  type: mapToCredentialType(parsed.data),
113
- provider: targetProvider,
114
110
  data: entry,
115
111
  };
116
112
  }
@@ -1,18 +1,18 @@
1
1
  /**
2
- * Build refreshed credentials with provider context preserved.
2
+ * Build refreshed credentials from refresh operation output.
3
3
  */
4
4
  import type { Credentials } from "./types.js";
5
5
  /**
6
- * Build refreshed credentials, preserving provider context.
6
+ * Build refreshed credentials from the refresh operation.
7
7
  *
8
- * For OpenCode (per-provider), extracts the specific provider's refreshed data.
9
- * For other agents, uses the refreshed data directly.
8
+ * Returns the refreshed type and data. Provider context is handled
9
+ * by the caller, not embedded in credentials.
10
10
  *
11
- * @param originalCreds - Original credentials
11
+ * @param _originalCreds - Original credentials (reserved for future validation)
12
12
  * @param refreshedCreds - Credentials returned from refresh operation
13
13
  * @returns Built credentials or error message
14
14
  */
15
- declare function buildRefreshedCredentials(originalCreds: Credentials, refreshedCreds: Credentials): {
15
+ declare function buildRefreshedCredentials(_originalCreds: Credentials, refreshedCreds: Credentials): {
16
16
  ok: true;
17
17
  credentials: Credentials;
18
18
  } | {
@@ -1,60 +1,20 @@
1
1
  /**
2
- * Build refreshed credentials with provider context preserved.
2
+ * Build refreshed credentials from refresh operation output.
3
3
  */
4
4
  /**
5
- * Build refreshed credentials, preserving provider context.
5
+ * Build refreshed credentials from the refresh operation.
6
6
  *
7
- * For OpenCode (per-provider), extracts the specific provider's refreshed data.
8
- * For other agents, uses the refreshed data directly.
7
+ * Returns the refreshed type and data. Provider context is handled
8
+ * by the caller, not embedded in credentials.
9
9
  *
10
- * @param originalCreds - Original credentials
10
+ * @param _originalCreds - Original credentials (reserved for future validation)
11
11
  * @param refreshedCreds - Credentials returned from refresh operation
12
12
  * @returns Built credentials or error message
13
13
  */
14
- function buildRefreshedCredentials(originalCreds, refreshedCreds) {
15
- // OpenCode per-provider refresh: verify provider matches and use data directly
16
- if (originalCreds.agent === "opencode" && originalCreds.provider) {
17
- const normalizedProvider = originalCreds.provider.trim();
18
- // refreshedCreds from loadCredentialsFromDirectory is now per-provider format:
19
- // - refreshedCreds.provider = the provider name
20
- // - refreshedCreds.data = the provider's auth entry directly
21
- if (refreshedCreds.agent !== "opencode") {
22
- return {
23
- ok: false,
24
- error: `Provider '${normalizedProvider}' not found in refreshed credentials`,
25
- };
26
- }
27
- // TypeScript narrows to OpenCodeCredentials which has provider as required
28
- // (z.string().trim().min(1) in axshared) - NOT optional despite Credentials union
29
- if (refreshedCreds.provider.trim() !== normalizedProvider) {
30
- return {
31
- ok: false,
32
- error: `Provider '${normalizedProvider}' not found in refreshed credentials`,
33
- };
34
- }
35
- return {
36
- ok: true,
37
- credentials: {
38
- agent: originalCreds.agent,
39
- type: refreshedCreds.type,
40
- provider: normalizedProvider,
41
- data: refreshedCreds.data,
42
- },
43
- };
44
- }
45
- // Standard agent refresh: use refreshed data
46
- // Both originalCreds and refreshedCreds are standard agents here (not OpenCode)
47
- if (originalCreds.agent === "opencode") {
48
- // TypeScript narrowing: unreachable, but satisfies type checker
49
- return {
50
- ok: false,
51
- error: "Unexpected OpenCode credential without provider",
52
- };
53
- }
14
+ function buildRefreshedCredentials(_originalCreds, refreshedCreds) {
54
15
  return {
55
16
  ok: true,
56
17
  credentials: {
57
- agent: originalCreds.agent,
58
18
  type: refreshedCreds.type,
59
19
  data: refreshedCreds.data,
60
20
  },
@@ -31,7 +31,7 @@ function extractCredsFromDirectory(agentId, directory, fileName, transform) {
31
31
  const data = transform ? transform(record) : record;
32
32
  if (!data)
33
33
  return undefined;
34
- return { agent: agentId, type: "oauth-credentials", data };
34
+ return { type: "oauth-credentials", data };
35
35
  }
36
36
  catch {
37
37
  return undefined;
@@ -63,13 +63,6 @@ async function installCredentialsFromEnvironmentCore(parameters) {
63
63
  }
64
64
  // Install all credentials
65
65
  for (const credentials of credentialsList.credentials) {
66
- // Defense-in-depth: verify each credential matches the target agent
67
- if (credentials.agent !== parameters.agent) {
68
- return {
69
- ok: false,
70
- error: `Credential agent mismatch: expected '${parameters.agent}', got '${credentials.agent}'`,
71
- };
72
- }
73
66
  const result = parameters.installCredentials(credentials, {
74
67
  configDir: parameters.configDir,
75
68
  dataDir: parameters.dataDir,
@@ -9,4 +9,8 @@ declare function loadFromKeychain(service: string, account: string): string | un
9
9
  declare function saveToKeychain(service: string, account: string, data: string): boolean;
10
10
  /** Delete entry from macOS Keychain */
11
11
  declare function deleteFromKeychain(service: string, account: string): boolean;
12
- export { deleteFromKeychain, isMacOS, loadFromKeychain, saveToKeychain };
12
+ /** Load data from macOS Keychain by service only (any account) */
13
+ declare function loadFromKeychainByService(service: string): string | undefined;
14
+ /** Delete entry from macOS Keychain by service only (first match) */
15
+ declare function deleteFromKeychainByService(service: string): boolean;
16
+ export { deleteFromKeychain, deleteFromKeychainByService, isMacOS, loadFromKeychain, loadFromKeychainByService, saveToKeychain, };
@@ -53,4 +53,32 @@ function deleteFromKeychain(service, account) {
53
53
  return false;
54
54
  }
55
55
  }
56
- export { deleteFromKeychain, isMacOS, loadFromKeychain, saveToKeychain };
56
+ /** Load data from macOS Keychain by service only (any account) */
57
+ function loadFromKeychainByService(service) {
58
+ if (!isMacOS()) {
59
+ return undefined;
60
+ }
61
+ try {
62
+ const result = execFileSync("security", ["find-generic-password", "-s", service, "-w"], { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
63
+ return result.trim();
64
+ }
65
+ catch {
66
+ return undefined;
67
+ }
68
+ }
69
+ /** Delete entry from macOS Keychain by service only (first match) */
70
+ function deleteFromKeychainByService(service) {
71
+ if (!isMacOS()) {
72
+ return false;
73
+ }
74
+ try {
75
+ execFileSync("security", ["delete-generic-password", "-s", service], {
76
+ stdio: ["pipe", "pipe", "pipe"],
77
+ });
78
+ return true;
79
+ }
80
+ catch {
81
+ return false;
82
+ }
83
+ }
84
+ export { deleteFromKeychain, deleteFromKeychainByService, isMacOS, loadFromKeychain, loadFromKeychainByService, saveToKeychain, };