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,25 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PUT /api/v1/credentials/:
|
|
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 {
|
|
22
|
+
const { name } = request.params;
|
|
16
23
|
const { apiKey } = authenticatedRequest;
|
|
17
|
-
if (!hasWriteAccess(apiKey,
|
|
24
|
+
if (!hasWriteAccess(apiKey, name)) {
|
|
18
25
|
logAccess(database, {
|
|
19
26
|
apiKeyId: apiKey.id,
|
|
20
27
|
action: "write",
|
|
21
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
name,
|
|
74
|
+
credentialName: name,
|
|
128
75
|
success: false,
|
|
129
|
-
errorMessage: `
|
|
76
|
+
errorMessage: `Storage failed: ${message}`,
|
|
130
77
|
});
|
|
131
|
-
response.status(500).json({ error: "Failed to
|
|
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,
|
|
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,
|
|
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, };
|
package/dist/lib/format.d.ts
CHANGED
|
@@ -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.
|
|
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;
|
package/dist/lib/format.js
CHANGED
|
@@ -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.
|
|
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
|
-
* - "
|
|
95
|
+
* - "name" (1-128 characters: letters, numbers, dots, hyphens, underscores)
|
|
96
96
|
*
|
|
97
|
-
* Partial wildcards like "claude
|
|
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
|
-
|
|
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 "
|
|
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
|
|
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
|
|
5
|
-
* and ensure consistent
|
|
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
|
|
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
|
|
5
|
-
* and ensure consistent
|
|
4
|
+
* Validates credential name parameter to prevent injection attacks
|
|
5
|
+
* and ensure consistent naming.
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
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 {
|
|
12
|
-
if (
|
|
10
|
+
const { name } = request.params;
|
|
11
|
+
if (name !== undefined && !isValidCredentialName(name)) {
|
|
13
12
|
response.status(400).json({
|
|
14
|
-
error:
|
|
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
|
}
|
package/dist/server/routes.js
CHANGED
|
@@ -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/:
|
|
32
|
+
router.get("/api/v1/credentials/:name", validateParameters, createGetCredentialHandler(database, config));
|
|
33
33
|
// Store/update a credential
|
|
34
|
-
router.put("/api/v1/credentials/:
|
|
34
|
+
router.put("/api/v1/credentials/:name", validateParameters, createPutCredentialHandler(database));
|
|
35
35
|
// Delete a credential
|
|
36
|
-
router.delete("/api/v1/credentials/:
|
|
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.
|
|
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": "^
|
|
53
|
-
"axshared": "
|
|
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.
|
|
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.
|
|
82
|
-
"@vitest/coverage-v8": "^4.0.
|
|
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.
|
|
88
|
-
"prettier": "3.8.
|
|
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.
|
|
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 };
|