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,41 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure functions for parsing credential database rows.
|
|
3
|
+
*
|
|
4
|
+
* Credentials are fully opaque blobs - we don't inspect their structure.
|
|
3
5
|
*/
|
|
4
|
-
import { CredentialType } from "axshared";
|
|
5
|
-
/** Parse and validate credential type from database */
|
|
6
|
-
function parseCredentialType(type) {
|
|
7
|
-
const result = CredentialType.safeParse(type);
|
|
8
|
-
if (!result.success) {
|
|
9
|
-
throw new Error(`Invalid credential type: ${type}`);
|
|
10
|
-
}
|
|
11
|
-
return result.data;
|
|
12
|
-
}
|
|
13
6
|
/** Convert database row to record */
|
|
14
7
|
function rowToRecord(row) {
|
|
15
8
|
return {
|
|
16
|
-
agent: row.agent,
|
|
17
9
|
name: row.name,
|
|
18
|
-
type: parseCredentialType(row.type),
|
|
19
|
-
provider: row.provider ?? undefined,
|
|
20
10
|
encryptedData: row.encrypted_data,
|
|
21
11
|
salt: row.salt,
|
|
22
12
|
iv: row.iv,
|
|
23
13
|
authTag: row.auth_tag,
|
|
24
14
|
createdAt: new Date(row.created_at),
|
|
25
15
|
updatedAt: new Date(row.updated_at),
|
|
26
|
-
expiresAt: row.expires_at ? new Date(row.expires_at) : undefined,
|
|
27
16
|
};
|
|
28
17
|
}
|
|
29
18
|
/** Convert metadata row to metadata */
|
|
30
19
|
function rowToMetadata(row) {
|
|
31
20
|
return {
|
|
32
|
-
agent: row.agent,
|
|
33
21
|
name: row.name,
|
|
34
|
-
type: parseCredentialType(row.type),
|
|
35
|
-
provider: row.provider ?? undefined,
|
|
36
22
|
createdAt: new Date(row.created_at),
|
|
37
23
|
updatedAt: new Date(row.updated_at),
|
|
38
|
-
expiresAt: row.expires_at ? new Date(row.expires_at) : undefined,
|
|
39
24
|
};
|
|
40
25
|
}
|
|
41
26
|
export { rowToMetadata, rowToRecord };
|
package/dist/db/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Database row types
|
|
2
|
+
* Database row types.
|
|
3
3
|
*/
|
|
4
4
|
/** Raw API key row from database */
|
|
5
5
|
export interface ApiKeyRow {
|
|
@@ -19,32 +19,23 @@ export interface AuditLogRow {
|
|
|
19
19
|
timestamp: number;
|
|
20
20
|
api_key_id: string | null;
|
|
21
21
|
action: string;
|
|
22
|
-
|
|
23
|
-
name: string | null;
|
|
22
|
+
credential_name: string | null;
|
|
24
23
|
success: number;
|
|
25
24
|
error_message: string | null;
|
|
26
25
|
}
|
|
27
|
-
/** Raw credential row from database */
|
|
26
|
+
/** Raw credential row from database (opaque blob storage) */
|
|
28
27
|
export interface CredentialRow {
|
|
29
|
-
agent: string;
|
|
30
28
|
name: string;
|
|
31
|
-
type: string;
|
|
32
|
-
provider: string | null;
|
|
33
29
|
encrypted_data: Buffer;
|
|
34
30
|
salt: Buffer;
|
|
35
31
|
iv: Buffer;
|
|
36
32
|
auth_tag: Buffer;
|
|
37
33
|
created_at: number;
|
|
38
34
|
updated_at: number;
|
|
39
|
-
expires_at: number | null;
|
|
40
35
|
}
|
|
41
36
|
/** Raw credential metadata row from database */
|
|
42
37
|
export interface MetadataRow {
|
|
43
|
-
agent: string;
|
|
44
38
|
name: string;
|
|
45
|
-
type: string;
|
|
46
|
-
provider: string | null;
|
|
47
39
|
created_at: number;
|
|
48
40
|
updated_at: number;
|
|
49
|
-
expires_at: number | null;
|
|
50
41
|
}
|
package/dist/db/types.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DELETE /api/v1/credentials/:
|
|
2
|
+
* DELETE /api/v1/credentials/:name handler.
|
|
3
3
|
*/
|
|
4
4
|
import type { RequestHandler } from "express";
|
|
5
5
|
import type Database from "better-sqlite3";
|
|
6
|
-
/** Handler type for credential routes with
|
|
6
|
+
/** Handler type for credential routes with name param */
|
|
7
7
|
type CredentialHandler = RequestHandler<{
|
|
8
|
-
agent: string;
|
|
9
8
|
name: string;
|
|
10
9
|
}, unknown, unknown, unknown>;
|
|
11
10
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DELETE /api/v1/credentials/:
|
|
2
|
+
* DELETE /api/v1/credentials/:name handler.
|
|
3
3
|
*/
|
|
4
4
|
import { hasWriteAccess } from "../db/repositories/api-keys.js";
|
|
5
5
|
import { deleteCredential } from "../db/repositories/credentials.js";
|
|
@@ -10,27 +10,25 @@ import { logAccess } from "../db/repositories/audit-log.js";
|
|
|
10
10
|
function createDeleteCredentialHandler(database) {
|
|
11
11
|
return (request, response) => {
|
|
12
12
|
const authenticatedRequest = request;
|
|
13
|
-
const {
|
|
13
|
+
const { name } = request.params;
|
|
14
14
|
const { apiKey } = authenticatedRequest;
|
|
15
|
-
if (!hasWriteAccess(apiKey,
|
|
15
|
+
if (!hasWriteAccess(apiKey, name)) {
|
|
16
16
|
logAccess(database, {
|
|
17
17
|
apiKeyId: apiKey.id,
|
|
18
18
|
action: "delete",
|
|
19
|
-
|
|
20
|
-
name,
|
|
19
|
+
credentialName: name,
|
|
21
20
|
success: false,
|
|
22
21
|
errorMessage: "Access denied",
|
|
23
22
|
});
|
|
24
23
|
response.status(403).json({ error: "Access denied" });
|
|
25
24
|
return;
|
|
26
25
|
}
|
|
27
|
-
const deleted = deleteCredential(database,
|
|
26
|
+
const deleted = deleteCredential(database, name);
|
|
28
27
|
if (!deleted) {
|
|
29
28
|
logAccess(database, {
|
|
30
29
|
apiKeyId: apiKey.id,
|
|
31
30
|
action: "delete",
|
|
32
|
-
|
|
33
|
-
name,
|
|
31
|
+
credentialName: name,
|
|
34
32
|
success: false,
|
|
35
33
|
errorMessage: "Not found",
|
|
36
34
|
});
|
|
@@ -40,11 +38,10 @@ function createDeleteCredentialHandler(database) {
|
|
|
40
38
|
logAccess(database, {
|
|
41
39
|
apiKeyId: apiKey.id,
|
|
42
40
|
action: "delete",
|
|
43
|
-
|
|
44
|
-
name,
|
|
41
|
+
credentialName: name,
|
|
45
42
|
success: true,
|
|
46
43
|
});
|
|
47
|
-
response.
|
|
44
|
+
response.status(204).end();
|
|
48
45
|
};
|
|
49
46
|
}
|
|
50
47
|
export { createDeleteCredentialHandler };
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* GET /api/v1/credentials/:
|
|
2
|
+
* GET /api/v1/credentials/:name handler.
|
|
3
|
+
*
|
|
4
|
+
* Fully opaque: The stored blob is decrypted and returned as-is. axvault does
|
|
5
|
+
* not inspect the blob's structure - all introspection (refresh checks, expiry
|
|
6
|
+
* detection) is delegated to axauth functions that accept opaque blobs.
|
|
3
7
|
*/
|
|
4
8
|
import type { Request, Response } from "express";
|
|
5
9
|
import type Database from "better-sqlite3";
|
|
@@ -9,12 +13,11 @@ interface GetCredentialConfig {
|
|
|
9
13
|
refreshTimeoutMs: number;
|
|
10
14
|
}
|
|
11
15
|
/**
|
|
12
|
-
* Retrieve a credential by
|
|
16
|
+
* Retrieve a credential by name.
|
|
13
17
|
*
|
|
14
18
|
* Automatically refreshes credentials that are near expiration.
|
|
15
19
|
*/
|
|
16
20
|
declare function createGetCredentialHandler(database: Database.Database, config: GetCredentialConfig): (request: Request<{
|
|
17
|
-
agent: string;
|
|
18
21
|
name: string;
|
|
19
22
|
}>, response: Response) => Promise<void>;
|
|
20
23
|
export { createGetCredentialHandler };
|
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* GET /api/v1/credentials/:
|
|
2
|
+
* GET /api/v1/credentials/:name handler.
|
|
3
|
+
*
|
|
4
|
+
* Fully opaque: The stored blob is decrypted and returned as-is. axvault does
|
|
5
|
+
* not inspect the blob's structure - all introspection (refresh checks, expiry
|
|
6
|
+
* detection) is delegated to axauth functions that accept opaque blobs.
|
|
3
7
|
*/
|
|
4
|
-
import { isRefreshableCredentialType } from "axshared";
|
|
5
8
|
import { hasReadAccess } from "../db/repositories/api-keys.js";
|
|
6
9
|
import { getCredential } from "../db/repositories/credentials.js";
|
|
7
10
|
import { logAccess } from "../db/repositories/audit-log.js";
|
|
8
11
|
import { decryptCredential } from "../lib/encryption.js";
|
|
9
|
-
import {
|
|
12
|
+
import { refreshCredentialOnRead } from "./refresh-credential-on-read.js";
|
|
10
13
|
/**
|
|
11
|
-
* Retrieve a credential by
|
|
14
|
+
* Retrieve a credential by name.
|
|
12
15
|
*
|
|
13
16
|
* Automatically refreshes credentials that are near expiration.
|
|
14
17
|
*/
|
|
15
18
|
function createGetCredentialHandler(database, config) {
|
|
16
19
|
return async (request, response) => {
|
|
17
20
|
const authenticatedRequest = request;
|
|
18
|
-
const {
|
|
21
|
+
const { name } = request.params;
|
|
19
22
|
const { apiKey } = authenticatedRequest;
|
|
20
23
|
// Access control check
|
|
21
|
-
if (!hasReadAccess(apiKey,
|
|
24
|
+
if (!hasReadAccess(apiKey, name)) {
|
|
22
25
|
logAccess(database, {
|
|
23
26
|
apiKeyId: apiKey.id,
|
|
24
27
|
action: "read",
|
|
25
|
-
|
|
26
|
-
name,
|
|
28
|
+
credentialName: name,
|
|
27
29
|
success: false,
|
|
28
30
|
errorMessage: "Access denied",
|
|
29
31
|
});
|
|
@@ -31,98 +33,57 @@ function createGetCredentialHandler(database, config) {
|
|
|
31
33
|
return;
|
|
32
34
|
}
|
|
33
35
|
// Fetch credential from database
|
|
34
|
-
const credential = getCredential(database,
|
|
36
|
+
const credential = getCredential(database, name);
|
|
35
37
|
if (!credential) {
|
|
36
38
|
logAccess(database, {
|
|
37
39
|
apiKeyId: apiKey.id,
|
|
38
40
|
action: "read",
|
|
39
|
-
|
|
40
|
-
name,
|
|
41
|
+
credentialName: name,
|
|
41
42
|
success: false,
|
|
42
43
|
errorMessage: "Not found",
|
|
43
44
|
});
|
|
44
45
|
response.status(404).json({ error: "Credential not found" });
|
|
45
46
|
return;
|
|
46
47
|
}
|
|
47
|
-
// Decrypt credential
|
|
48
|
-
let
|
|
48
|
+
// Decrypt credential blob (opaque - we don't inspect its structure)
|
|
49
|
+
let blob;
|
|
49
50
|
try {
|
|
50
|
-
|
|
51
|
+
blob = decryptCredential(credential);
|
|
51
52
|
}
|
|
52
53
|
catch (error) {
|
|
53
54
|
const message = error instanceof Error ? error.message : String(error);
|
|
54
55
|
logAccess(database, {
|
|
55
56
|
apiKeyId: apiKey.id,
|
|
56
57
|
action: "read",
|
|
57
|
-
|
|
58
|
-
name,
|
|
58
|
+
credentialName: name,
|
|
59
59
|
success: false,
|
|
60
60
|
errorMessage: `Decryption failed: ${message}`,
|
|
61
61
|
});
|
|
62
62
|
response.status(500).json({ error: "Failed to decrypt credential" });
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const refreshResult = await refreshWithMutex(database, agent, name, credential.type, credential.provider, data, apiKey.id, credential.updatedAt, { timeoutMs: config.refreshTimeoutMs });
|
|
78
|
-
if (refreshResult.ok) {
|
|
79
|
-
finalData = refreshResult.data;
|
|
80
|
-
finalExpiresAt = refreshResult.expiresAt;
|
|
81
|
-
finalUpdatedAt = refreshResult.updatedAt;
|
|
82
|
-
wasRefreshed = true;
|
|
83
|
-
}
|
|
84
|
-
else {
|
|
85
|
-
// Refresh failed - re-fetch to check if credential was deleted/modified
|
|
86
|
-
const currentCredential = getCredential(database, agent, name);
|
|
87
|
-
if (!currentCredential) {
|
|
88
|
-
// Credential was deleted during refresh - don't leak old data
|
|
89
|
-
logAccess(database, {
|
|
90
|
-
apiKeyId: apiKey.id,
|
|
91
|
-
action: "read",
|
|
92
|
-
agent,
|
|
93
|
-
name,
|
|
94
|
-
success: false,
|
|
95
|
-
errorMessage: "Credential deleted during refresh",
|
|
96
|
-
});
|
|
97
|
-
response.status(404).json({ error: "Credential not found" });
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
if (currentCredential.updatedAt.getTime() !==
|
|
101
|
-
credential.updatedAt.getTime()) {
|
|
102
|
-
// Credential was modified during refresh - return fresh data
|
|
103
|
-
const freshData = decryptCredential(currentCredential);
|
|
104
|
-
finalData = freshData;
|
|
105
|
-
finalExpiresAt = currentCredential.expiresAt;
|
|
106
|
-
finalUpdatedAt = currentCredential.updatedAt;
|
|
107
|
-
}
|
|
108
|
-
// Otherwise credential unchanged, return original with warning
|
|
109
|
-
refreshFailed = true;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
catch (error) {
|
|
113
|
-
// Programming error safety net - refreshWithMutex handles all expected
|
|
114
|
-
// errors internally and returns { ok: false }. This catch is for bugs
|
|
115
|
-
// in our code, not provider errors. Only log message, not stack/object.
|
|
116
|
-
console.error(`Unexpected refresh error for ${agent}/${name}:`, error instanceof Error ? error.message : error);
|
|
117
|
-
refreshFailed = true;
|
|
118
|
-
}
|
|
65
|
+
const refreshResult = await refreshCredentialOnRead({
|
|
66
|
+
database,
|
|
67
|
+
apiKeyId: apiKey.id,
|
|
68
|
+
name,
|
|
69
|
+
blob,
|
|
70
|
+
expectedUpdatedAt: credential.updatedAt,
|
|
71
|
+
refreshThresholdSeconds: config.refreshThresholdSeconds,
|
|
72
|
+
refreshTimeoutMs: config.refreshTimeoutMs,
|
|
73
|
+
});
|
|
74
|
+
if (refreshResult.status === "not-found") {
|
|
75
|
+
response.status(404).json({ error: "Credential not found" });
|
|
76
|
+
return;
|
|
119
77
|
}
|
|
78
|
+
const finalBlob = refreshResult.blob;
|
|
79
|
+
const finalUpdatedAt = refreshResult.updatedAt;
|
|
80
|
+
const wasRefreshed = refreshResult.wasRefreshed;
|
|
81
|
+
const refreshFailed = refreshResult.refreshFailed;
|
|
120
82
|
// Log successful read
|
|
121
83
|
logAccess(database, {
|
|
122
84
|
apiKeyId: apiKey.id,
|
|
123
85
|
action: "read",
|
|
124
|
-
|
|
125
|
-
name,
|
|
86
|
+
credentialName: name,
|
|
126
87
|
success: true,
|
|
127
88
|
});
|
|
128
89
|
// Set refresh header if token was refreshed
|
|
@@ -130,17 +91,13 @@ function createGetCredentialHandler(database, config) {
|
|
|
130
91
|
response.setHeader("X-Axvault-Refreshed", "true");
|
|
131
92
|
}
|
|
132
93
|
// Set warning header if refresh failed (still return 200 with stale data)
|
|
133
|
-
// Error details are logged to audit log; header is boolean to avoid invalid chars
|
|
134
94
|
if (refreshFailed) {
|
|
135
95
|
response.setHeader("X-Axvault-Refresh-Failed", "true");
|
|
136
96
|
}
|
|
97
|
+
// Return the blob as-is (opaque to axvault)
|
|
137
98
|
response.json({
|
|
138
|
-
agent,
|
|
139
99
|
name,
|
|
140
|
-
|
|
141
|
-
...(credential.provider && { provider: credential.provider }),
|
|
142
|
-
data: finalData,
|
|
143
|
-
expiresAt: finalExpiresAt?.toISOString(),
|
|
100
|
+
credential: finalBlob,
|
|
144
101
|
updatedAt: finalUpdatedAt.toISOString(),
|
|
145
102
|
});
|
|
146
103
|
};
|
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* GET /api/v1/credentials handler.
|
|
3
|
+
*
|
|
4
|
+
* Lists all credentials accessible by the API key (metadata only).
|
|
5
|
+
* With opaque schema, only name and timestamps are available without decrypting.
|
|
6
|
+
*
|
|
7
|
+
* Supports opt-in cursor-based pagination via `limit` and `cursor` query
|
|
8
|
+
* parameters. Without `limit`, all accessible credentials are returned
|
|
9
|
+
* (backward-compatible with pre-pagination behavior).
|
|
3
10
|
*/
|
|
4
11
|
import type { RequestHandler } from "express";
|
|
5
12
|
import type Database from "better-sqlite3";
|
|
6
|
-
/** Handler type for list route
|
|
7
|
-
type ListHandler = RequestHandler<Record<string, never>, unknown, unknown,
|
|
13
|
+
/** Handler type for list route with pagination query params */
|
|
14
|
+
type ListHandler = RequestHandler<Record<string, never>, unknown, unknown, {
|
|
15
|
+
limit?: string;
|
|
16
|
+
cursor?: string;
|
|
17
|
+
}>;
|
|
8
18
|
/**
|
|
9
|
-
* List
|
|
19
|
+
* List credentials accessible by the API key (metadata only).
|
|
20
|
+
*
|
|
21
|
+
* Query parameters:
|
|
22
|
+
* - `limit` — Results per page (1–1000). Omit for all results.
|
|
23
|
+
* - `cursor` — Name of the last credential from the previous page (requires `limit`)
|
|
24
|
+
*
|
|
25
|
+
* Returns `nextCursor` when more results are available.
|
|
10
26
|
*/
|
|
11
27
|
declare function createListCredentialsHandler(database: Database.Database): ListHandler;
|
|
12
28
|
export { createListCredentialsHandler };
|
|
@@ -1,30 +1,105 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* GET /api/v1/credentials handler.
|
|
3
|
+
*
|
|
4
|
+
* Lists all credentials accessible by the API key (metadata only).
|
|
5
|
+
* With opaque schema, only name and timestamps are available without decrypting.
|
|
6
|
+
*
|
|
7
|
+
* Supports opt-in cursor-based pagination via `limit` and `cursor` query
|
|
8
|
+
* parameters. Without `limit`, all accessible credentials are returned
|
|
9
|
+
* (backward-compatible with pre-pagination behavior).
|
|
3
10
|
*/
|
|
4
|
-
import { listCredentialsForApiKey } from "../db/repositories/credentials.js";
|
|
11
|
+
import { listCredentialsForApiKey, listCredentialsPaginated, } from "../db/repositories/credentials.js";
|
|
5
12
|
import { logAccess } from "../db/repositories/audit-log.js";
|
|
13
|
+
/** Maximum page size when limit is specified */
|
|
14
|
+
const MAX_LIMIT = 1000;
|
|
6
15
|
/**
|
|
7
|
-
* List
|
|
16
|
+
* List credentials accessible by the API key (metadata only).
|
|
17
|
+
*
|
|
18
|
+
* Query parameters:
|
|
19
|
+
* - `limit` — Results per page (1–1000). Omit for all results.
|
|
20
|
+
* - `cursor` — Name of the last credential from the previous page (requires `limit`)
|
|
21
|
+
*
|
|
22
|
+
* Returns `nextCursor` when more results are available.
|
|
8
23
|
*/
|
|
9
24
|
function createListCredentialsHandler(database) {
|
|
10
25
|
return (request, response) => {
|
|
11
26
|
const authenticatedRequest = request;
|
|
12
27
|
const { apiKey } = authenticatedRequest;
|
|
13
|
-
const
|
|
28
|
+
const rawLimit = request.query.limit;
|
|
29
|
+
const rawCursor = request.query.cursor;
|
|
30
|
+
// Validate query param types (repeated params produce arrays)
|
|
31
|
+
if ((rawLimit !== undefined && typeof rawLimit !== "string") ||
|
|
32
|
+
(rawCursor !== undefined && typeof rawCursor !== "string")) {
|
|
33
|
+
logAccess(database, {
|
|
34
|
+
apiKeyId: apiKey.id,
|
|
35
|
+
action: "list",
|
|
36
|
+
success: false,
|
|
37
|
+
errorMessage: "Repeated query parameters",
|
|
38
|
+
});
|
|
39
|
+
response
|
|
40
|
+
.status(400)
|
|
41
|
+
.json({ error: "Repeated query parameters are not supported" });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// cursor without limit is invalid — pagination requires an explicit page size
|
|
45
|
+
if (rawCursor && rawLimit === undefined) {
|
|
46
|
+
logAccess(database, {
|
|
47
|
+
apiKeyId: apiKey.id,
|
|
48
|
+
action: "list",
|
|
49
|
+
success: false,
|
|
50
|
+
errorMessage: "cursor requires limit",
|
|
51
|
+
});
|
|
52
|
+
response.status(400).json({ error: "cursor requires limit" });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// No limit specified: return all accessible credentials (backward-compatible)
|
|
56
|
+
if (rawLimit === undefined) {
|
|
57
|
+
const credentials = listCredentialsForApiKey(database, apiKey.readAccess);
|
|
58
|
+
logAccess(database, {
|
|
59
|
+
apiKeyId: apiKey.id,
|
|
60
|
+
action: "list",
|
|
61
|
+
success: true,
|
|
62
|
+
});
|
|
63
|
+
response.json({
|
|
64
|
+
credentials: credentials.map((cred) => ({
|
|
65
|
+
name: cred.name,
|
|
66
|
+
createdAt: cred.createdAt.toISOString(),
|
|
67
|
+
updatedAt: cred.updatedAt.toISOString(),
|
|
68
|
+
})),
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Paginated path (rawLimit is guaranteed to be defined here)
|
|
73
|
+
const parsedLimit = Number(rawLimit);
|
|
74
|
+
if (!Number.isInteger(parsedLimit) || parsedLimit < 1) {
|
|
75
|
+
logAccess(database, {
|
|
76
|
+
apiKeyId: apiKey.id,
|
|
77
|
+
action: "list",
|
|
78
|
+
success: false,
|
|
79
|
+
errorMessage: "Invalid limit parameter",
|
|
80
|
+
});
|
|
81
|
+
response.status(400).json({ error: "limit must be a positive integer" });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const limit = Math.min(parsedLimit, MAX_LIMIT);
|
|
85
|
+
const result = listCredentialsPaginated(database, apiKey.readAccess, {
|
|
86
|
+
cursor: rawCursor || undefined,
|
|
87
|
+
limit,
|
|
88
|
+
});
|
|
14
89
|
logAccess(database, {
|
|
15
90
|
apiKeyId: apiKey.id,
|
|
16
91
|
action: "list",
|
|
17
92
|
success: true,
|
|
18
93
|
});
|
|
19
94
|
response.json({
|
|
20
|
-
credentials: credentials.map((cred) => ({
|
|
21
|
-
agent: cred.agent,
|
|
95
|
+
credentials: result.credentials.map((cred) => ({
|
|
22
96
|
name: cred.name,
|
|
23
|
-
|
|
24
|
-
...(cred.provider && { provider: cred.provider }),
|
|
25
|
-
expiresAt: cred.expiresAt?.toISOString(),
|
|
97
|
+
createdAt: cred.createdAt.toISOString(),
|
|
26
98
|
updatedAt: cred.updatedAt.toISOString(),
|
|
27
99
|
})),
|
|
100
|
+
...(result.nextCursor !== undefined && {
|
|
101
|
+
nextCursor: result.nextCursor,
|
|
102
|
+
}),
|
|
28
103
|
});
|
|
29
104
|
};
|
|
30
105
|
}
|
|
@@ -1,15 +1,22 @@
|
|
|
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
8
|
import type { RequestHandler } from "express";
|
|
5
9
|
import type Database from "better-sqlite3";
|
|
6
|
-
/** Handler type for credential routes with
|
|
10
|
+
/** Handler type for credential routes with name param */
|
|
7
11
|
type CredentialHandler = RequestHandler<{
|
|
8
|
-
agent: string;
|
|
9
12
|
name: string;
|
|
10
13
|
}, unknown, unknown, unknown>;
|
|
11
14
|
/**
|
|
12
15
|
* Store or update a credential.
|
|
16
|
+
*
|
|
17
|
+
* Request body is treated as an opaque blob. axvault does not validate its
|
|
18
|
+
* structure - the client (via axauth) is responsible for providing a valid
|
|
19
|
+
* Credentials object. The blob is encrypted and stored as-is.
|
|
13
20
|
*/
|
|
14
21
|
declare function createPutCredentialHandler(database: Database.Database): CredentialHandler;
|
|
15
22
|
export { createPutCredentialHandler };
|