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.
Files changed (48) hide show
  1. package/README.md +120 -36
  2. package/dist/cli.js +7 -7
  3. package/dist/commands/credential.d.ts +1 -1
  4. package/dist/commands/credential.js +26 -32
  5. package/dist/commands/init.js +12 -11
  6. package/dist/commands/serve.js +1 -0
  7. package/dist/db/migrations.d.ts +3 -2
  8. package/dist/db/migrations.js +25 -120
  9. package/dist/db/repositories/api-key-utilities.d.ts +3 -3
  10. package/dist/db/repositories/api-key-utilities.js +9 -10
  11. package/dist/db/repositories/audit-log.d.ts +3 -5
  12. package/dist/db/repositories/audit-log.js +7 -8
  13. package/dist/db/repositories/credentials-queries.d.ts +9 -5
  14. package/dist/db/repositories/credentials-queries.js +28 -12
  15. package/dist/db/repositories/credentials.d.ts +31 -14
  16. package/dist/db/repositories/credentials.js +39 -21
  17. package/dist/db/repositories/list-credentials-paginated.d.ts +26 -0
  18. package/dist/db/repositories/list-credentials-paginated.js +69 -0
  19. package/dist/db/repositories/parse-credential-row.d.ts +2 -9
  20. package/dist/db/repositories/parse-credential-row.js +2 -17
  21. package/dist/db/types.d.ts +3 -12
  22. package/dist/db/types.js +1 -1
  23. package/dist/handlers/delete-credential.d.ts +2 -3
  24. package/dist/handlers/delete-credential.js +8 -11
  25. package/dist/handlers/get-credential.d.ts +6 -3
  26. package/dist/handlers/get-credential.js +35 -78
  27. package/dist/handlers/list-credentials.d.ts +19 -3
  28. package/dist/handlers/list-credentials.js +83 -8
  29. package/dist/handlers/put-credential.d.ts +10 -3
  30. package/dist/handlers/put-credential.js +25 -78
  31. package/dist/handlers/refresh-credential-on-read.d.ts +26 -0
  32. package/dist/handlers/refresh-credential-on-read.js +145 -0
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.js +1 -1
  35. package/dist/lib/credential-name.d.ts +10 -0
  36. package/dist/lib/credential-name.js +12 -0
  37. package/dist/lib/format.d.ts +1 -7
  38. package/dist/lib/format.js +7 -55
  39. package/dist/middleware/validate-parameters.d.ts +3 -3
  40. package/dist/middleware/validate-parameters.js +7 -14
  41. package/dist/server/routes.js +3 -3
  42. package/package.json +9 -9
  43. package/dist/refresh/check-refresh.d.ts +0 -29
  44. package/dist/refresh/check-refresh.js +0 -51
  45. package/dist/refresh/log-refresh.d.ts +0 -17
  46. package/dist/refresh/log-refresh.js +0 -35
  47. package/dist/refresh/refresh-manager.d.ts +0 -54
  48. 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 };