axvault 1.8.0 → 1.8.2
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/README.md +120 -36
- package/dist/cli.js +7 -7
- package/dist/commands/credential.d.ts +1 -1
- package/dist/commands/credential.js +26 -32
- package/dist/commands/init.js +12 -11
- package/dist/commands/serve.js +1 -0
- package/dist/db/migrations.d.ts +3 -2
- package/dist/db/migrations.js +25 -120
- package/dist/db/repositories/api-key-utilities.d.ts +3 -3
- package/dist/db/repositories/api-key-utilities.js +9 -10
- package/dist/db/repositories/audit-log.d.ts +3 -5
- package/dist/db/repositories/audit-log.js +7 -8
- package/dist/db/repositories/credentials-queries.d.ts +9 -5
- package/dist/db/repositories/credentials-queries.js +28 -12
- package/dist/db/repositories/credentials.d.ts +31 -14
- package/dist/db/repositories/credentials.js +39 -21
- package/dist/db/repositories/list-credentials-paginated.d.ts +26 -0
- package/dist/db/repositories/list-credentials-paginated.js +69 -0
- package/dist/db/repositories/parse-credential-row.d.ts +2 -9
- package/dist/db/repositories/parse-credential-row.js +2 -17
- package/dist/db/types.d.ts +3 -12
- package/dist/db/types.js +1 -1
- package/dist/handlers/delete-credential.d.ts +2 -3
- package/dist/handlers/delete-credential.js +8 -11
- package/dist/handlers/get-credential.d.ts +6 -3
- package/dist/handlers/get-credential.js +35 -78
- package/dist/handlers/list-credentials.d.ts +19 -3
- package/dist/handlers/list-credentials.js +83 -8
- package/dist/handlers/put-credential.d.ts +10 -3
- package/dist/handlers/put-credential.js +25 -78
- package/dist/handlers/refresh-credential-on-read.d.ts +26 -0
- package/dist/handlers/refresh-credential-on-read.js +145 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/credential-name.d.ts +10 -0
- package/dist/lib/credential-name.js +12 -0
- package/dist/lib/format.d.ts +1 -7
- package/dist/lib/format.js +7 -55
- package/dist/middleware/validate-parameters.d.ts +3 -3
- package/dist/middleware/validate-parameters.js +7 -14
- package/dist/server/routes.js +3 -3
- package/package.json +9 -9
- package/dist/refresh/check-refresh.d.ts +0 -29
- package/dist/refresh/check-refresh.js +0 -51
- package/dist/refresh/log-refresh.d.ts +0 -17
- package/dist/refresh/log-refresh.js +0 -35
- package/dist/refresh/refresh-manager.d.ts +0 -54
- package/dist/refresh/refresh-manager.js +0 -137
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure functions for checking if credentials need refresh.
|
|
3
|
-
*
|
|
4
|
-
* Extracted from refresh-manager to reduce complexity.
|
|
5
|
-
*/
|
|
6
|
-
import { AGENT_CLIS } from "axshared";
|
|
7
|
-
import { isCredentialExpired, isRefreshable } from "axauth";
|
|
8
|
-
/** Valid agent CLI names - sourced from axshared for consistency */
|
|
9
|
-
const VALID_AGENTS = new Set(AGENT_CLIS);
|
|
10
|
-
/**
|
|
11
|
-
* Check if credential needs refresh based on expiry.
|
|
12
|
-
*
|
|
13
|
-
* Only returns true if:
|
|
14
|
-
* 1. Credential data is an object with refresh token (required for axauth refresh)
|
|
15
|
-
* 2. Credential has expiry info that is within threshold
|
|
16
|
-
*
|
|
17
|
-
* @param data - Decrypted credential data (accepts unknown for safety)
|
|
18
|
-
* @param thresholdSeconds - Refresh if expires within this many seconds
|
|
19
|
-
* @returns true if refresh needed, false otherwise
|
|
20
|
-
*/
|
|
21
|
-
function needsRefresh(data, thresholdSeconds) {
|
|
22
|
-
// Must be an object with refresh token to be refreshable
|
|
23
|
-
if (!isRefreshable(data))
|
|
24
|
-
return false;
|
|
25
|
-
const expired = isCredentialExpired(data, thresholdSeconds);
|
|
26
|
-
// undefined means no expiry info found - don't refresh
|
|
27
|
-
return expired === true;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Map vault credential data to axauth Credentials type.
|
|
31
|
-
*
|
|
32
|
-
* @param agent - Agent CLI name
|
|
33
|
-
* @param data - Decrypted credential data from vault (must have refresh token)
|
|
34
|
-
* @param provider - Optional provider for multi-provider agents like OpenCode
|
|
35
|
-
* @returns Credentials object for axauth, or undefined if not mappable
|
|
36
|
-
*/
|
|
37
|
-
function toAxauthCredentials(agent, data, provider) {
|
|
38
|
-
if (!VALID_AGENTS.has(agent)) {
|
|
39
|
-
return undefined;
|
|
40
|
-
}
|
|
41
|
-
// Only OAuth credentials with refresh tokens can be refreshed
|
|
42
|
-
return {
|
|
43
|
-
agent: agent,
|
|
44
|
-
type: "oauth-credentials",
|
|
45
|
-
...(provider && { provider }),
|
|
46
|
-
data,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
// Re-export from axauth for convenience
|
|
50
|
-
export { extractExpiryDate, isRefreshable } from "axauth";
|
|
51
|
-
export { needsRefresh, toAxauthCredentials };
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Logging helpers for refresh operations.
|
|
3
|
-
*/
|
|
4
|
-
import type Database from "better-sqlite3";
|
|
5
|
-
interface RefreshLogContext {
|
|
6
|
-
database: Database.Database;
|
|
7
|
-
apiKeyId: string;
|
|
8
|
-
agent: string;
|
|
9
|
-
name: string;
|
|
10
|
-
}
|
|
11
|
-
/** Log a successful refresh */
|
|
12
|
-
declare function logRefreshSuccess(context: RefreshLogContext): void;
|
|
13
|
-
/** Log a failed refresh (best-effort, ignores logging errors) */
|
|
14
|
-
declare function logRefreshError(context: RefreshLogContext, errorMessage: string): void;
|
|
15
|
-
/** Extract error message from unknown error */
|
|
16
|
-
declare function getErrorMessage(error: unknown): string;
|
|
17
|
-
export { getErrorMessage, logRefreshError, logRefreshSuccess };
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Logging helpers for refresh operations.
|
|
3
|
-
*/
|
|
4
|
-
import { logAccess } from "../db/repositories/audit-log.js";
|
|
5
|
-
/** Log a successful refresh */
|
|
6
|
-
function logRefreshSuccess(context) {
|
|
7
|
-
logAccess(context.database, {
|
|
8
|
-
apiKeyId: context.apiKeyId,
|
|
9
|
-
action: "refresh",
|
|
10
|
-
agent: context.agent,
|
|
11
|
-
name: context.name,
|
|
12
|
-
success: true,
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
/** Log a failed refresh (best-effort, ignores logging errors) */
|
|
16
|
-
function logRefreshError(context, errorMessage) {
|
|
17
|
-
try {
|
|
18
|
-
logAccess(context.database, {
|
|
19
|
-
apiKeyId: context.apiKeyId,
|
|
20
|
-
action: "refresh",
|
|
21
|
-
agent: context.agent,
|
|
22
|
-
name: context.name,
|
|
23
|
-
success: false,
|
|
24
|
-
errorMessage,
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
// Ignore logging failures
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
/** Extract error message from unknown error */
|
|
32
|
-
function getErrorMessage(error) {
|
|
33
|
-
return error instanceof Error ? error.message : String(error);
|
|
34
|
-
}
|
|
35
|
-
export { getErrorMessage, logRefreshError, logRefreshSuccess };
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Refresh manager with per-credential mutex locking.
|
|
3
|
-
*
|
|
4
|
-
* Prevents concurrent refreshes of the same credential and coordinates
|
|
5
|
-
* with axauth's refresh functionality.
|
|
6
|
-
*/
|
|
7
|
-
import type Database from "better-sqlite3";
|
|
8
|
-
import type { CredentialType } from "axshared";
|
|
9
|
-
/** Key for credential-specific mutex */
|
|
10
|
-
type CredentialKey = `${string}/${string}`;
|
|
11
|
-
/** Result type for the full refresh operation */
|
|
12
|
-
type FullRefreshResult = {
|
|
13
|
-
ok: true;
|
|
14
|
-
data: unknown;
|
|
15
|
-
expiresAt: Date | undefined;
|
|
16
|
-
updatedAt: Date;
|
|
17
|
-
} | {
|
|
18
|
-
ok: false;
|
|
19
|
-
error: string;
|
|
20
|
-
};
|
|
21
|
-
/** Options for refresh operation */
|
|
22
|
-
interface RefreshManagerOptions {
|
|
23
|
-
timeoutMs: number;
|
|
24
|
-
}
|
|
25
|
-
/** Build credential key for mutex */
|
|
26
|
-
declare function buildKey(agent: string, name: string): CredentialKey;
|
|
27
|
-
/**
|
|
28
|
-
* Perform refresh with mutex locking.
|
|
29
|
-
*
|
|
30
|
-
* If a refresh is already in progress for this credential, waits for it
|
|
31
|
-
* rather than starting a new one. The mutex covers the FULL operation
|
|
32
|
-
* (refresh + validate + persist), ensuring all callers see the same result.
|
|
33
|
-
*
|
|
34
|
-
* Note: There is a small race window where a request that loaded stale
|
|
35
|
-
* credential data could start a duplicate refresh if it checks the mutex
|
|
36
|
-
* right after the previous refresh completes. This is rare (requires precise
|
|
37
|
-
* timing), harmless (produces correct results), and self-limiting (OAuth
|
|
38
|
-
* providers handle duplicate refreshes). The complexity of cooldown timers
|
|
39
|
-
* or reference counting isn't justified for this edge case.
|
|
40
|
-
*
|
|
41
|
-
* @param database - Database connection
|
|
42
|
-
* @param agent - Agent name
|
|
43
|
-
* @param name - Credential name
|
|
44
|
-
* @param type - Credential type (must be "oauth"; caller must gate on type)
|
|
45
|
-
* @param provider - Optional provider for multi-provider agents like OpenCode
|
|
46
|
-
* @param data - Current decrypted credential data
|
|
47
|
-
* @param apiKeyId - API key ID for audit logging
|
|
48
|
-
* @param originalUpdatedAt - Original updatedAt for optimistic locking
|
|
49
|
-
* @param options - Refresh options
|
|
50
|
-
* @returns Refresh result with new data, expiresAt, updatedAt or error
|
|
51
|
-
*/
|
|
52
|
-
declare function refreshWithMutex(database: Database.Database, agent: string, name: string, type: CredentialType, provider: string | undefined, data: Record<string, unknown>, apiKeyId: string, originalUpdatedAt: Date, options: RefreshManagerOptions): Promise<FullRefreshResult>;
|
|
53
|
-
export { extractExpiryDate, isRefreshable, needsRefresh, toAxauthCredentials, } from "./check-refresh.js";
|
|
54
|
-
export { buildKey, refreshWithMutex };
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Refresh manager with per-credential mutex locking.
|
|
3
|
-
*
|
|
4
|
-
* Prevents concurrent refreshes of the same credential and coordinates
|
|
5
|
-
* with axauth's refresh functionality.
|
|
6
|
-
*/
|
|
7
|
-
import { refreshCredentials } from "axauth";
|
|
8
|
-
import { getCredential, upsertCredential, } from "../db/repositories/credentials.js";
|
|
9
|
-
import { encryptCredential } from "../lib/encryption.js";
|
|
10
|
-
import { extractExpiryDate, toAxauthCredentials } from "./check-refresh.js";
|
|
11
|
-
import { getErrorMessage, logRefreshError, logRefreshSuccess, } from "./log-refresh.js";
|
|
12
|
-
/** Map of in-progress refreshes to prevent concurrent operations */
|
|
13
|
-
const pendingRefreshes = new Map();
|
|
14
|
-
/** Build credential key for mutex */
|
|
15
|
-
function buildKey(agent, name) {
|
|
16
|
-
return `${agent}/${name}`;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Execute the full refresh operation: call axauth, validate, persist.
|
|
20
|
-
* This is the inner operation that gets stored in the mutex.
|
|
21
|
-
*/
|
|
22
|
-
async function executeRefresh(database, agent, name, type, provider, data, apiKeyId, originalUpdatedAt, refreshStartedAt, options) {
|
|
23
|
-
const logContext = { database, apiKeyId, agent, name };
|
|
24
|
-
// Map to axauth format, including provider for multi-provider agents
|
|
25
|
-
const creds = toAxauthCredentials(agent, data, provider);
|
|
26
|
-
if (!creds) {
|
|
27
|
-
return { ok: false, error: `Unknown agent: ${agent}` };
|
|
28
|
-
}
|
|
29
|
-
try {
|
|
30
|
-
const result = await refreshCredentials(creds, {
|
|
31
|
-
timeout: options.timeoutMs,
|
|
32
|
-
provider,
|
|
33
|
-
});
|
|
34
|
-
if (!result.ok) {
|
|
35
|
-
logRefreshError(logContext, result.error);
|
|
36
|
-
return { ok: false, error: result.error };
|
|
37
|
-
}
|
|
38
|
-
// Check if credential still exists and wasn't modified during refresh
|
|
39
|
-
const existingCredential = getCredential(database, agent, name);
|
|
40
|
-
if (!existingCredential) {
|
|
41
|
-
logRefreshError(logContext, "Credential was deleted during refresh");
|
|
42
|
-
return { ok: false, error: "Credential was deleted during refresh" };
|
|
43
|
-
}
|
|
44
|
-
// Optimistic locking: abort if credential was modified during refresh
|
|
45
|
-
if (existingCredential.updatedAt.getTime() !== originalUpdatedAt.getTime()) {
|
|
46
|
-
logRefreshError(logContext, "Credential was modified during refresh");
|
|
47
|
-
return { ok: false, error: "Credential was modified during refresh" };
|
|
48
|
-
}
|
|
49
|
-
// Persist newly refreshed credentials to database
|
|
50
|
-
// Merge original data with refreshed data to preserve fields like refresh_token
|
|
51
|
-
const newData = { ...data, ...result.credentials.data };
|
|
52
|
-
const encrypted = encryptCredential(newData);
|
|
53
|
-
const expiresAt = extractExpiryDate(newData);
|
|
54
|
-
try {
|
|
55
|
-
upsertCredential(database, {
|
|
56
|
-
agent,
|
|
57
|
-
name,
|
|
58
|
-
type,
|
|
59
|
-
provider: existingCredential.provider,
|
|
60
|
-
...encrypted,
|
|
61
|
-
expiresAt,
|
|
62
|
-
});
|
|
63
|
-
logRefreshSuccess(logContext);
|
|
64
|
-
return {
|
|
65
|
-
ok: true,
|
|
66
|
-
data: newData,
|
|
67
|
-
expiresAt,
|
|
68
|
-
updatedAt: refreshStartedAt,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
catch (error) {
|
|
72
|
-
const message = getErrorMessage(error);
|
|
73
|
-
logRefreshError(logContext, `Persistence failed: ${message}`);
|
|
74
|
-
return {
|
|
75
|
-
ok: false,
|
|
76
|
-
error: `Failed to persist refreshed credentials: ${message}`,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
catch (error) {
|
|
81
|
-
// Handle unexpected errors (e.g., refreshCredentials promise rejection)
|
|
82
|
-
const message = getErrorMessage(error);
|
|
83
|
-
logRefreshError(logContext, `Refresh error: ${message}`);
|
|
84
|
-
return { ok: false, error: `Refresh failed: ${message}` };
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Perform refresh with mutex locking.
|
|
89
|
-
*
|
|
90
|
-
* If a refresh is already in progress for this credential, waits for it
|
|
91
|
-
* rather than starting a new one. The mutex covers the FULL operation
|
|
92
|
-
* (refresh + validate + persist), ensuring all callers see the same result.
|
|
93
|
-
*
|
|
94
|
-
* Note: There is a small race window where a request that loaded stale
|
|
95
|
-
* credential data could start a duplicate refresh if it checks the mutex
|
|
96
|
-
* right after the previous refresh completes. This is rare (requires precise
|
|
97
|
-
* timing), harmless (produces correct results), and self-limiting (OAuth
|
|
98
|
-
* providers handle duplicate refreshes). The complexity of cooldown timers
|
|
99
|
-
* or reference counting isn't justified for this edge case.
|
|
100
|
-
*
|
|
101
|
-
* @param database - Database connection
|
|
102
|
-
* @param agent - Agent name
|
|
103
|
-
* @param name - Credential name
|
|
104
|
-
* @param type - Credential type (must be "oauth"; caller must gate on type)
|
|
105
|
-
* @param provider - Optional provider for multi-provider agents like OpenCode
|
|
106
|
-
* @param data - Current decrypted credential data
|
|
107
|
-
* @param apiKeyId - API key ID for audit logging
|
|
108
|
-
* @param originalUpdatedAt - Original updatedAt for optimistic locking
|
|
109
|
-
* @param options - Refresh options
|
|
110
|
-
* @returns Refresh result with new data, expiresAt, updatedAt or error
|
|
111
|
-
*/
|
|
112
|
-
async function refreshWithMutex(database, agent, name, type, provider, data, apiKeyId, originalUpdatedAt, options) {
|
|
113
|
-
const key = buildKey(agent, name);
|
|
114
|
-
// Check if refresh already in progress
|
|
115
|
-
const existing = pendingRefreshes.get(key);
|
|
116
|
-
if (existing) {
|
|
117
|
-
// Wait for existing FULL operation to complete
|
|
118
|
-
// All callers observe the same final success/failure result
|
|
119
|
-
return existing.promise;
|
|
120
|
-
}
|
|
121
|
-
// Start new refresh - store promise for the FULL operation
|
|
122
|
-
const refreshStartedAt = new Date();
|
|
123
|
-
const fullOperationPromise = executeRefresh(database, agent, name, type, provider, data, apiKeyId, originalUpdatedAt, refreshStartedAt, options);
|
|
124
|
-
pendingRefreshes.set(key, {
|
|
125
|
-
promise: fullOperationPromise,
|
|
126
|
-
startedAt: refreshStartedAt,
|
|
127
|
-
});
|
|
128
|
-
try {
|
|
129
|
-
return await fullOperationPromise;
|
|
130
|
-
}
|
|
131
|
-
finally {
|
|
132
|
-
// Always clean up mutex
|
|
133
|
-
pendingRefreshes.delete(key);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
export { extractExpiryDate, isRefreshable, needsRefresh, toAxauthCredentials, } from "./check-refresh.js";
|
|
137
|
-
export { buildKey, refreshWithMutex };
|