axauth 1.11.2 → 2.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.
@@ -92,7 +92,7 @@ interface TokenOptions {
92
92
  provider?: string;
93
93
  /** Skip auto-refresh even for expired tokens (default: false) */
94
94
  skipRefresh?: boolean;
95
- /** Timeout for refresh operation in ms (default: 10000) */
95
+ /** Timeout for refresh operation in ms (default: 30000) */
96
96
  refreshTimeout?: number;
97
97
  }
98
98
  /** Capabilities that an adapter supports */
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Helpers for formatting axexec run failures.
3
+ */
4
+ import type { RunResult } from "axexec";
5
+ declare function formatRunError(result: RunResult): string;
6
+ export { formatRunError };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Helpers for formatting axexec run failures.
3
+ */
4
+ function findLastSessionError(events) {
5
+ for (let index = events.length - 1; index >= 0; index -= 1) {
6
+ const event = events[index];
7
+ if (!event)
8
+ continue;
9
+ if (event.type === "session.error")
10
+ return event;
11
+ }
12
+ return undefined;
13
+ }
14
+ function formatRunError(result) {
15
+ const errorEvent = findLastSessionError(result.events);
16
+ if (!errorEvent) {
17
+ return "Agent execution failed";
18
+ }
19
+ const detail = errorEvent.message.trim();
20
+ if (detail) {
21
+ return `Agent execution failed (${errorEvent.code}): ${detail}`;
22
+ }
23
+ return `Agent execution failed (${errorEvent.code})`;
24
+ }
25
+ export { formatRunError };
@@ -21,10 +21,10 @@ type RefreshResult = {
21
21
  error: string;
22
22
  };
23
23
  /**
24
- * Refresh credentials by spawning the agent briefly.
24
+ * Refresh credentials by running the agent briefly.
25
25
  *
26
- * Creates an isolated temp config directory, installs credentials there,
27
- * and runs the agent with a minimal prompt to trigger its internal refresh.
26
+ * Uses axexec to run the agent in an isolated temp config directory,
27
+ * triggering its internal auth refresh mechanism.
28
28
  *
29
29
  * @param creds - The credentials to refresh (must contain refresh_token for OAuth)
30
30
  * @param options - Refresh options (timeout, provider for multi-provider agents)
@@ -4,127 +4,103 @@
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 { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
8
- import { tmpdir } from "node:os";
9
- import path from "node:path";
10
- import { buildAgentRuntimeEnvironment, getAgent, getAgentConfigSubdirectory, } from "axshared";
7
+ import { cleanupCredentials, runAgent } from "axexec";
11
8
  import { buildRefreshedCredentials } from "./build-refreshed-credentials.js";
12
- import { spawnAgentWithFileMonitor } from "./spawn-agent-with-file-monitor.js";
13
- import { getFileStats } from "./wait-for-file-update.js";
9
+ import { formatRunError } from "./format-run-error.js";
10
+ import { mergeOpenCodeBundle, resolveRefreshCredentials, } from "./resolve-refresh-credentials.js";
11
+ import { waitForRefreshedCredentials } from "./wait-for-refreshed-credentials.js";
14
12
  /** Default timeout for refresh operations in milliseconds */
15
13
  const DEFAULT_REFRESH_TIMEOUT_MS = 30_000;
16
- /**
17
- * Write agent-specific config to force file-based storage.
18
- *
19
- * This prevents the agent from using keychain credentials,
20
- * ensuring it reads/writes from our temp directory.
21
- */
22
- function writeForceFileConfig(agentId, temporaryDirectory) {
23
- switch (agentId) {
24
- case "codex": {
25
- // Codex uses config.toml with cli_auth_credentials_store setting
26
- writeFileSync(path.join(temporaryDirectory, "config.toml"), 'cli_auth_credentials_store = "file"\nmcp_oauth_credentials_store = "file"\n');
27
- break;
28
- }
29
- case "copilot": {
30
- // Copilot uses JSON config with store_token_plaintext flag
31
- writeFileSync(path.join(temporaryDirectory, "config.json"), JSON.stringify({ store_token_plaintext: true }, undefined, 2));
32
- break;
33
- }
34
- // Claude: keychain service name includes hash of CLAUDE_CONFIG_DIR,
35
- // so unique temp dir = unique keychain entry that won't exist
36
- // Gemini: file storage behavior is controlled via the GEMINI_FORCE_FILE_STORAGE
37
- // environment variable, which is set by buildAgentRuntimeEnvironment and
38
- // later included in fullEnvironment when spawning the agent.
39
- // OpenCode: file-only by design, no action needed
14
+ async function safeCleanup(directory) {
15
+ try {
16
+ await cleanupCredentials(directory);
17
+ }
18
+ catch (error) {
19
+ const message = error instanceof Error ? error.message : String(error);
20
+ console.error(`Warning: Failed to clean up temp directory '${directory}': ${message}`);
40
21
  }
41
22
  }
42
23
  /**
43
- * Refresh credentials by spawning the agent briefly.
24
+ * Refresh credentials by running the agent briefly.
44
25
  *
45
- * Creates an isolated temp config directory, installs credentials there,
46
- * and runs the agent with a minimal prompt to trigger its internal refresh.
26
+ * Uses axexec to run the agent in an isolated temp config directory,
27
+ * triggering its internal auth refresh mechanism.
47
28
  *
48
29
  * @param creds - The credentials to refresh (must contain refresh_token for OAuth)
49
30
  * @param options - Refresh options (timeout, provider for multi-provider agents)
50
31
  * @returns RefreshResult with new credentials or error
51
32
  */
52
33
  async function refreshCredentials(creds, options) {
53
- const timeout = options?.timeout ?? DEFAULT_REFRESH_TIMEOUT_MS;
54
- const agent = getAgent(creds.agent);
55
- // 1. Create temp config directory
56
- const temporaryDirectory = mkdtempSync(path.join(tmpdir(), `axauth-refresh-${creds.agent}-`));
34
+ const timeoutMs = options?.timeout ?? DEFAULT_REFRESH_TIMEOUT_MS;
35
+ const deadlineMs = Date.now() + timeoutMs;
36
+ const resolved = resolveRefreshCredentials(creds, options);
37
+ if (!resolved.ok) {
38
+ return { ok: false, error: resolved.error };
39
+ }
40
+ // Run agent with a minimal prompt to trigger internal token refresh.
41
+ // preserveConfigDirectory keeps the temp dir so we can read refreshed credentials.
42
+ const resultPromise = runAgent(resolved.credentials.agent, {
43
+ prompt: "ping",
44
+ credentials: resolved.credentials,
45
+ preserveConfigDirectory: true,
46
+ });
47
+ const timeoutMarker = { timedOut: true };
48
+ let timeoutId;
49
+ let raceResult;
57
50
  try {
58
- // 2. Determine where credentials should be installed
59
- // Some agents (Gemini, Copilot, OpenCode) require a subdirectory structure
60
- const subdirectory = getAgentConfigSubdirectory(creds.agent);
61
- const credentialsDirectory = subdirectory
62
- ? path.join(temporaryDirectory, subdirectory)
63
- : temporaryDirectory;
64
- // 3. Ensure credentials directory exists before writing config
65
- mkdirSync(credentialsDirectory, { recursive: true });
66
- // 4. Write agent-specific config to force file storage
67
- writeForceFileConfig(creds.agent, credentialsDirectory);
68
- // 5. Install credentials to the appropriate directory
69
- // Import dynamically to avoid circular dependency
70
- const { installCredentials, getAdapter } = await import("./registry.js");
71
- const installResult = installCredentials(creds, {
72
- configDir: credentialsDirectory,
73
- dataDir: credentialsDirectory,
74
- });
75
- if (!installResult.ok) {
76
- return {
77
- ok: false,
78
- error: `Failed to install credentials: ${installResult.message}`,
79
- };
80
- }
81
- // 5b. Get credential file path and record stats for file monitoring
82
- const adapter = getAdapter(creds.agent);
83
- const credentialFilePath = adapter.getCredentialFilePath(credentialsDirectory);
84
- const beforeStats = getFileStats(credentialFilePath);
85
- // 6. Build runtime environment
86
- // Pass the full credentialsDirectory (which may include an agent-specific
87
- // subdirectory, e.g. "~/.gemini/"); buildAgentRuntimeEnvironment will
88
- // derive the parent directory internally for agents that use subdirectories.
89
- const runtimeEnvironment = buildAgentRuntimeEnvironment(creds.agent, credentialsDirectory);
90
- const fullEnvironment = {
91
- ...process.env,
92
- ...runtimeEnvironment,
93
- };
94
- // 7. Build CLI arguments using simplePromptArguments from agent
95
- const cliArguments = agent.simplePromptArguments("ping", {
96
- provider: options?.provider,
51
+ raceResult = await Promise.race([
52
+ resultPromise,
53
+ new Promise((resolve) => {
54
+ timeoutId = setTimeout(() => {
55
+ resolve(timeoutMarker);
56
+ }, timeoutMs);
57
+ }),
58
+ ]);
59
+ }
60
+ catch (error) {
61
+ if (timeoutId)
62
+ clearTimeout(timeoutId);
63
+ const message = error instanceof Error ? error.message : String(error);
64
+ return { ok: false, error: `Agent execution failed: ${message}` };
65
+ }
66
+ if (timeoutId)
67
+ clearTimeout(timeoutId);
68
+ if ("timedOut" in raceResult) {
69
+ // runAgent has no cancellation API yet; return early and clean up later.
70
+ void resultPromise
71
+ .then((lateResult) => {
72
+ const directories = lateResult.execution.directories;
73
+ if (directories) {
74
+ return safeCleanup(directories.base);
75
+ }
76
+ })
77
+ .catch(() => {
78
+ // Swallow cleanup errors for late results.
97
79
  });
98
- // 8. Spawn agent with file monitoring
99
- // File monitoring starts before agent spawn to catch async writes that may
100
- // occur during process execution (e.g., Gemini's async event handlers).
101
- const { timedOut, exitCode, fileUpdated } = await spawnAgentWithFileMonitor(agent.cli, cliArguments, fullEnvironment, timeout, credentialFilePath, beforeStats);
102
- if (timedOut) {
103
- return { ok: false, error: "Refresh timed out" };
104
- }
105
- if (exitCode !== 0 && exitCode !== undefined) {
106
- return { ok: false, error: `Agent exited with code ${exitCode}` };
80
+ return { ok: false, error: `Refresh timed out after ${timeoutMs}ms` };
81
+ }
82
+ const result = raceResult;
83
+ const { directories } = result.execution;
84
+ try {
85
+ if (!result.success) {
86
+ return { ok: false, error: formatRunError(result) };
107
87
  }
108
- if (!fileUpdated) {
109
- console.error(`Warning: Credential file "${credentialFilePath}" was not observed ` +
110
- `to be updated. Proceeding may read stale credentials.`);
88
+ if (!directories) {
89
+ return { ok: false, error: "No directories in execution metadata" };
111
90
  }
112
- // 9. Read refreshed credentials from temp directory
113
- const { extractRawCredentialsFromDirectory } = await import("./registry.js");
114
- const refreshedCredentials = extractRawCredentialsFromDirectory(creds.agent, { configDir: credentialsDirectory, dataDir: credentialsDirectory });
91
+ const refreshedCredentials = await waitForRefreshedCredentials(resolved.credentials.agent, directories, deadlineMs);
115
92
  if (!refreshedCredentials) {
116
93
  return { ok: false, error: "No credentials found after refresh" };
117
94
  }
118
- return { ok: true, credentials: refreshedCredentials };
95
+ return {
96
+ ok: true,
97
+ credentials: mergeOpenCodeBundle(creds, refreshedCredentials, resolved.mergeProvider),
98
+ };
119
99
  }
120
100
  finally {
121
- // 10. Clean up temp directory
122
- try {
123
- rmSync(temporaryDirectory, { recursive: true, force: true });
124
- }
125
- catch (error) {
126
- const message = error instanceof Error ? error.message : String(error);
127
- console.error(`Warning: Failed to clean up temp directory '${temporaryDirectory}': ${message}`);
101
+ if (directories) {
102
+ // Clean up the temp directory (use base to remove entire temp structure)
103
+ await safeCleanup(directories.base);
128
104
  }
129
105
  }
130
106
  }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Resolve which credentials to use for refresh and how to merge results.
3
+ */
4
+ import type { Credentials } from "./types.js";
5
+ interface RefreshProviderOptions {
6
+ provider?: string;
7
+ }
8
+ type RefreshResolution = {
9
+ ok: true;
10
+ credentials: Credentials;
11
+ mergeProvider?: string;
12
+ } | {
13
+ ok: false;
14
+ error: string;
15
+ };
16
+ declare function resolveRefreshCredentials(creds: Credentials, options?: RefreshProviderOptions): RefreshResolution;
17
+ declare function mergeOpenCodeBundle(original: Credentials, refreshed: Credentials, mergeProvider?: string): Credentials;
18
+ export { mergeOpenCodeBundle, resolveRefreshCredentials };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Resolve which credentials to use for refresh and how to merge results.
3
+ */
4
+ import { extractProviderCredentials, splitToPerProviderCredentials, } from "./agents/opencode-credentials.js";
5
+ function resolveRefreshCredentials(creds, options) {
6
+ if (creds.agent !== "opencode") {
7
+ return { ok: true, credentials: creds };
8
+ }
9
+ if (creds.provider) {
10
+ return { ok: true, credentials: creds };
11
+ }
12
+ if (options?.provider) {
13
+ const providerCreds = extractProviderCredentials(creds, options.provider);
14
+ if (!providerCreds) {
15
+ return {
16
+ ok: false,
17
+ error: `No credentials found for provider '${options.provider}'`,
18
+ };
19
+ }
20
+ return {
21
+ ok: true,
22
+ credentials: providerCreds,
23
+ mergeProvider: providerCreds.provider,
24
+ };
25
+ }
26
+ const perProvider = splitToPerProviderCredentials(creds);
27
+ const refreshCreds = perProvider.find((cred) => cred.type === "oauth-credentials") ??
28
+ perProvider.at(0);
29
+ if (!refreshCreds) {
30
+ return { ok: false, error: "No provider credentials found for opencode" };
31
+ }
32
+ return {
33
+ ok: true,
34
+ credentials: refreshCreds,
35
+ mergeProvider: refreshCreds.provider,
36
+ };
37
+ }
38
+ function mergeOpenCodeBundle(original, refreshed, mergeProvider) {
39
+ if (original.agent !== "opencode")
40
+ return refreshed;
41
+ if (original.provider)
42
+ return refreshed;
43
+ if (!mergeProvider)
44
+ return refreshed;
45
+ const mergedData = { ...original.data, ...refreshed.data };
46
+ if ("_source" in original.data) {
47
+ mergedData._source = original.data._source;
48
+ }
49
+ return { ...refreshed, data: mergedData };
50
+ }
51
+ export { mergeOpenCodeBundle, resolveRefreshCredentials };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Polls for refreshed credentials to appear after agent execution.
3
+ */
4
+ import type { ExecutionDirectories } from "axexec";
5
+ import type { Credentials } from "./types.js";
6
+ declare function waitForRefreshedCredentials(agent: Credentials["agent"], directories: ExecutionDirectories, deadlineMs: number): Promise<Credentials | undefined>;
7
+ export { waitForRefreshedCredentials };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Polls for refreshed credentials to appear after agent execution.
3
+ */
4
+ const CREDENTIAL_POLL_INTERVAL_MS = 200;
5
+ function delay(ms) {
6
+ return new Promise((resolve) => setTimeout(resolve, ms));
7
+ }
8
+ async function waitForRefreshedCredentials(agent, directories, deadlineMs) {
9
+ const { extractRawCredentialsFromDirectory } = await import("./registry.js");
10
+ for (;;) {
11
+ const refreshedCredentials = extractRawCredentialsFromDirectory(agent, {
12
+ configDir: directories.config,
13
+ dataDir: directories.data,
14
+ });
15
+ if (refreshedCredentials)
16
+ return refreshedCredentials;
17
+ const remaining = deadlineMs - Date.now();
18
+ if (remaining <= 0)
19
+ return undefined;
20
+ // Some agents write credentials asynchronously after exit; poll briefly.
21
+ await delay(Math.min(CREDENTIAL_POLL_INTERVAL_MS, remaining));
22
+ }
23
+ }
24
+ export { waitForRefreshedCredentials };
package/dist/cli.js CHANGED
@@ -28,24 +28,29 @@ Examples:
28
28
  # List authenticated agents
29
29
  axauth list
30
30
 
31
- # Filter to show only authenticated agents
32
- axauth list | tail -n +2 | awk -F'\t' '$2 == "authenticated"'
33
-
34
31
  # Get access token for an agent
35
32
  axauth token --agent claude
36
33
 
37
- # Export credentials for CI/CD
38
- axauth export --agent claude --output creds.json --no-password
34
+ # Export/import credentials (file-based)
35
+ axauth export --agent claude --output creds.json
36
+ axauth install-credentials --agent claude --input creds.json
37
+
38
+ # For vault operations: export raw format (--no-encrypt)
39
+ # Note: install-credentials cannot consume raw exports
40
+ axauth export --agent claude --output creds.json --no-encrypt
39
41
 
40
42
  # Remove credentials (agent will prompt for login)
41
43
  axauth remove-credentials --agent claude
42
44
 
43
- # Install credentials from exported file
44
- axauth install-credentials --agent claude --input creds.json
45
+ CI/CD with axvault (recommended):
46
+ # One-time: push local credentials to vault
47
+ axauth vault push --agent claude --name ci
48
+
49
+ # In CI/CD: fetch and install credentials
50
+ AXVAULT_URL=https://vault.example.com AXVAULT_API_KEY=axv_sk_xxx \
51
+ axauth vault fetch --agent claude --name ci --install
45
52
 
46
- # Fetch credentials from vault (JSON config)
47
- AXVAULT='{"url":"https://vault.example.com","apiKey":"axv_sk_xxx"}' \
48
- axauth vault fetch --agent claude --name ci --install`);
53
+ # See "axauth vault --help" for full workflow documentation`);
49
54
  program
50
55
  .command("list")
51
56
  .description("List agents and their auth status")
@@ -118,7 +123,32 @@ program
118
123
  // Vault subcommand
119
124
  const vault = program
120
125
  .command("vault")
121
- .description("Manage credentials stored in axvault server");
126
+ .description("Manage credentials stored in axvault server")
127
+ .addHelpText("after", String.raw `
128
+ Workflow:
129
+ 1. Push local credentials to vault (one-time setup from authenticated machine):
130
+ axauth vault push --agent claude --name ci
131
+
132
+ 2. Fetch credentials in CI/CD (set AXVAULT_URL and AXVAULT_API_KEY):
133
+ axauth vault fetch --agent claude --name ci --install
134
+
135
+ Environment variables:
136
+ AXVAULT JSON config: {"url":"...","apiKey":"..."}
137
+ AXVAULT_URL Vault server URL
138
+ AXVAULT_API_KEY API key for authentication
139
+
140
+ OpenCode multi-provider support:
141
+ OpenCode stores credentials for multiple providers (anthropic, openai, google).
142
+ Use --provider to push a single provider (fetch uses the stored name):
143
+
144
+ axauth vault push --agent opencode --provider anthropic --name ci-anthropic
145
+ axauth vault fetch --agent opencode --name ci-anthropic --install
146
+
147
+ Quick reference:
148
+ vault push Upload local credentials to vault (requires write access)
149
+ vault fetch Download credentials from vault (requires read access)
150
+
151
+ Use "axauth vault <command> --help" for detailed options.`);
122
152
  vault
123
153
  .command("fetch")
124
154
  .description("Fetch credentials from vault server")
@@ -167,6 +197,7 @@ vault
167
197
  .description("Push local credentials to vault server")
168
198
  .requiredOption("-a, --agent <agent>", `Agent to push credentials from (${AGENT_CLIS.join(", ")})`)
169
199
  .requiredOption("-n, --name <name>", "Credential name in vault (e.g., ci, prod)")
200
+ .option("-p, --provider <provider>", "Provider to push (opencode only; pushes single provider instead of all)")
170
201
  .addHelpText("after", String.raw `
171
202
  Environment variables (option 1 - single JSON):
172
203
  AXVAULT JSON config: {"url":"...","apiKey":"..."}
@@ -179,6 +210,9 @@ Examples:
179
210
  # Push local Claude credentials to vault as "ci"
180
211
  axauth vault push --agent claude --name ci
181
212
 
213
+ # Push a single OpenCode provider to vault
214
+ axauth vault push --agent opencode --provider anthropic --name ci-anthropic
215
+
182
216
  # Push to a different vault instance
183
217
  AXVAULT_URL=https://vault.example.com AXVAULT_API_KEY=axv_sk_xxx \
184
218
  axauth vault push --agent gemini --name prod`)
@@ -22,6 +22,7 @@ declare function handleVaultFetch(options: VaultFetchOptions): Promise<void>;
22
22
  interface VaultPushOptions {
23
23
  agent: string;
24
24
  name: string;
25
+ provider?: string;
25
26
  }
26
27
  /**
27
28
  * Handle vault push command.
@@ -2,6 +2,7 @@
2
2
  * Vault commands - fetch and push credentials to axvault server.
3
3
  */
4
4
  import { credentialsToEnvironment, extractRawCredentials, installCredentials, } from "../auth/registry.js";
5
+ import { extractProviderCredentials } from "../auth/agents/opencode.js";
5
6
  import { fetchVaultCredentials, pushVaultCredentials, } from "../vault/vault-client.js";
6
7
  import { getVaultConfig } from "../vault/vault-config.js";
7
8
  import { validateAgent } from "./validate-agent.js";
@@ -132,6 +133,12 @@ async function handleVaultPush(options) {
132
133
  const agentId = validateAgent(options.agent);
133
134
  if (!agentId)
134
135
  return;
136
+ // Validate --provider is only used with opencode
137
+ if (options.provider && agentId !== "opencode") {
138
+ console.error("Error: --provider flag is only supported for opencode agent");
139
+ process.exitCode = 2;
140
+ return;
141
+ }
135
142
  // Check vault is configured
136
143
  const vaultConfig = getVaultConfig();
137
144
  if (!vaultConfig) {
@@ -140,13 +147,22 @@ async function handleVaultPush(options) {
140
147
  return;
141
148
  }
142
149
  // Extract local credentials
143
- const credentials = extractRawCredentials(agentId);
144
- if (!credentials) {
150
+ const rawCredentials = extractRawCredentials(agentId);
151
+ if (!rawCredentials) {
145
152
  console.error(`Error: No credentials found for ${agentId}`);
146
153
  console.error("Hint: Authenticate with the agent first, or use 'axauth list' to check status");
147
154
  process.exitCode = 1;
148
155
  return;
149
156
  }
157
+ // Handle per-provider extraction for OpenCode
158
+ const credentials = options.provider
159
+ ? extractProviderCredentials(rawCredentials, options.provider)
160
+ : rawCredentials;
161
+ if (!credentials) {
162
+ console.error(`Error: No credentials found for provider '${options.provider}'`);
163
+ process.exitCode = 1;
164
+ return;
165
+ }
150
166
  // Push to vault
151
167
  const result = await pushVaultCredentials({
152
168
  agentId,
@@ -158,6 +174,7 @@ async function handleVaultPush(options) {
158
174
  process.exitCode = 1;
159
175
  return;
160
176
  }
161
- console.error(`Credentials pushed to vault: ${agentId}/${options.name}`);
177
+ const label = options.provider ? `${agentId}/${options.provider}` : agentId;
178
+ console.error(`Credentials pushed to vault: ${label} → ${options.name}`);
162
179
  }
163
180
  export { handleVaultFetch, handleVaultPush };
@@ -14,6 +14,7 @@ const VaultCredentialResponse = z.object({
14
14
  name: z.string(),
15
15
  type: CredentialType,
16
16
  data: z.record(z.string(), z.unknown()),
17
+ provider: z.string().trim().min(1).optional(), // OpenCode provider support
17
18
  expiresAt: z.string().nullish(), // optional for oauth-token type
18
19
  updatedAt: z.string(),
19
20
  });
@@ -88,6 +89,7 @@ async function fetchVaultCredentials(options) {
88
89
  agent: options.agentId,
89
90
  type: body.type,
90
91
  data: body.data,
92
+ provider: body.provider,
91
93
  };
92
94
  return {
93
95
  ok: true,
@@ -142,6 +144,7 @@ async function pushVaultCredentials(options) {
142
144
  body: JSON.stringify({
143
145
  type: options.credentials.type,
144
146
  data: options.credentials.data,
147
+ provider: options.credentials.provider,
145
148
  }),
146
149
  });
147
150
  // Handle error responses
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "axauth",
3
3
  "author": "Łukasz Jerciński",
4
4
  "license": "MIT",
5
- "version": "1.11.2",
5
+ "version": "2.0.0",
6
6
  "description": "Authentication management library and CLI for AI coding agents",
7
7
  "repository": {
8
8
  "type": "git",
@@ -70,7 +70,8 @@
70
70
  "@commander-js/extra-typings": "^14.0.0",
71
71
  "@inquirer/password": "^5.0.3",
72
72
  "axconfig": "^3.6.0",
73
- "axshared": "^1.9.0",
73
+ "axexec": "^1.1.0",
74
+ "axshared": "^2.0.0",
74
75
  "commander": "^14.0.2",
75
76
  "zod": "^4.3.5"
76
77
  },
@@ -1,20 +0,0 @@
1
- /**
2
- * Agent subprocess spawning with file monitoring.
3
- *
4
- * Starts file monitoring before agent spawn to catch async writes that may
5
- * occur during process execution (e.g., Gemini's async event handlers).
6
- */
7
- import { type SpawnResult } from "./spawn-agent.js";
8
- import { type FileStats } from "./wait-for-file-update.js";
9
- /**
10
- * Spawn agent and monitor a file for updates.
11
- *
12
- * File monitoring starts before agent spawn to catch writes that occur during
13
- * process execution, not just after exit.
14
- *
15
- * @returns Object with spawn result and whether file was updated
16
- */
17
- declare function spawnAgentWithFileMonitor(cli: string, arguments_: string[], environment: NodeJS.ProcessEnv, timeout: number, filePath: string, beforeStats: FileStats | undefined): Promise<SpawnResult & {
18
- fileUpdated: boolean;
19
- }>;
20
- export { spawnAgentWithFileMonitor };
@@ -1,57 +0,0 @@
1
- /**
2
- * Agent subprocess spawning with file monitoring.
3
- *
4
- * Starts file monitoring before agent spawn to catch async writes that may
5
- * occur during process execution (e.g., Gemini's async event handlers).
6
- */
7
- import { spawnAgent } from "./spawn-agent.js";
8
- import { getFileStats, waitForFileUpdate, } from "./wait-for-file-update.js";
9
- /**
10
- * Spawn agent and monitor a file for updates.
11
- *
12
- * File monitoring starts before agent spawn to catch writes that occur during
13
- * process execution, not just after exit.
14
- *
15
- * @returns Object with spawn result and whether file was updated
16
- */
17
- async function spawnAgentWithFileMonitor(cli, arguments_, environment, timeout, filePath, beforeStats) {
18
- const fileMonitorAbort = new AbortController();
19
- // Start file monitoring BEFORE spawning agent
20
- const fileMonitorPromise = waitForFileUpdate(filePath, beforeStats, {
21
- timeout: timeout + 5000,
22
- signal: fileMonitorAbort.signal,
23
- });
24
- try {
25
- // Spawn agent
26
- const spawnResult = await spawnAgent(cli, arguments_, environment, timeout);
27
- // On early exit, abort file monitor
28
- if (spawnResult.timedOut ||
29
- (spawnResult.exitCode !== 0 && spawnResult.exitCode !== undefined)) {
30
- return { ...spawnResult, fileUpdated: false };
31
- }
32
- // Agent exited successfully - wait for in-flight writes
33
- // Use 5s grace period to match previous behavior (some agents like Gemini
34
- // may write credentials several seconds after process exit)
35
- const gracePeriodMs = 5000;
36
- const fileResult = await Promise.race([
37
- fileMonitorPromise,
38
- new Promise((resolve) => {
39
- setTimeout(() => {
40
- resolve({ ok: false });
41
- }, gracePeriodMs).unref();
42
- }),
43
- ]);
44
- // Final check in case write completed after race
45
- const finalStats = getFileStats(filePath);
46
- const fileUpdated = fileResult.ok ||
47
- (finalStats !== undefined &&
48
- (beforeStats === undefined ||
49
- finalStats.mtimeMs !== beforeStats.mtimeMs ||
50
- finalStats.size !== beforeStats.size));
51
- return { ...spawnResult, fileUpdated };
52
- }
53
- finally {
54
- fileMonitorAbort.abort();
55
- }
56
- }
57
- export { spawnAgentWithFileMonitor };
@@ -1,19 +0,0 @@
1
- /**
2
- * Agent subprocess spawning utility.
3
- */
4
- /** Result of spawning an agent process */
5
- interface SpawnResult {
6
- timedOut: boolean;
7
- exitCode: number | undefined;
8
- }
9
- /**
10
- * Spawn agent and wait for completion.
11
- *
12
- * Enforces a hard timeout - if the process doesn't exit after SIGTERM,
13
- * escalates to SIGKILL after a grace period and resolves anyway.
14
- *
15
- * @returns Object with timedOut flag and exit code
16
- */
17
- declare function spawnAgent(binary: string, arguments_: string[], environment: NodeJS.ProcessEnv, timeout: number): Promise<SpawnResult>;
18
- export { spawnAgent };
19
- export type { SpawnResult };
@@ -1,63 +0,0 @@
1
- /**
2
- * Agent subprocess spawning utility.
3
- */
4
- import { spawn } from "node:child_process";
5
- /** Grace period before escalating SIGTERM to SIGKILL */
6
- const SIGKILL_GRACE_MS = 5000;
7
- /**
8
- * Spawn agent and wait for completion.
9
- *
10
- * Enforces a hard timeout - if the process doesn't exit after SIGTERM,
11
- * escalates to SIGKILL after a grace period and resolves anyway.
12
- *
13
- * @returns Object with timedOut flag and exit code
14
- */
15
- async function spawnAgent(binary, arguments_, environment, timeout) {
16
- return new Promise((resolve) => {
17
- const child = spawn(binary, arguments_, {
18
- env: environment,
19
- // Ignore stdin/stdout to prevent pipe buffer deadlocks, but inherit
20
- // stderr so agent error messages are visible for debugging
21
- stdio: ["ignore", "ignore", "inherit"],
22
- });
23
- let timedOut = false;
24
- let resolved = false;
25
- let killTimer;
26
- const cleanup = () => {
27
- clearTimeout(timer);
28
- if (killTimer)
29
- clearTimeout(killTimer);
30
- };
31
- const timer = setTimeout(() => {
32
- if (!resolved) {
33
- timedOut = true;
34
- child.kill("SIGTERM");
35
- // If process doesn't exit after SIGTERM, escalate to SIGKILL
36
- // and resolve anyway to prevent indefinite hangs
37
- killTimer = setTimeout(() => {
38
- if (!resolved) {
39
- child.kill("SIGKILL");
40
- resolved = true;
41
- cleanup();
42
- resolve({ timedOut: true, exitCode: undefined });
43
- }
44
- }, SIGKILL_GRACE_MS);
45
- }
46
- }, timeout);
47
- child.on("error", () => {
48
- if (!resolved) {
49
- resolved = true;
50
- cleanup();
51
- resolve({ timedOut: false, exitCode: 1 });
52
- }
53
- });
54
- child.on("close", (code) => {
55
- if (!resolved) {
56
- resolved = true;
57
- cleanup();
58
- resolve({ timedOut, exitCode: code ?? undefined });
59
- }
60
- });
61
- });
62
- }
63
- export { spawnAgent };
@@ -1,42 +0,0 @@
1
- /**
2
- * File change monitoring utility.
3
- *
4
- * Watches for file creation or modification using fs.watch,
5
- * used by token refresh to wait for credential files to be written.
6
- */
7
- import { type Stats } from "node:fs";
8
- /** File stats type for external use */
9
- type FileStats = Stats;
10
- /** Options for waiting on file changes */
11
- interface WaitOptions {
12
- /** Maximum time to wait in milliseconds (default: 5000) */
13
- timeout?: number;
14
- /** AbortSignal to cancel waiting early */
15
- signal?: AbortSignal;
16
- }
17
- /** Result of waiting for file change */
18
- type WaitResult = {
19
- ok: true;
20
- reason: "created" | "modified";
21
- } | {
22
- ok: false;
23
- reason: "timeout" | "error" | "aborted";
24
- error?: string;
25
- };
26
- /**
27
- * Get file stats safely, returning undefined if file doesn't exist.
28
- */
29
- declare function getFileStats(filePath: string): Stats | undefined;
30
- /**
31
- * Wait for a file to be created or modified.
32
- *
33
- * Monitors the target file and resolves when:
34
- * - The file is created (if it didn't exist before)
35
- * - The file's mtime changes (if it existed before)
36
- *
37
- * Uses fs.watch on the parent directory for responsiveness, with a
38
- * polling fallback since fs.watch is documented as potentially unreliable.
39
- */
40
- declare function waitForFileUpdate(filePath: string, beforeStats: Stats | undefined, options?: WaitOptions): Promise<WaitResult>;
41
- export { getFileStats, waitForFileUpdate };
42
- export type { FileStats };
@@ -1,128 +0,0 @@
1
- /**
2
- * File change monitoring utility.
3
- *
4
- * Watches for file creation or modification using fs.watch,
5
- * used by token refresh to wait for credential files to be written.
6
- */
7
- import { statSync, watch } from "node:fs";
8
- import path from "node:path";
9
- /**
10
- * Get file stats safely, returning undefined if file doesn't exist.
11
- */
12
- function getFileStats(filePath) {
13
- try {
14
- return statSync(filePath);
15
- }
16
- catch {
17
- return undefined;
18
- }
19
- }
20
- /** Check if the file was created or modified compared to before. */
21
- function checkFileChange(filePath, beforeStats) {
22
- const currentStats = getFileStats(filePath);
23
- // File was created
24
- if (!beforeStats && currentStats) {
25
- return { ok: true, reason: "created" };
26
- }
27
- // File was modified (mtime or size changed)
28
- // Use !== instead of > to catch same-millisecond writes and handle
29
- // filesystems with coarse timestamp resolution (ext3, HFS+, FAT32).
30
- if (beforeStats &&
31
- currentStats &&
32
- (currentStats.mtimeMs !== beforeStats.mtimeMs ||
33
- currentStats.size !== beforeStats.size)) {
34
- return { ok: true, reason: "modified" };
35
- }
36
- return undefined;
37
- }
38
- /**
39
- * Wait for a file to be created or modified.
40
- *
41
- * Monitors the target file and resolves when:
42
- * - The file is created (if it didn't exist before)
43
- * - The file's mtime changes (if it existed before)
44
- *
45
- * Uses fs.watch on the parent directory for responsiveness, with a
46
- * polling fallback since fs.watch is documented as potentially unreliable.
47
- */
48
- async function waitForFileUpdate(filePath, beforeStats, options) {
49
- const timeout = options?.timeout ?? 5000;
50
- const signal = options?.signal;
51
- const parentDirectory = path.dirname(filePath);
52
- const fileName = path.basename(filePath);
53
- // Check if already aborted
54
- if (signal?.aborted) {
55
- return { ok: false, reason: "aborted" };
56
- }
57
- // Check immediately (file might already be written)
58
- const immediate = checkFileChange(filePath, beforeStats);
59
- if (immediate) {
60
- return immediate;
61
- }
62
- return new Promise((resolve) => {
63
- let watcher;
64
- let resolved = false;
65
- let abortHandler;
66
- const cleanup = (timeoutId, pollId) => {
67
- if (resolved)
68
- return;
69
- resolved = true;
70
- watcher?.close();
71
- clearTimeout(timeoutId);
72
- clearInterval(pollId);
73
- if (abortHandler && signal) {
74
- signal.removeEventListener("abort", abortHandler);
75
- }
76
- };
77
- // Set timeout
78
- const timeoutId = setTimeout(() => {
79
- cleanup(timeoutId, pollIntervalId);
80
- resolve({ ok: false, reason: "timeout" });
81
- }, timeout);
82
- // Polling fallback: fs.watch is documented as potentially unreliable,
83
- // so we also poll periodically to catch changes if events are missed.
84
- const pollIntervalId = setInterval(() => {
85
- const result = checkFileChange(filePath, beforeStats);
86
- if (result) {
87
- cleanup(timeoutId, pollIntervalId);
88
- resolve(result);
89
- }
90
- }, 100);
91
- // Handle abort signal
92
- if (signal) {
93
- abortHandler = () => {
94
- cleanup(timeoutId, pollIntervalId);
95
- resolve({ ok: false, reason: "aborted" });
96
- };
97
- signal.addEventListener("abort", abortHandler);
98
- }
99
- // Watch parent directory for changes (for faster responsiveness)
100
- try {
101
- watcher = watch(parentDirectory, (_eventType, changedFile) => {
102
- // Check when the filename matches OR when filename is null/undefined.
103
- // Per Node.js docs, changedFile can be null on some platforms (e.g., Linux),
104
- // so we check our target file whenever the filename isn't provided.
105
- if (!changedFile || changedFile === fileName) {
106
- const result = checkFileChange(filePath, beforeStats);
107
- if (result) {
108
- cleanup(timeoutId, pollIntervalId);
109
- resolve(result);
110
- }
111
- }
112
- });
113
- watcher.on("error", (error) => {
114
- cleanup(timeoutId, pollIntervalId);
115
- resolve({ ok: false, reason: "error", error: error.message });
116
- });
117
- }
118
- catch (error) {
119
- cleanup(timeoutId, pollIntervalId);
120
- resolve({
121
- ok: false,
122
- reason: "error",
123
- error: error instanceof Error ? error.message : String(error),
124
- });
125
- }
126
- });
127
- }
128
- export { getFileStats, waitForFileUpdate };