axauth 3.1.2 → 3.1.5

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.
@@ -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, };
@@ -4,6 +4,7 @@
4
4
  * Refreshes credentials by running the agent in an isolated temp config,
5
5
  * triggering its internal auth refresh mechanism.
6
6
  */
7
+ import type { AgentCli } from "axshared";
7
8
  import { type Credentials } from "./types.js";
8
9
  /** Options for credential refresh */
9
10
  interface RefreshOptions {
@@ -32,7 +33,7 @@ type RefreshResult = {
32
33
  * @param options - Refresh options (timeout, provider for multi-provider agents)
33
34
  * @returns RefreshResult with new credentials or error
34
35
  */
35
- declare function refreshCredentials(creds: Credentials, options?: RefreshOptions): Promise<RefreshResult>;
36
+ declare function refreshCredentials(agentId: AgentCli, creds: Credentials, options?: RefreshOptions): Promise<RefreshResult>;
36
37
  /** Result of refresh and persist operation */
37
38
  type RefreshAndPersistResult = {
38
39
  ok: true;
@@ -42,7 +43,7 @@ type RefreshAndPersistResult = {
42
43
  staleCredentials: Credentials;
43
44
  };
44
45
  /** Install function type for refreshAndPersist */
45
- type InstallFunction = (creds: Credentials, options?: {
46
+ type InstallFunction = (agentId: AgentCli, creds: Credentials, options?: {
46
47
  storage?: "keychain" | "file";
47
48
  }) => {
48
49
  ok: boolean;
@@ -55,12 +56,13 @@ type InstallFunction = (creds: Credentials, options?: {
55
56
  * 1. Refreshes credentials via agent subprocess
56
57
  * 2. Persists refreshed credentials to the specified storage location
57
58
  *
59
+ * @param agentId - Agent identifier
58
60
  * @param creds - Original credentials
59
61
  * @param installFunction - Function to install credentials (avoids circular import)
60
62
  * @param options - Refresh options including storage type
61
63
  * @returns Refreshed credentials or original credentials on failure
62
64
  */
63
- declare function refreshAndPersist(creds: Credentials, installFunction: InstallFunction, options?: RefreshOptions): Promise<RefreshAndPersistResult>;
65
+ declare function refreshAndPersist(agentId: AgentCli, creds: Credentials, installFunction: InstallFunction, options?: RefreshOptions): Promise<RefreshAndPersistResult>;
64
66
  /** Result of refreshing an opaque credential blob */
65
67
  type RefreshBlobResult = {
66
68
  ok: true;
@@ -70,6 +72,11 @@ type RefreshBlobResult = {
70
72
  ok: false;
71
73
  error: string;
72
74
  };
75
+ /** Options for refreshBlob, extending refresh options with routing metadata */
76
+ interface RefreshBlobOptions extends RefreshOptions {
77
+ /** Agent identifier (required — comes from vault column) */
78
+ agent: AgentCli;
79
+ }
73
80
  /**
74
81
  * Refresh credentials from an opaque blob.
75
82
  *
@@ -78,9 +85,9 @@ type RefreshBlobResult = {
78
85
  * never needs to understand the blob's internal structure.
79
86
  *
80
87
  * @param blob - Opaque credential blob (expected to be a Credentials object)
81
- * @param options - Refresh options (timeout)
88
+ * @param options - Refresh options with agent and optional provider
82
89
  * @returns RefreshBlobResult with new blob and expiresAt, or error
83
90
  */
84
- declare function refreshBlob(blob: unknown, options?: RefreshOptions): Promise<RefreshBlobResult>;
91
+ declare function refreshBlob(blob: unknown, options: RefreshBlobOptions): Promise<RefreshBlobResult>;
85
92
  export { refreshAndPersist, refreshBlob, refreshCredentials };
86
93
  export type { RefreshAndPersistResult, RefreshBlobResult, RefreshOptions, RefreshResult, };
@@ -32,7 +32,7 @@ async function safeCleanup(directory) {
32
32
  * @param options - Refresh options (timeout, provider for multi-provider agents)
33
33
  * @returns RefreshResult with new credentials or error
34
34
  */
35
- async function refreshCredentials(creds, options) {
35
+ async function refreshCredentials(agentId, creds, options) {
36
36
  const timeoutMs = options?.timeout ?? DEFAULT_REFRESH_TIMEOUT_MS;
37
37
  const deadlineMs = Date.now() + timeoutMs;
38
38
  const resolved = resolveRefreshCredentials(creds);
@@ -41,12 +41,8 @@ async function refreshCredentials(creds, options) {
41
41
  }
42
42
  // Run agent with a minimal prompt to trigger internal token refresh.
43
43
  // preserveConfigDirectory keeps the temp dir so we can read refreshed credentials.
44
- // For OpenCode, use explicit provider option or fall back to credentials.provider.
45
- const provider = options?.provider ??
46
- (resolved.credentials.agent === "opencode"
47
- ? resolved.credentials.provider
48
- : undefined);
49
- const resultPromise = runAgent(resolved.credentials.agent, {
44
+ const provider = options?.provider;
45
+ const resultPromise = runAgent(agentId, {
50
46
  prompt: "ping",
51
47
  credentials: resolved.credentials,
52
48
  provider,
@@ -96,7 +92,7 @@ async function refreshCredentials(creds, options) {
96
92
  if (!directories) {
97
93
  return { ok: false, error: "No directories in execution metadata" };
98
94
  }
99
- const refreshedCredentials = await waitForRefreshedCredentials(resolved.credentials.agent, directories, deadlineMs, { provider });
95
+ const refreshedCredentials = await waitForRefreshedCredentials(agentId, directories, deadlineMs, { provider });
100
96
  if (!refreshedCredentials) {
101
97
  return { ok: false, error: "No credentials found after refresh" };
102
98
  }
@@ -123,20 +119,23 @@ async function refreshCredentials(creds, options) {
123
119
  * 1. Refreshes credentials via agent subprocess
124
120
  * 2. Persists refreshed credentials to the specified storage location
125
121
  *
122
+ * @param agentId - Agent identifier
126
123
  * @param creds - Original credentials
127
124
  * @param installFunction - Function to install credentials (avoids circular import)
128
125
  * @param options - Refresh options including storage type
129
126
  * @returns Refreshed credentials or original credentials on failure
130
127
  */
131
- async function refreshAndPersist(creds, installFunction, options) {
132
- const result = await refreshCredentials(creds, options);
128
+ async function refreshAndPersist(agentId, creds, installFunction, options) {
129
+ const result = await refreshCredentials(agentId, creds, options);
133
130
  if (!result.ok) {
134
131
  console.error(`Warning: Token refresh failed: ${result.error}`);
135
132
  return { ok: false, staleCredentials: creds };
136
133
  }
137
134
  // Persist refreshed credentials back to storage
138
135
  const storage = options?.storage ?? "file";
139
- const installResult = installFunction(result.credentials, { storage });
136
+ const installResult = installFunction(agentId, result.credentials, {
137
+ storage,
138
+ });
140
139
  if (!installResult.ok) {
141
140
  console.error(`Warning: Failed to save refreshed credentials: ${installResult.message}`);
142
141
  }
@@ -150,7 +149,7 @@ async function refreshAndPersist(creds, installFunction, options) {
150
149
  * never needs to understand the blob's internal structure.
151
150
  *
152
151
  * @param blob - Opaque credential blob (expected to be a Credentials object)
153
- * @param options - Refresh options (timeout)
152
+ * @param options - Refresh options with agent and optional provider
154
153
  * @returns RefreshBlobResult with new blob and expiresAt, or error
155
154
  */
156
155
  async function refreshBlob(blob, options) {
@@ -160,7 +159,7 @@ async function refreshBlob(blob, options) {
160
159
  return { ok: false, error: "Invalid credential format" };
161
160
  }
162
161
  // Refresh using existing function
163
- const result = await refreshCredentials(credentials, options);
162
+ const result = await refreshCredentials(options.agent, credentials, options);
164
163
  if (!result.ok) {
165
164
  return { ok: false, error: result.error };
166
165
  }
@@ -92,7 +92,7 @@ declare function findCredentialsFromDirectory(agentId: AgentCli, options: Extrac
92
92
  * const result = installCredentials(creds, { storage: "keychain" });
93
93
  * if (!result.ok) { console.error(result.message); }
94
94
  */
95
- declare function installCredentials(creds: Credentials, options?: InstallOptions): OperationResult;
95
+ declare function installCredentials(agentId: AgentCli, creds: Credentials, options?: InstallOptions): OperationResult;
96
96
  /**
97
97
  * Remove credentials for an agent.
98
98
  *
@@ -117,7 +117,7 @@ declare function removeCredentials(agentId: AgentCli, options?: RemoveOptions):
117
117
  * const token = await getAccessToken(creds);
118
118
  * const token = await getAccessToken(creds, { skipRefresh: true });
119
119
  */
120
- declare function getAccessToken(creds: Credentials, options?: TokenOptions): Promise<string | undefined>;
120
+ declare function getAccessToken(agentId: AgentCli, creds: Credentials, options?: TokenOptions): Promise<string | undefined>;
121
121
  /**
122
122
  * Get access token for an agent by ID.
123
123
  *
@@ -136,7 +136,7 @@ declare function getAgentAccessToken(agentId: AgentCli, options?: TokenOptions):
136
136
  * const env = credentialsToEnvironment(creds);
137
137
  * Object.assign(process.env, env);
138
138
  */
139
- declare function credentialsToEnvironment(creds: Credentials): Record<string, string>;
139
+ declare function credentialsToEnvironment(agentId: AgentCli, creds: Credentials): Record<string, string>;
140
140
  /**
141
141
  * Get the conventional environment variable name for agent credentials.
142
142
  *
@@ -128,8 +128,8 @@ function findCredentialsFromDirectory(agentId, options) {
128
128
  * const result = installCredentials(creds, { storage: "keychain" });
129
129
  * if (!result.ok) { console.error(result.message); }
130
130
  */
131
- function installCredentials(creds, options) {
132
- return ADAPTERS[creds.agent].installCredentials(creds, options);
131
+ function installCredentials(agentId, creds, options) {
132
+ return ADAPTERS[agentId].installCredentials(creds, options);
133
133
  }
134
134
  /**
135
135
  * Remove credentials for an agent.
@@ -157,8 +157,8 @@ function removeCredentials(agentId, options) {
157
157
  * const token = await getAccessToken(creds);
158
158
  * const token = await getAccessToken(creds, { skipRefresh: true });
159
159
  */
160
- async function getAccessToken(creds, options) {
161
- const token = ADAPTERS[creds.agent].getAccessToken(creds, options);
160
+ async function getAccessToken(agentId, creds, options) {
161
+ const token = ADAPTERS[agentId].getAccessToken(creds, options);
162
162
  // No token found
163
163
  if (!token)
164
164
  return undefined;
@@ -177,13 +177,13 @@ async function getAccessToken(creds, options) {
177
177
  if (expired !== true)
178
178
  return token; // Valid or no expiry info
179
179
  // Refresh and persist
180
- const result = await refreshAndPersist(creds, installCredentials, {
180
+ const result = await refreshAndPersist(agentId, creds, (aid, c, installOptions) => installCredentials(aid, c, installOptions), {
181
181
  timeout: options?.refreshTimeout,
182
182
  provider: options?.provider,
183
183
  storage: options?.storage,
184
184
  });
185
185
  const finalCreds = result.ok ? result.credentials : result.staleCredentials;
186
- return ADAPTERS[creds.agent].getAccessToken(finalCreds, options);
186
+ return ADAPTERS[agentId].getAccessToken(finalCreds, options);
187
187
  }
188
188
  /**
189
189
  * Get access token for an agent by ID.
@@ -202,7 +202,7 @@ async function getAgentAccessToken(agentId, options) {
202
202
  if (!stored)
203
203
  return undefined;
204
204
  // Pass the source to getAccessToken so refresh preserves storage location
205
- return getAccessToken(stored.credentials, {
205
+ return getAccessToken(agentId, stored.credentials, {
206
206
  ...options,
207
207
  storage: options?.storage ?? stored.source,
208
208
  });
@@ -214,8 +214,8 @@ async function getAgentAccessToken(agentId, options) {
214
214
  * const env = credentialsToEnvironment(creds);
215
215
  * Object.assign(process.env, env);
216
216
  */
217
- function credentialsToEnvironment(creds) {
218
- return ADAPTERS[creds.agent].credentialsToEnvironment(creds);
217
+ function credentialsToEnvironment(agentId, creds) {
218
+ return ADAPTERS[agentId].credentialsToEnvironment(creds);
219
219
  }
220
220
  /**
221
221
  * Get the conventional environment variable name for agent credentials.
@@ -251,7 +251,7 @@ function getCredentialsEnvironmentVariableName(agent) {
251
251
  function installCredentialsFromEnvironmentVariable(agent, options) {
252
252
  return installCredentialsFromEnvironmentCore({
253
253
  agent,
254
- installCredentials,
254
+ installCredentials: (creds, installOptions) => installCredentials(agent, creds, installOptions),
255
255
  configDir: options?.configDir,
256
256
  dataDir: options?.dataDir,
257
257
  });
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * Polls for refreshed credentials to appear after agent execution.
3
3
  */
4
+ import type { AgentCli } from "axshared";
4
5
  import type { ExecutionDirectories } from "axexec";
5
6
  import type { Credentials } from "./types.js";
6
7
  interface WaitOptions {
7
8
  /** Provider ID for multi-provider agents (OpenCode) */
8
9
  provider?: string;
9
10
  }
10
- declare function waitForRefreshedCredentials(agent: Credentials["agent"], directories: ExecutionDirectories, deadlineMs: number, options?: WaitOptions): Promise<Credentials | undefined>;
11
+ declare function waitForRefreshedCredentials(agent: AgentCli, directories: ExecutionDirectories, deadlineMs: number, options?: WaitOptions): Promise<Credentials | undefined>;
11
12
  export { waitForRefreshedCredentials };
package/dist/cli.js CHANGED
@@ -84,6 +84,7 @@ program
84
84
  program
85
85
  .command("encrypt")
86
86
  .description("Encrypt raw credentials to encrypted format")
87
+ .requiredOption("-a, --agent <agent>", `Agent the credentials belong to (${AGENT_CLIS.join(", ")})`)
87
88
  .requiredOption("--input <file>", "Raw credentials file path")
88
89
  .option("--output <file>", "Output file path (use - for stdout)")
89
90
  .option("--no-password", "Use default password (no prompt)")
@@ -161,12 +162,11 @@ vault
161
162
  .option("--json", "Pretty-print JSON output")
162
163
  .addHelpText("after", `
163
164
  Environment variables (option 1 - single JSON):
164
- AXVAULT JSON config: {"url":"...","apiKey":"...","credentialName":"..."}
165
+ AXVAULT JSON config: {"url":"...","apiKey":"..."}
165
166
 
166
167
  Environment variables (option 2 - individual):
167
168
  AXVAULT_URL Vault server URL (required)
168
169
  AXVAULT_API_KEY API key for authentication (required)
169
- AXVAULT_CREDENTIAL Default credential name (optional)
170
170
 
171
171
  Output modes:
172
172
  (default) Output credentials as JSON
@@ -37,7 +37,7 @@ async function handleAuthToken(options) {
37
37
  return;
38
38
  }
39
39
  // Extract the token based on credential type
40
- const token = await getAccessToken(creds, { provider: options.provider });
40
+ const token = await getAccessToken(agentId, creds, { provider: options.provider });
41
41
  if (!token) {
42
42
  const providerHint = options.provider
43
43
  ? ` for provider '${options.provider}'`
@@ -4,6 +4,7 @@
4
4
  * Encrypts raw credentials to encrypted format.
5
5
  */
6
6
  interface EncryptOptions {
7
+ agent: string;
7
8
  input: string;
8
9
  output?: string;
9
10
  password: boolean;
@@ -7,9 +7,13 @@ import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
7
7
  import promptPassword from "@inquirer/password";
8
8
  import { parseCredentialsOrArray } from "../auth/types.js";
9
9
  import { DEFAULT_PASSWORD, encrypt, toBase64 } from "../crypto.js";
10
+ import { validateAgent } from "./validate-agent.js";
10
11
  /** Handle encrypt command */
11
12
  async function handleEncrypt(options) {
12
13
  const isStdout = options.output === "-";
14
+ const agentId = validateAgent(options.agent);
15
+ if (!agentId)
16
+ return;
13
17
  if (!options.output) {
14
18
  console.error("Error: --output is required (use - for stdout)");
15
19
  process.exitCode = 2;
@@ -42,24 +46,6 @@ async function handleEncrypt(options) {
42
46
  process.exitCode = 1;
43
47
  return;
44
48
  }
45
- // Extract agent ID and validate all credentials are for the same agent
46
- const firstCred = Array.isArray(creds) ? creds[0] : creds;
47
- if (!firstCred) {
48
- console.error("Error: No credentials found in input file");
49
- process.exitCode = 1;
50
- return;
51
- }
52
- const agentId = firstCred.agent;
53
- // For arrays, verify all credentials are for the same agent
54
- if (Array.isArray(creds)) {
55
- const mixedAgent = creds.find((c) => c.agent !== agentId);
56
- if (mixedAgent) {
57
- console.error(`Error: Mixed agents in credentials array (found '${mixedAgent.agent}', expected '${agentId}')`);
58
- console.error("All credentials in an array must be for the same agent");
59
- process.exitCode = 1;
60
- return;
61
- }
62
- }
63
49
  // Get password
64
50
  const userPassword = options.password
65
51
  ? await promptPassword({ message: "Encryption password" })
@@ -10,7 +10,7 @@ import { parseCredentials } from "../auth/types.js";
10
10
  import { fromBase64, tryDecrypt } from "../crypto.js";
11
11
  import { validateAgent } from "./validate-agent.js";
12
12
  /** Parse decrypted content as single credential or array of credentials */
13
- function parseDecryptedCredentials(parsed, agentId) {
13
+ function parseDecryptedCredentials(parsed) {
14
14
  // Handle array of credentials (OpenCode per-provider format)
15
15
  if (Array.isArray(parsed)) {
16
16
  const credentials = [];
@@ -19,13 +19,6 @@ function parseDecryptedCredentials(parsed, agentId) {
19
19
  if (!cred) {
20
20
  return { ok: false, error: "Invalid credentials format in array" };
21
21
  }
22
- // Defense-in-depth: verify each credential matches the target agent
23
- if (cred.agent !== agentId) {
24
- return {
25
- ok: false,
26
- error: `Credential agent mismatch: expected '${agentId}', got '${cred.agent}'`,
27
- };
28
- }
29
22
  credentials.push(cred);
30
23
  }
31
24
  if (credentials.length === 0) {
@@ -38,13 +31,6 @@ function parseDecryptedCredentials(parsed, agentId) {
38
31
  if (!cred) {
39
32
  return { ok: false, error: "Invalid credentials format" };
40
33
  }
41
- // Defense-in-depth: verify credential matches the target agent
42
- if (cred.agent !== agentId) {
43
- return {
44
- ok: false,
45
- error: `Decrypted credentials are for '${cred.agent}', not '${agentId}'`,
46
- };
47
- }
48
34
  return { ok: true, credentials: [cred] };
49
35
  }
50
36
  /** Handle auth install-credentials command */
@@ -87,7 +73,7 @@ async function handleAuthInstall(options) {
87
73
  });
88
74
  const parsed = JSON.parse(decrypted);
89
75
  // Parse and validate credentials (handles both single and array formats)
90
- const parseResult = parseDecryptedCredentials(parsed, agentId);
76
+ const parseResult = parseDecryptedCredentials(parsed);
91
77
  if (!parseResult.ok) {
92
78
  console.error(`Error: ${parseResult.error}`);
93
79
  process.exitCode = 1;
@@ -95,7 +81,7 @@ async function handleAuthInstall(options) {
95
81
  }
96
82
  // Install all credentials
97
83
  for (const cred of parseResult.credentials) {
98
- const result = installCredentials(cred, {
84
+ const result = installCredentials(agentId, cred, {
99
85
  configDir: options.configDir,
100
86
  dataDir: options.dataDir,
101
87
  storage,
@@ -73,7 +73,7 @@ async function handleVaultFetch(options) {
73
73
  }
74
74
  if (options.env) {
75
75
  // Output shell export commands (for eval/source)
76
- const environmentVariables = credentialsToEnvironment(credentials);
76
+ const environmentVariables = credentialsToEnvironment(agentId, credentials);
77
77
  const environmentEntries = Object.entries(environmentVariables);
78
78
  if (environmentEntries.length === 0) {
79
79
  console.error("Warning: No environment variables to export for this credential type");
@@ -92,7 +92,7 @@ async function handleVaultFetch(options) {
92
92
  }
93
93
  else if (options.install) {
94
94
  // Install credentials locally (writes to file)
95
- const installResult = installCredentials(credentials, {
95
+ const installResult = installCredentials(agentId, credentials, {
96
96
  configDir: options.configDir,
97
97
  dataDir: options.dataDir,
98
98
  });
@@ -169,6 +169,7 @@ async function handleVaultPush(options) {
169
169
  agentId,
170
170
  name: options.name,
171
171
  credentials,
172
+ provider: options.provider,
172
173
  });
173
174
  if (!result.ok) {
174
175
  console.error(`Error: ${FAILURE_MESSAGES[result.reason]}`);
@@ -59,6 +59,8 @@ interface VaultPushOptions {
59
59
  name: string;
60
60
  /** Credentials to push */
61
61
  credentials: Credentials;
62
+ /** Provider for multi-provider agents (e.g., "anthropic" for OpenCode) */
63
+ provider?: string;
62
64
  }
63
65
  /**
64
66
  * Push credentials to the axvault server.
@@ -72,7 +74,7 @@ interface VaultPushOptions {
72
74
  * const result = await pushVaultCredentials({
73
75
  * agentId: "claude",
74
76
  * name: "ci",
75
- * credentials: { agent: "claude", type: "oauth-credentials", data: { ... } }
77
+ * credentials: { type: "oauth-credentials", data: { ... } }
76
78
  * });
77
79
  * if (result.ok) {
78
80
  * console.log("Credentials pushed successfully");
@@ -105,7 +105,7 @@ async function fetchVaultCredentials(options) {
105
105
  * const result = await pushVaultCredentials({
106
106
  * agentId: "claude",
107
107
  * name: "ci",
108
- * credentials: { agent: "claude", type: "oauth-credentials", data: { ... } }
108
+ * credentials: { type: "oauth-credentials", data: { ... } }
109
109
  * });
110
110
  * if (result.ok) {
111
111
  * console.log("Credentials pushed successfully");
@@ -128,15 +128,11 @@ async function pushVaultCredentials(options) {
128
128
  Accept: "application/json",
129
129
  "User-Agent": "axauth-vault-client",
130
130
  },
131
- // agent and name are in the URL path (RESTful resource identifier);
132
- // body contains only the credential payload to avoid redundancy
133
131
  body: JSON.stringify({
132
+ agent: options.agentId,
134
133
  type: options.credentials.type,
135
134
  data: options.credentials.data,
136
- // Include provider only for OpenCode credentials
137
- ...(options.credentials.agent === "opencode" && {
138
- provider: options.credentials.provider,
139
- }),
135
+ ...(options.provider !== undefined && { provider: options.provider }),
140
136
  }),
141
137
  });
142
138
  // Handle error responses
@@ -11,8 +11,6 @@ interface VaultConfig {
11
11
  url: string;
12
12
  /** API key for authentication */
13
13
  apiKey: string;
14
- /** Default credential name to fetch (e.g., "ci", "prod") */
15
- credentialName?: string;
16
14
  }
17
15
  /**
18
16
  * Get vault configuration from environment variables.
@@ -20,23 +18,21 @@ interface VaultConfig {
20
18
  * Supports two configuration methods (checked in order):
21
19
  *
22
20
  * 1. Single JSON env var:
23
- * - `AXVAULT`: JSON object with url, apiKey, and optional credentialName
21
+ * - `AXVAULT`: JSON object with url and apiKey
24
22
  *
25
23
  * 2. Individual env vars:
26
24
  * - `AXVAULT_URL`: Vault server URL (required)
27
25
  * - `AXVAULT_API_KEY`: API key for authentication (required)
28
- * - `AXVAULT_CREDENTIAL`: Default credential name (optional)
29
26
  *
30
27
  * @returns Vault configuration if configured, undefined otherwise
31
28
  *
32
29
  * @example
33
30
  * // Option 1: Single JSON env var
34
- * // AXVAULT='{"url":"https://vault.axkit.dev","apiKey":"axv_sk_xxx","credentialName":"ci"}'
31
+ * // AXVAULT='{"url":"https://vault.axkit.dev","apiKey":"axv_sk_xxx"}'
35
32
  *
36
33
  * // Option 2: Individual env vars
37
34
  * // AXVAULT_URL=https://vault.axkit.dev
38
35
  * // AXVAULT_API_KEY=axv_sk_xxx
39
- * // AXVAULT_CREDENTIAL=ci
40
36
  *
41
37
  * const config = getVaultConfig();
42
38
  * if (config) {
@@ -7,11 +7,12 @@
7
7
  */
8
8
  import { z } from "zod";
9
9
  /** Zod schema for vault config JSON */
10
- const VaultConfigJson = z.object({
10
+ const VaultConfigJson = z
11
+ .object({
11
12
  url: z.string(),
12
13
  apiKey: z.string(),
13
- credentialName: z.string().optional(),
14
- });
14
+ })
15
+ .strict();
15
16
  /**
16
17
  * Validate and normalize a vault URL.
17
18
  *
@@ -44,7 +45,6 @@ function parseVaultConfigJson() {
44
45
  return {
45
46
  url,
46
47
  apiKey: parsed.apiKey,
47
- credentialName: parsed.credentialName,
48
48
  };
49
49
  }
50
50
  catch {
@@ -58,23 +58,21 @@ function parseVaultConfigJson() {
58
58
  * Supports two configuration methods (checked in order):
59
59
  *
60
60
  * 1. Single JSON env var:
61
- * - `AXVAULT`: JSON object with url, apiKey, and optional credentialName
61
+ * - `AXVAULT`: JSON object with url and apiKey
62
62
  *
63
63
  * 2. Individual env vars:
64
64
  * - `AXVAULT_URL`: Vault server URL (required)
65
65
  * - `AXVAULT_API_KEY`: API key for authentication (required)
66
- * - `AXVAULT_CREDENTIAL`: Default credential name (optional)
67
66
  *
68
67
  * @returns Vault configuration if configured, undefined otherwise
69
68
  *
70
69
  * @example
71
70
  * // Option 1: Single JSON env var
72
- * // AXVAULT='{"url":"https://vault.axkit.dev","apiKey":"axv_sk_xxx","credentialName":"ci"}'
71
+ * // AXVAULT='{"url":"https://vault.axkit.dev","apiKey":"axv_sk_xxx"}'
73
72
  *
74
73
  * // Option 2: Individual env vars
75
74
  * // AXVAULT_URL=https://vault.axkit.dev
76
75
  * // AXVAULT_API_KEY=axv_sk_xxx
77
- * // AXVAULT_CREDENTIAL=ci
78
76
  *
79
77
  * const config = getVaultConfig();
80
78
  * if (config) {
@@ -98,7 +96,6 @@ function getVaultConfig() {
98
96
  return {
99
97
  url,
100
98
  apiKey,
101
- credentialName: process.env.AXVAULT_CREDENTIAL,
102
99
  };
103
100
  }
104
101
  /**
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "axauth",
3
3
  "author": "Łukasz Jerciński",
4
4
  "license": "MIT",
5
- "version": "3.1.2",
5
+ "version": "3.1.5",
6
6
  "description": "Authentication management library and CLI for AI coding agents",
7
7
  "repository": {
8
8
  "type": "git",
@@ -70,8 +70,8 @@
70
70
  "@commander-js/extra-typings": "^14.0.0",
71
71
  "@inquirer/password": "^5.0.4",
72
72
  "axconfig": "^3.6.3",
73
- "axexec": "^1.7.0",
74
- "axshared": "^4.0.0",
73
+ "axexec": "^2.0.0",
74
+ "axshared": "^5.0.0",
75
75
  "commander": "^14.0.2",
76
76
  "zod": "^4.3.5"
77
77
  },