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,25 +1,31 @@
1
1
  /**
2
- * PUT /api/v1/credentials/:agent/:name handler.
2
+ * PUT /api/v1/credentials/:name handler.
3
+ *
4
+ * Fully opaque: The request body is treated as an opaque blob. axvault does not
5
+ * validate or inspect its structure - that responsibility belongs to axauth.
6
+ * The blob is encrypted and stored as-is.
3
7
  */
4
- import { CredentialType, } from "axshared";
5
8
  import { hasWriteAccess } from "../db/repositories/api-keys.js";
6
9
  import { upsertCredential } from "../db/repositories/credentials.js";
7
10
  import { logAccess } from "../db/repositories/audit-log.js";
8
11
  import { encryptCredential } from "../lib/encryption.js";
9
12
  /**
10
13
  * Store or update a credential.
14
+ *
15
+ * Request body is treated as an opaque blob. axvault does not validate its
16
+ * structure - the client (via axauth) is responsible for providing a valid
17
+ * Credentials object. The blob is encrypted and stored as-is.
11
18
  */
12
19
  function createPutCredentialHandler(database) {
13
20
  return (request, response) => {
14
21
  const authenticatedRequest = request;
15
- const { agent, name } = request.params;
22
+ const { name } = request.params;
16
23
  const { apiKey } = authenticatedRequest;
17
- if (!hasWriteAccess(apiKey, agent, name)) {
24
+ if (!hasWriteAccess(apiKey, name)) {
18
25
  logAccess(database, {
19
26
  apiKeyId: apiKey.id,
20
27
  action: "write",
21
- agent,
22
- name,
28
+ credentialName: name,
23
29
  success: false,
24
30
  errorMessage: "Access denied",
25
31
  });
@@ -27,95 +33,37 @@ function createPutCredentialHandler(database) {
27
33
  return;
28
34
  }
29
35
  const body = request.body;
30
- if (!body ||
31
- typeof body.data !== "object" ||
32
- body.data === null ||
33
- Array.isArray(body.data)) {
36
+ // Minimal validation: body must be a non-null object (the blob)
37
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
34
38
  logAccess(database, {
35
39
  apiKeyId: apiKey.id,
36
40
  action: "write",
37
- agent,
38
- name,
41
+ credentialName: name,
39
42
  success: false,
40
43
  errorMessage: "Invalid request body",
41
44
  });
42
45
  response
43
46
  .status(400)
44
- .json({ error: "Request body must include 'data' object" });
45
- return;
46
- }
47
- const typeResult = CredentialType.safeParse(body.type);
48
- if (!typeResult.success) {
49
- logAccess(database, {
50
- apiKeyId: apiKey.id,
51
- action: "write",
52
- agent,
53
- name,
54
- success: false,
55
- errorMessage: "Invalid type",
56
- });
57
- response.status(400).json({
58
- error: `Request body must include 'type' (${CredentialType.options.map((t) => `"${t}"`).join(", ")})`,
59
- });
47
+ .json({ error: "Request body must be a JSON object" });
60
48
  return;
61
49
  }
62
- const credentialType = typeResult.data;
63
- // Validate provider if present (must be non-empty string)
64
- let provider;
65
- if (body.provider !== undefined) {
66
- if (typeof body.provider !== "string" || body.provider.trim() === "") {
67
- logAccess(database, {
68
- apiKeyId: apiKey.id,
69
- action: "write",
70
- agent,
71
- name,
72
- success: false,
73
- errorMessage: "Invalid provider",
74
- });
75
- response
76
- .status(400)
77
- .json({ error: "Provider must be a non-empty string" });
78
- return;
79
- }
80
- provider = body.provider.trim();
81
- }
82
50
  try {
83
- const encrypted = encryptCredential(body.data);
84
- let expiresAt;
85
- if (body.expiresAt) {
86
- expiresAt = new Date(body.expiresAt);
87
- if (Number.isNaN(expiresAt.getTime())) {
88
- logAccess(database, {
89
- apiKeyId: apiKey.id,
90
- action: "write",
91
- agent,
92
- name,
93
- success: false,
94
- errorMessage: "Invalid expiresAt date",
95
- });
96
- response.status(400).json({ error: "Invalid expiresAt date" });
97
- return;
98
- }
99
- }
100
- upsertCredential(database, {
101
- agent,
51
+ // Encrypt the blob as-is (opaque storage)
52
+ const encrypted = encryptCredential(body);
53
+ const timestamps = upsertCredential(database, {
102
54
  name,
103
- type: credentialType,
104
- provider,
105
55
  ...encrypted,
106
- expiresAt,
107
56
  });
108
57
  logAccess(database, {
109
58
  apiKeyId: apiKey.id,
110
59
  action: "write",
111
- agent,
112
- name,
60
+ credentialName: name,
113
61
  success: true,
114
62
  });
115
63
  response.status(201).json({
116
- message: "Credential stored",
117
- agent,
118
64
  name,
65
+ createdAt: timestamps.createdAt.toISOString(),
66
+ updatedAt: timestamps.updatedAt.toISOString(),
119
67
  });
120
68
  }
121
69
  catch (error) {
@@ -123,12 +71,11 @@ function createPutCredentialHandler(database) {
123
71
  logAccess(database, {
124
72
  apiKeyId: apiKey.id,
125
73
  action: "write",
126
- agent,
127
- name,
74
+ credentialName: name,
128
75
  success: false,
129
- errorMessage: `Encryption failed: ${message}`,
76
+ errorMessage: `Storage failed: ${message}`,
130
77
  });
131
- response.status(500).json({ error: "Failed to encrypt credential" });
78
+ response.status(500).json({ error: "Failed to store credential" });
132
79
  }
133
80
  };
134
81
  }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Refresh-on-read helper for GET /api/v1/credentials/:name.
3
+ *
4
+ * Encapsulates refresh decision logic + per-credential mutex so the handler
5
+ * stays small enough for static complexity checks (FTA).
6
+ */
7
+ import type Database from "better-sqlite3";
8
+ type RefreshCredentialOnReadResult = {
9
+ status: "not-found";
10
+ } | {
11
+ status: "ok";
12
+ blob: unknown;
13
+ updatedAt: Date;
14
+ wasRefreshed: boolean;
15
+ refreshFailed: boolean;
16
+ };
17
+ declare function refreshCredentialOnRead(options: {
18
+ database: Database.Database;
19
+ apiKeyId: string;
20
+ name: string;
21
+ blob: unknown;
22
+ expectedUpdatedAt: Date;
23
+ refreshThresholdSeconds: number;
24
+ refreshTimeoutMs: number;
25
+ }): Promise<RefreshCredentialOnReadResult>;
26
+ export { refreshCredentialOnRead };
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Refresh-on-read helper for GET /api/v1/credentials/:name.
3
+ *
4
+ * Encapsulates refresh decision logic + per-credential mutex so the handler
5
+ * stays small enough for static complexity checks (FTA).
6
+ */
7
+ import { isCredentialExpired, isRefreshable, refreshBlob, } from "axauth";
8
+ import { isRefreshableCredentialType, parseCredentials } from "axshared";
9
+ import { getCredential, updateCredentialIfUnchanged, } from "../db/repositories/credentials.js";
10
+ import { logAccess } from "../db/repositories/audit-log.js";
11
+ import { decryptCredential, encryptCredential } from "../lib/encryption.js";
12
+ /** Per-credential mutex to prevent concurrent refreshes */
13
+ const pendingRefreshes = new Map();
14
+ function getRefreshPromise(name, blob, refreshTimeoutMs) {
15
+ const existing = pendingRefreshes.get(name);
16
+ if (existing)
17
+ return existing;
18
+ const promise = refreshBlob(blob, { timeout: refreshTimeoutMs });
19
+ pendingRefreshes.set(name, promise);
20
+ void promise.finally(() => {
21
+ if (pendingRefreshes.get(name) === promise) {
22
+ pendingRefreshes.delete(name);
23
+ }
24
+ });
25
+ return promise;
26
+ }
27
+ async function refreshCredentialOnRead(options) {
28
+ if (options.refreshThresholdSeconds <= 0) {
29
+ return {
30
+ status: "ok",
31
+ blob: options.blob,
32
+ updatedAt: options.expectedUpdatedAt,
33
+ wasRefreshed: false,
34
+ refreshFailed: false,
35
+ };
36
+ }
37
+ const parsedCredentials = parseCredentials(options.blob);
38
+ if (parsedCredentials === undefined ||
39
+ !isRefreshableCredentialType(parsedCredentials.type) ||
40
+ !isRefreshable(parsedCredentials.data) ||
41
+ isCredentialExpired(parsedCredentials.data, options.refreshThresholdSeconds) !== true) {
42
+ return {
43
+ status: "ok",
44
+ blob: options.blob,
45
+ updatedAt: options.expectedUpdatedAt,
46
+ wasRefreshed: false,
47
+ refreshFailed: false,
48
+ };
49
+ }
50
+ try {
51
+ const refreshResult = await getRefreshPromise(options.name, options.blob, options.refreshTimeoutMs);
52
+ if (refreshResult.ok) {
53
+ const encrypted = encryptCredential(refreshResult.blob);
54
+ const updateResult = updateCredentialIfUnchanged(options.database, {
55
+ name: options.name,
56
+ ...encrypted,
57
+ expectedUpdatedAt: options.expectedUpdatedAt,
58
+ });
59
+ if (updateResult.updated) {
60
+ logAccess(options.database, {
61
+ apiKeyId: options.apiKeyId,
62
+ action: "refresh",
63
+ credentialName: options.name,
64
+ success: true,
65
+ });
66
+ return {
67
+ status: "ok",
68
+ blob: refreshResult.blob,
69
+ updatedAt: updateResult.updatedAt,
70
+ wasRefreshed: true,
71
+ refreshFailed: false,
72
+ };
73
+ }
74
+ const currentCredential = getCredential(options.database, options.name);
75
+ if (!currentCredential) {
76
+ logAccess(options.database, {
77
+ apiKeyId: options.apiKeyId,
78
+ action: "read",
79
+ credentialName: options.name,
80
+ success: false,
81
+ errorMessage: "Credential deleted during refresh",
82
+ });
83
+ return { status: "not-found" };
84
+ }
85
+ logAccess(options.database, {
86
+ apiKeyId: options.apiKeyId,
87
+ action: "refresh",
88
+ credentialName: options.name,
89
+ success: false,
90
+ errorMessage: "Credential changed during refresh; skipped persist",
91
+ });
92
+ return {
93
+ status: "ok",
94
+ blob: decryptCredential(currentCredential),
95
+ updatedAt: currentCredential.updatedAt,
96
+ wasRefreshed: false,
97
+ refreshFailed: false,
98
+ };
99
+ }
100
+ const currentCredential = getCredential(options.database, options.name);
101
+ if (!currentCredential) {
102
+ logAccess(options.database, {
103
+ apiKeyId: options.apiKeyId,
104
+ action: "read",
105
+ credentialName: options.name,
106
+ success: false,
107
+ errorMessage: "Credential deleted during refresh",
108
+ });
109
+ return { status: "not-found" };
110
+ }
111
+ let finalBlob = options.blob;
112
+ let finalUpdatedAt = options.expectedUpdatedAt;
113
+ if (currentCredential.updatedAt.getTime() !==
114
+ options.expectedUpdatedAt.getTime()) {
115
+ finalBlob = decryptCredential(currentCredential);
116
+ finalUpdatedAt = currentCredential.updatedAt;
117
+ }
118
+ logAccess(options.database, {
119
+ apiKeyId: options.apiKeyId,
120
+ action: "refresh",
121
+ credentialName: options.name,
122
+ success: false,
123
+ errorMessage: refreshResult.error,
124
+ });
125
+ return {
126
+ status: "ok",
127
+ blob: finalBlob,
128
+ updatedAt: finalUpdatedAt,
129
+ wasRefreshed: false,
130
+ refreshFailed: true,
131
+ };
132
+ }
133
+ catch (error) {
134
+ pendingRefreshes.delete(options.name);
135
+ console.error(`Unexpected refresh error for ${options.name}:`, error instanceof Error ? error.message : error);
136
+ return {
137
+ status: "ok",
138
+ blob: options.blob,
139
+ updatedAt: options.expectedUpdatedAt,
140
+ wasRefreshed: false,
141
+ refreshFailed: true,
142
+ };
143
+ }
144
+ }
145
+ export { refreshCredentialOnRead };
package/dist/index.d.ts CHANGED
@@ -15,4 +15,4 @@ export { createApiKey, deleteApiKey, findApiKeyById, findApiKeyByKey, hasReadAcc
15
15
  export type { AuditLogEntry } from "./db/repositories/audit-log.js";
16
16
  export { getLogsForCredential, getRecentLogs, logAccess, pruneOldLogs, } from "./db/repositories/audit-log.js";
17
17
  export type { CredentialMetadata, CredentialRecord, } from "./db/repositories/credentials.js";
18
- export { deleteCredential, getCredential, listCredentials, listCredentialsForApiKey, updateExpiresAt, upsertCredential, } from "./db/repositories/credentials.js";
18
+ export { deleteCredential, getCredential, listCredentials, listCredentialsForApiKey, listCredentialsPaginated, upsertCredential, } from "./db/repositories/credentials.js";
package/dist/index.js CHANGED
@@ -11,4 +11,4 @@ export { closeDatabase, getDatabase, isDatabaseConnected, } from "./db/client.js
11
11
  export { CURRENT_VERSION, getSchemaVersion, runMigrations, } from "./db/migrations.js";
12
12
  export { createApiKey, deleteApiKey, findApiKeyById, findApiKeyByKey, hasReadAccess, hasWriteAccess, listApiKeys, updateLastUsed, } from "./db/repositories/api-keys.js";
13
13
  export { getLogsForCredential, getRecentLogs, logAccess, pruneOldLogs, } from "./db/repositories/audit-log.js";
14
- export { deleteCredential, getCredential, listCredentials, listCredentialsForApiKey, updateExpiresAt, upsertCredential, } from "./db/repositories/credentials.js";
14
+ export { deleteCredential, getCredential, listCredentials, listCredentialsForApiKey, listCredentialsPaginated, upsertCredential, } from "./db/repositories/credentials.js";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Shared credential name validation.
3
+ *
4
+ * Used by both HTTP routes and CLI utilities to keep rules consistent.
5
+ */
6
+ /** Valid pattern for credential names (and access list entries). */
7
+ declare const CREDENTIAL_NAME_PATTERN: RegExp;
8
+ declare const CREDENTIAL_NAME_FORMAT_DESCRIPTION = "1-128 characters: letters, numbers, dots, hyphens, underscores";
9
+ declare function isValidCredentialName(name: string): boolean;
10
+ export { CREDENTIAL_NAME_FORMAT_DESCRIPTION, CREDENTIAL_NAME_PATTERN, isValidCredentialName, };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Shared credential name validation.
3
+ *
4
+ * Used by both HTTP routes and CLI utilities to keep rules consistent.
5
+ */
6
+ /** Valid pattern for credential names (and access list entries). */
7
+ const CREDENTIAL_NAME_PATTERN = /^[\w.-]{1,128}$/u;
8
+ const CREDENTIAL_NAME_FORMAT_DESCRIPTION = "1-128 characters: letters, numbers, dots, hyphens, underscores";
9
+ function isValidCredentialName(name) {
10
+ return CREDENTIAL_NAME_PATTERN.test(name);
11
+ }
12
+ export { CREDENTIAL_NAME_FORMAT_DESCRIPTION, CREDENTIAL_NAME_PATTERN, isValidCredentialName, };
@@ -19,8 +19,7 @@ export declare function sanitizeForTsv(value: string): string;
19
19
  *
20
20
  * This helper is intended for past events (e.g., "last updated"), so future
21
21
  * dates are treated as unexpected and return the generic string "in the future"
22
- * instead of using granular units. For precise future-oriented formatting
23
- * (e.g., "in 2h", "in 3d"), use {@link formatExpiresAt}.
22
+ * instead of using granular units.
24
23
  */
25
24
  export declare function formatRelativeTime(date: Date | undefined): string;
26
25
  /**
@@ -87,8 +86,3 @@ export declare function formatDateForJson(date: Date | undefined): string | null
87
86
  * Returns the error's message if it's an Error, otherwise stringifies it.
88
87
  */
89
88
  export declare function getErrorMessage(error: unknown): string;
90
- /**
91
- * Format expiration time relative to now.
92
- * Returns "(never)" for no expiration, or relative time like "in 2h", "expired 5m ago".
93
- */
94
- export declare function formatExpiresAt(date: Date | undefined): string;
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Shared formatting utilities for CLI output.
3
3
  */
4
+ import { CREDENTIAL_NAME_FORMAT_DESCRIPTION, CREDENTIAL_NAME_PATTERN, } from "./credential-name.js";
4
5
  /**
5
6
  * Strip control characters and ANSI escape sequences to prevent terminal
6
7
  * injection and preserve TSV structure.
@@ -30,8 +31,7 @@ export function sanitizeForTsv(value) {
30
31
  *
31
32
  * This helper is intended for past events (e.g., "last updated"), so future
32
33
  * dates are treated as unexpected and return the generic string "in the future"
33
- * instead of using granular units. For precise future-oriented formatting
34
- * (e.g., "in 2h", "in 3d"), use {@link formatExpiresAt}.
34
+ * instead of using granular units.
35
35
  */
36
36
  export function formatRelativeTime(date) {
37
37
  if (!date)
@@ -92,24 +92,15 @@ export function formatKeyRow(key) {
92
92
  * Validate access list entry format.
93
93
  * Valid formats:
94
94
  * - "*" (full wildcard granting access to all credentials)
95
- * - "agent/name" (exactly one slash, both parts non-empty, no wildcards)
95
+ * - "name" (1-128 characters: letters, numbers, dots, hyphens, underscores)
96
96
  *
97
- * Partial wildcards like "claude/*" are rejected because the authorization
97
+ * Partial wildcards like "claude.*" are rejected because the authorization
98
98
  * logic uses exact matching, not pattern matching.
99
99
  */
100
100
  function isValidAccessEntry(entry) {
101
101
  if (entry === "*")
102
102
  return true;
103
- const parts = entry.split("/");
104
- const agent = parts[0];
105
- const name = parts[1];
106
- return (parts.length === 2 &&
107
- agent !== undefined &&
108
- agent.length > 0 &&
109
- !agent.includes("*") &&
110
- name !== undefined &&
111
- name.length > 0 &&
112
- !name.includes("*"));
103
+ return CREDENTIAL_NAME_PATTERN.test(entry);
113
104
  }
114
105
  /**
115
106
  * Parse comma-separated access list into array of entries.
@@ -132,7 +123,7 @@ export function parseAccessList(value) {
132
123
  const entries = rawEntries
133
124
  .map((item) => item.trim())
134
125
  .filter((item) => item.length > 0);
135
- // Validate entry format: must be "*" or "agent/name"
126
+ // Validate entry format: must be "*" or "name"
136
127
  if (!entries.every((entry) => isValidAccessEntry(entry))) {
137
128
  return { error: "invalid-format" };
138
129
  }
@@ -145,7 +136,7 @@ export function getAccessListErrorMessage(error) {
145
136
  if (error === "control-chars") {
146
137
  return "Access list contains control characters.";
147
138
  }
148
- return 'Invalid access list entry format. Entries must be "*" or "agent/name" (e.g., claude/work).';
139
+ return `Invalid access list entry format. Entries must be "*" or a credential name (${CREDENTIAL_NAME_FORMAT_DESCRIPTION}).`;
149
140
  }
150
141
  /**
151
142
  * Normalize access list by collapsing to ['*'] if wildcard is present with other entries.
@@ -186,42 +177,3 @@ export function formatDateForJson(date) {
186
177
  export function getErrorMessage(error) {
187
178
  return error instanceof Error ? error.message : String(error);
188
179
  }
189
- /**
190
- * Format expiration time relative to now.
191
- * Returns "(never)" for no expiration, or relative time like "in 2h", "expired 5m ago".
192
- */
193
- export function formatExpiresAt(date) {
194
- if (!date)
195
- return "(never)";
196
- const now = Date.now();
197
- const diff = date.getTime() - now;
198
- // Already expired
199
- if (diff < 0) {
200
- const elapsed = Math.abs(diff);
201
- const seconds = Math.floor(elapsed / 1000);
202
- if (seconds < 60)
203
- return "expired just now";
204
- const minutes = Math.floor(seconds / 60);
205
- if (minutes < 60)
206
- return `expired ${minutes}m ago`;
207
- const hours = Math.floor(minutes / 60);
208
- if (hours < 24)
209
- return `expired ${hours}h ago`;
210
- const days = Math.floor(hours / 24);
211
- return `expired ${days}d ago`;
212
- }
213
- // Future expiration
214
- const seconds = Math.floor(diff / 1000);
215
- if (seconds === 0)
216
- return "expires soon";
217
- if (seconds < 60)
218
- return `in ${seconds}s`;
219
- const minutes = Math.floor(seconds / 60);
220
- if (minutes < 60)
221
- return `in ${minutes}m`;
222
- const hours = Math.floor(minutes / 60);
223
- if (hours < 24)
224
- return `in ${hours}h`;
225
- const days = Math.floor(hours / 24);
226
- return `in ${days}d`;
227
- }
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Path parameter validation middleware.
3
3
  *
4
- * Validates agent and name parameters to prevent injection attacks
5
- * and ensure consistent credential naming.
4
+ * Validates credential name parameter to prevent injection attacks
5
+ * and ensure consistent naming.
6
6
  */
7
7
  import type { NextFunction, Request, Response } from "express";
8
- /** Validate agent and name path parameters */
8
+ /** Validate credential name path parameter */
9
9
  declare function validateParameters(request: Request, response: Response, next: NextFunction): void;
10
10
  export { validateParameters };
@@ -1,23 +1,16 @@
1
1
  /**
2
2
  * Path parameter validation middleware.
3
3
  *
4
- * Validates agent and name parameters to prevent injection attacks
5
- * and ensure consistent credential naming.
4
+ * Validates credential name parameter to prevent injection attacks
5
+ * and ensure consistent naming.
6
6
  */
7
- /** Valid pattern for agent and name path segments */
8
- const VALID_PATH_SEGMENT = /^[\w.-]{1,64}$/u;
9
- /** Validate agent and name path parameters */
7
+ import { CREDENTIAL_NAME_FORMAT_DESCRIPTION, isValidCredentialName, } from "../lib/credential-name.js";
8
+ /** Validate credential name path parameter */
10
9
  function validateParameters(request, response, next) {
11
- const { agent, name } = request.params;
12
- if (agent !== undefined && !VALID_PATH_SEGMENT.test(agent)) {
10
+ const { name } = request.params;
11
+ if (name !== undefined && !isValidCredentialName(name)) {
13
12
  response.status(400).json({
14
- error: "Invalid agent format. Must be 1-64 characters: letters, numbers, dots, hyphens, underscores.",
15
- });
16
- return;
17
- }
18
- if (name !== undefined && !VALID_PATH_SEGMENT.test(name)) {
19
- response.status(400).json({
20
- error: "Invalid name format. Must be 1-64 characters: letters, numbers, dots, hyphens, underscores.",
13
+ error: `Invalid name format. Must be ${CREDENTIAL_NAME_FORMAT_DESCRIPTION}.`,
21
14
  });
22
15
  return;
23
16
  }
@@ -29,11 +29,11 @@ export function createCredentialRouter(database, config) {
29
29
  // List all accessible credentials
30
30
  router.get("/api/v1/credentials", createListCredentialsHandler(database));
31
31
  // Get a specific credential (with automatic refresh)
32
- router.get("/api/v1/credentials/:agent/:name", validateParameters, createGetCredentialHandler(database, config));
32
+ router.get("/api/v1/credentials/:name", validateParameters, createGetCredentialHandler(database, config));
33
33
  // Store/update a credential
34
- router.put("/api/v1/credentials/:agent/:name", validateParameters, createPutCredentialHandler(database));
34
+ router.put("/api/v1/credentials/:name", validateParameters, createPutCredentialHandler(database));
35
35
  // Delete a credential
36
- router.delete("/api/v1/credentials/:agent/:name", validateParameters, createDeleteCredentialHandler(database));
36
+ router.delete("/api/v1/credentials/:name", validateParameters, createDeleteCredentialHandler(database));
37
37
  return router;
38
38
  }
39
39
  /** Create all API routers (legacy compatibility) */
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "axvault",
3
3
  "author": "Łukasz Jerciński",
4
4
  "license": "MIT",
5
- "version": "1.8.0",
5
+ "version": "1.8.2",
6
6
  "description": "Remote credential storage server for axkit",
7
7
  "repository": {
8
8
  "type": "git",
@@ -49,8 +49,8 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@commander-js/extra-typings": "^14.0.0",
52
- "axauth": "^2.1.0",
53
- "axshared": "2.0.0",
52
+ "axauth": "^3.1.1",
53
+ "axshared": "4.0.0",
54
54
  "better-sqlite3": "^12.6.2",
55
55
  "commander": "^14.0.2",
56
56
  "express": "^5.2.1"
@@ -70,7 +70,7 @@
70
70
  "automation",
71
71
  "coding-assistant"
72
72
  ],
73
- "packageManager": "pnpm@10.27.0",
73
+ "packageManager": "pnpm@10.28.1",
74
74
  "engines": {
75
75
  "node": ">=22.14.0"
76
76
  },
@@ -78,16 +78,16 @@
78
78
  "@total-typescript/ts-reset": "^0.6.1",
79
79
  "@types/better-sqlite3": "^7.6.13",
80
80
  "@types/express": "^5.0.6",
81
- "@types/node": "^25.0.9",
82
- "@vitest/coverage-v8": "^4.0.17",
81
+ "@types/node": "^25.0.10",
82
+ "@vitest/coverage-v8": "^4.0.18",
83
83
  "eslint": "^9.39.2",
84
84
  "eslint-config-axkit": "^1.1.0",
85
85
  "fta-check": "^1.5.1",
86
86
  "fta-cli": "^3.0.0",
87
- "knip": "^5.81.0",
88
- "prettier": "3.8.0",
87
+ "knip": "^5.82.1",
88
+ "prettier": "3.8.1",
89
89
  "semantic-release": "^25.0.2",
90
90
  "typescript": "^5.9.3",
91
- "vitest": "^4.0.17"
91
+ "vitest": "^4.0.18"
92
92
  }
93
93
  }
@@ -1,29 +0,0 @@
1
- /**
2
- * Pure functions for checking if credentials need refresh.
3
- *
4
- * Extracted from refresh-manager to reduce complexity.
5
- */
6
- import { type Credentials } from "axauth";
7
- /**
8
- * Check if credential needs refresh based on expiry.
9
- *
10
- * Only returns true if:
11
- * 1. Credential data is an object with refresh token (required for axauth refresh)
12
- * 2. Credential has expiry info that is within threshold
13
- *
14
- * @param data - Decrypted credential data (accepts unknown for safety)
15
- * @param thresholdSeconds - Refresh if expires within this many seconds
16
- * @returns true if refresh needed, false otherwise
17
- */
18
- declare function needsRefresh(data: unknown, thresholdSeconds: number): boolean;
19
- /**
20
- * Map vault credential data to axauth Credentials type.
21
- *
22
- * @param agent - Agent CLI name
23
- * @param data - Decrypted credential data from vault (must have refresh token)
24
- * @param provider - Optional provider for multi-provider agents like OpenCode
25
- * @returns Credentials object for axauth, or undefined if not mappable
26
- */
27
- declare function toAxauthCredentials(agent: string, data: Record<string, unknown>, provider?: string): Credentials | undefined;
28
- export { extractExpiryDate, isRefreshable } from "axauth";
29
- export { needsRefresh, toAxauthCredentials };