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.
- package/dist/auth/adapter.d.ts +1 -1
- package/dist/auth/format-run-error.d.ts +6 -0
- package/dist/auth/format-run-error.js +25 -0
- package/dist/auth/refresh-credentials.d.ts +3 -3
- package/dist/auth/refresh-credentials.js +75 -99
- package/dist/auth/resolve-refresh-credentials.d.ts +18 -0
- package/dist/auth/resolve-refresh-credentials.js +51 -0
- package/dist/auth/wait-for-refreshed-credentials.d.ts +7 -0
- package/dist/auth/wait-for-refreshed-credentials.js +24 -0
- package/dist/cli.js +45 -11
- package/dist/commands/vault.d.ts +1 -0
- package/dist/commands/vault.js +20 -3
- package/dist/vault/vault-client.js +3 -0
- package/package.json +3 -2
- package/dist/auth/spawn-agent-with-file-monitor.d.ts +0 -20
- package/dist/auth/spawn-agent-with-file-monitor.js +0 -57
- package/dist/auth/spawn-agent.d.ts +0 -19
- package/dist/auth/spawn-agent.js +0 -63
- package/dist/auth/wait-for-file-update.d.ts +0 -42
- package/dist/auth/wait-for-file-update.js +0 -128
package/dist/auth/adapter.d.ts
CHANGED
|
@@ -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:
|
|
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,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
|
|
24
|
+
* Refresh credentials by running the agent briefly.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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 {
|
|
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 {
|
|
13
|
-
import {
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
24
|
+
* Refresh credentials by running the agent briefly.
|
|
44
25
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
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
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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 (!
|
|
109
|
-
|
|
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
|
-
|
|
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 {
|
|
95
|
+
return {
|
|
96
|
+
ok: true,
|
|
97
|
+
credentials: mergeOpenCodeBundle(creds, refreshedCredentials, resolved.mergeProvider),
|
|
98
|
+
};
|
|
119
99
|
}
|
|
120
100
|
finally {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
38
|
-
axauth export --agent claude --output creds.json
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
#
|
|
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`)
|
package/dist/commands/vault.d.ts
CHANGED
package/dist/commands/vault.js
CHANGED
|
@@ -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
|
|
144
|
-
if (!
|
|
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
|
-
|
|
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": "
|
|
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
|
-
"
|
|
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 };
|
package/dist/auth/spawn-agent.js
DELETED
|
@@ -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 };
|