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
package/dist/db/migrations.js
CHANGED
|
@@ -1,32 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Database schema migrations.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Simple schema with opaque credential storage.
|
|
5
|
+
* Credentials are identified by name only - the blob contains all metadata.
|
|
5
6
|
*/
|
|
6
|
-
const CURRENT_VERSION =
|
|
7
|
+
const CURRENT_VERSION = 1;
|
|
7
8
|
/** Run all pending migrations */
|
|
8
9
|
function runMigrations(database) {
|
|
9
|
-
|
|
10
|
-
if (version
|
|
11
|
-
|
|
10
|
+
let version = getSchemaVersion(database);
|
|
11
|
+
if (version > CURRENT_VERSION) {
|
|
12
|
+
throw new Error(`Unsupported database schema version v${version} (expected v${CURRENT_VERSION}). Delete the database file to reinitialize.`);
|
|
12
13
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
migrateToV4(database);
|
|
21
|
-
}
|
|
22
|
-
if (version < 5) {
|
|
23
|
-
migrateToV5(database);
|
|
24
|
-
}
|
|
25
|
-
if (version < 6) {
|
|
26
|
-
migrateToV6(database);
|
|
27
|
-
}
|
|
28
|
-
if (version < 7) {
|
|
29
|
-
migrateToV7(database);
|
|
14
|
+
while (version < CURRENT_VERSION) {
|
|
15
|
+
if (version === 0) {
|
|
16
|
+
migrateToV1(database);
|
|
17
|
+
version = 1;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`Unsupported database schema version v${version} (expected v${CURRENT_VERSION}). Delete the database file to reinitialize.`);
|
|
30
21
|
}
|
|
31
22
|
}
|
|
32
23
|
/** Get current schema version */
|
|
@@ -41,7 +32,10 @@ function setSchemaVersion(database, version) {
|
|
|
41
32
|
/**
|
|
42
33
|
* Migration to version 1: Initial schema
|
|
43
34
|
*
|
|
44
|
-
*
|
|
35
|
+
* Simple opaque credential storage:
|
|
36
|
+
* - Credentials identified by name only
|
|
37
|
+
* - All credential metadata (agent, type, provider, data) in encrypted blob
|
|
38
|
+
* - API keys with name-based access patterns
|
|
45
39
|
*/
|
|
46
40
|
function migrateToV1(database) {
|
|
47
41
|
database.transaction(() => {
|
|
@@ -51,8 +45,10 @@ function migrateToV1(database) {
|
|
|
51
45
|
id TEXT PRIMARY KEY,
|
|
52
46
|
name TEXT NOT NULL,
|
|
53
47
|
key_hash TEXT NOT NULL UNIQUE,
|
|
48
|
+
key_prefix TEXT,
|
|
54
49
|
read_access TEXT NOT NULL,
|
|
55
50
|
write_access TEXT NOT NULL,
|
|
51
|
+
grant_access TEXT NOT NULL DEFAULT '[]',
|
|
56
52
|
created_at INTEGER NOT NULL,
|
|
57
53
|
last_used_at INTEGER
|
|
58
54
|
)
|
|
@@ -61,29 +57,26 @@ function migrateToV1(database) {
|
|
|
61
57
|
database.exec(`
|
|
62
58
|
CREATE INDEX idx_api_keys_key_hash ON api_keys(key_hash)
|
|
63
59
|
`);
|
|
64
|
-
// Encrypted credentials
|
|
60
|
+
// Encrypted credentials (opaque blob storage)
|
|
65
61
|
database.exec(`
|
|
66
62
|
CREATE TABLE credentials (
|
|
67
|
-
|
|
68
|
-
name TEXT NOT NULL,
|
|
63
|
+
name TEXT PRIMARY KEY,
|
|
69
64
|
encrypted_data BLOB NOT NULL,
|
|
65
|
+
salt BLOB NOT NULL,
|
|
70
66
|
iv BLOB NOT NULL,
|
|
71
67
|
auth_tag BLOB NOT NULL,
|
|
72
68
|
created_at INTEGER NOT NULL,
|
|
73
|
-
updated_at INTEGER NOT NULL
|
|
74
|
-
expires_at INTEGER,
|
|
75
|
-
PRIMARY KEY (agent, name)
|
|
69
|
+
updated_at INTEGER NOT NULL
|
|
76
70
|
)
|
|
77
71
|
`);
|
|
78
|
-
// Audit log
|
|
72
|
+
// Audit log
|
|
79
73
|
database.exec(`
|
|
80
74
|
CREATE TABLE audit_log (
|
|
81
75
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
82
76
|
timestamp INTEGER NOT NULL,
|
|
83
77
|
api_key_id TEXT,
|
|
84
78
|
action TEXT NOT NULL,
|
|
85
|
-
|
|
86
|
-
name TEXT,
|
|
79
|
+
credential_name TEXT,
|
|
87
80
|
success INTEGER NOT NULL,
|
|
88
81
|
error_message TEXT
|
|
89
82
|
)
|
|
@@ -95,92 +88,4 @@ function migrateToV1(database) {
|
|
|
95
88
|
setSchemaVersion(database, 1);
|
|
96
89
|
})();
|
|
97
90
|
}
|
|
98
|
-
/**
|
|
99
|
-
* Migration to version 2: Add salt column to credentials
|
|
100
|
-
*
|
|
101
|
-
* AES-256-GCM with PBKDF2 requires a salt for key derivation.
|
|
102
|
-
*/
|
|
103
|
-
function migrateToV2(database) {
|
|
104
|
-
database.transaction(() => {
|
|
105
|
-
database.exec(`
|
|
106
|
-
ALTER TABLE credentials ADD COLUMN salt BLOB
|
|
107
|
-
`);
|
|
108
|
-
setSchemaVersion(database, 2);
|
|
109
|
-
})();
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Migration to version 3: Add type column to credentials
|
|
113
|
-
*
|
|
114
|
-
* Explicit credential type ("oauth" or "api-key") instead of inferring from data.
|
|
115
|
-
* Existing credentials without type must be re-uploaded.
|
|
116
|
-
*/
|
|
117
|
-
function migrateToV3(database) {
|
|
118
|
-
database.transaction(() => {
|
|
119
|
-
database.exec(`
|
|
120
|
-
ALTER TABLE credentials ADD COLUMN type TEXT
|
|
121
|
-
`);
|
|
122
|
-
setSchemaVersion(database, 3);
|
|
123
|
-
})();
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Migration to version 4: Add grant_access column to api_keys
|
|
127
|
-
*
|
|
128
|
-
* The grant permission controls which credentials a key can delegate access to.
|
|
129
|
-
* Existing keys get an empty grant_access array (no delegation rights).
|
|
130
|
-
*/
|
|
131
|
-
function migrateToV4(database) {
|
|
132
|
-
database.transaction(() => {
|
|
133
|
-
database.exec(`
|
|
134
|
-
ALTER TABLE api_keys ADD COLUMN grant_access TEXT NOT NULL DEFAULT '[]'
|
|
135
|
-
`);
|
|
136
|
-
setSchemaVersion(database, 4);
|
|
137
|
-
})();
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Migration to version 5: Rename credential type "oauth" to "oauth-credentials"
|
|
141
|
-
*
|
|
142
|
-
* Distinguishes refreshable OAuth credentials (oauth-credentials) from
|
|
143
|
-
* long-lived OAuth tokens like CLAUDE_CODE_OAUTH_TOKEN (oauth-token).
|
|
144
|
-
*
|
|
145
|
-
* - `oauth-credentials`: Full OAuth flow with accessToken, refreshToken, expiresAt (refreshable)
|
|
146
|
-
* - `oauth-token`: Long-lived OAuth token for CI/CD (static, no refresh)
|
|
147
|
-
* - `api-key`: API key (static)
|
|
148
|
-
*/
|
|
149
|
-
function migrateToV5(database) {
|
|
150
|
-
database.transaction(() => {
|
|
151
|
-
database.exec(`
|
|
152
|
-
UPDATE credentials SET type = 'oauth-credentials' WHERE type = 'oauth'
|
|
153
|
-
`);
|
|
154
|
-
setSchemaVersion(database, 5);
|
|
155
|
-
})();
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Migration to version 6: Add key_prefix column to api_keys
|
|
159
|
-
*
|
|
160
|
-
* Stores first 8 characters of the key secret for identification.
|
|
161
|
-
* Existing keys will have NULL prefix (key value not recoverable).
|
|
162
|
-
*/
|
|
163
|
-
function migrateToV6(database) {
|
|
164
|
-
database.transaction(() => {
|
|
165
|
-
database.exec(`
|
|
166
|
-
ALTER TABLE api_keys ADD COLUMN key_prefix TEXT
|
|
167
|
-
`);
|
|
168
|
-
setSchemaVersion(database, 6);
|
|
169
|
-
})();
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* Migration to version 7: Add provider column to credentials
|
|
173
|
-
*
|
|
174
|
-
* Stores the provider name for multi-provider agents like OpenCode.
|
|
175
|
-
* For example, OpenCode credentials can have provider = "anthropic", "openai",
|
|
176
|
-
* "gemini", or "opencode" (the OpenCode Zen service).
|
|
177
|
-
*/
|
|
178
|
-
function migrateToV7(database) {
|
|
179
|
-
database.transaction(() => {
|
|
180
|
-
database.exec(`
|
|
181
|
-
ALTER TABLE credentials ADD COLUMN provider TEXT
|
|
182
|
-
`);
|
|
183
|
-
setSchemaVersion(database, 7);
|
|
184
|
-
})();
|
|
185
|
-
}
|
|
186
91
|
export { CURRENT_VERSION, getSchemaVersion, runMigrations };
|
|
@@ -18,9 +18,9 @@ interface AccessLists {
|
|
|
18
18
|
grantAccess: string[];
|
|
19
19
|
}
|
|
20
20
|
/** Check if API key has read access to a credential */
|
|
21
|
-
declare function hasReadAccess(apiKey: AccessLists,
|
|
21
|
+
declare function hasReadAccess(apiKey: AccessLists, name: string): boolean;
|
|
22
22
|
/** Check if API key has write access to a credential */
|
|
23
|
-
declare function hasWriteAccess(apiKey: AccessLists,
|
|
23
|
+
declare function hasWriteAccess(apiKey: AccessLists, name: string): boolean;
|
|
24
24
|
/** Check if API key has grant access to a credential */
|
|
25
|
-
declare function hasGrantAccess(apiKey: AccessLists,
|
|
25
|
+
declare function hasGrantAccess(apiKey: AccessLists, name: string): boolean;
|
|
26
26
|
export { extractKeyPrefix, generateKeyId, generateKeySecret, hashApiKey, hasGrantAccess, hasReadAccess, hasWriteAccess, };
|
|
@@ -23,21 +23,20 @@ function extractKeyPrefix(key) {
|
|
|
23
23
|
// Key format: axv_sk_ + 32 hex chars
|
|
24
24
|
return key.slice(0, 7 + KEY_PREFIX_LENGTH); // "axv_sk_" (7) + 8 chars
|
|
25
25
|
}
|
|
26
|
-
/** Check if access list includes the given credential
|
|
27
|
-
function hasAccess(accessList,
|
|
28
|
-
|
|
29
|
-
return accessList.includes("*") || accessList.includes(path);
|
|
26
|
+
/** Check if access list includes the given credential name */
|
|
27
|
+
function hasAccess(accessList, name) {
|
|
28
|
+
return accessList.includes("*") || accessList.includes(name);
|
|
30
29
|
}
|
|
31
30
|
/** Check if API key has read access to a credential */
|
|
32
|
-
function hasReadAccess(apiKey,
|
|
33
|
-
return hasAccess(apiKey.readAccess,
|
|
31
|
+
function hasReadAccess(apiKey, name) {
|
|
32
|
+
return hasAccess(apiKey.readAccess, name);
|
|
34
33
|
}
|
|
35
34
|
/** Check if API key has write access to a credential */
|
|
36
|
-
function hasWriteAccess(apiKey,
|
|
37
|
-
return hasAccess(apiKey.writeAccess,
|
|
35
|
+
function hasWriteAccess(apiKey, name) {
|
|
36
|
+
return hasAccess(apiKey.writeAccess, name);
|
|
38
37
|
}
|
|
39
38
|
/** Check if API key has grant access to a credential */
|
|
40
|
-
function hasGrantAccess(apiKey,
|
|
41
|
-
return hasAccess(apiKey.grantAccess,
|
|
39
|
+
function hasGrantAccess(apiKey, name) {
|
|
40
|
+
return hasAccess(apiKey.grantAccess, name);
|
|
42
41
|
}
|
|
43
42
|
export { extractKeyPrefix, generateKeyId, generateKeySecret, hashApiKey, hasGrantAccess, hasReadAccess, hasWriteAccess, };
|
|
@@ -10,8 +10,7 @@ interface AuditLogEntry {
|
|
|
10
10
|
timestamp: Date;
|
|
11
11
|
apiKeyId: string | undefined;
|
|
12
12
|
action: "auth" | "read" | "write" | "delete" | "refresh" | "list" | "grant";
|
|
13
|
-
|
|
14
|
-
name: string | undefined;
|
|
13
|
+
credentialName: string | undefined;
|
|
15
14
|
success: boolean;
|
|
16
15
|
errorMessage: string | undefined;
|
|
17
16
|
}
|
|
@@ -19,8 +18,7 @@ interface AuditLogEntry {
|
|
|
19
18
|
declare function logAccess(database: Database.Database, entry: {
|
|
20
19
|
apiKeyId?: string;
|
|
21
20
|
action: AuditLogEntry["action"];
|
|
22
|
-
|
|
23
|
-
name?: string;
|
|
21
|
+
credentialName?: string;
|
|
24
22
|
success: boolean;
|
|
25
23
|
errorMessage?: string;
|
|
26
24
|
}): void;
|
|
@@ -30,7 +28,7 @@ declare function getRecentLogs(database: Database.Database, options?: {
|
|
|
30
28
|
apiKeyId?: string;
|
|
31
29
|
}): AuditLogEntry[];
|
|
32
30
|
/** Get logs for a specific credential */
|
|
33
|
-
declare function getLogsForCredential(database: Database.Database,
|
|
31
|
+
declare function getLogsForCredential(database: Database.Database, credentialName: string, limit_?: number): AuditLogEntry[];
|
|
34
32
|
/** Prune old audit log entries */
|
|
35
33
|
declare function pruneOldLogs(database: Database.Database, olderThanDays: number): number;
|
|
36
34
|
export { getLogsForCredential, getRecentLogs, logAccess, pruneOldLogs };
|
|
@@ -10,19 +10,18 @@ function rowToEntry(row) {
|
|
|
10
10
|
timestamp: new Date(row.timestamp),
|
|
11
11
|
apiKeyId: row.api_key_id ?? undefined,
|
|
12
12
|
action: row.action,
|
|
13
|
-
|
|
14
|
-
name: row.name ?? undefined,
|
|
13
|
+
credentialName: row.credential_name ?? undefined,
|
|
15
14
|
success: row.success === 1,
|
|
16
15
|
errorMessage: row.error_message ?? undefined,
|
|
17
16
|
};
|
|
18
17
|
}
|
|
19
|
-
const SELECT_COLUMNS = `id, timestamp, api_key_id, action,
|
|
18
|
+
const SELECT_COLUMNS = `id, timestamp, api_key_id, action, credential_name, success, error_message`;
|
|
20
19
|
/** Log a credential access event */
|
|
21
20
|
function logAccess(database, entry) {
|
|
22
21
|
/* eslint-disable unicorn/no-null -- SQLite requires null for NULL values */
|
|
23
22
|
database
|
|
24
|
-
.prepare(`INSERT INTO audit_log (timestamp, api_key_id, action,
|
|
25
|
-
.run(Date.now(), entry.apiKeyId ?? null, entry.action, entry.
|
|
23
|
+
.prepare(`INSERT INTO audit_log (timestamp, api_key_id, action, credential_name, success, error_message) VALUES (?, ?, ?, ?, ?, ?)`)
|
|
24
|
+
.run(Date.now(), entry.apiKeyId ?? null, entry.action, entry.credentialName ?? null, entry.success ? 1 : 0, entry.errorMessage ?? null);
|
|
26
25
|
/* eslint-enable unicorn/no-null */
|
|
27
26
|
}
|
|
28
27
|
/** Get recent audit log entries */
|
|
@@ -39,11 +38,11 @@ function getRecentLogs(database, options) {
|
|
|
39
38
|
return database.prepare(query).all(...parameters).map((row) => rowToEntry(row));
|
|
40
39
|
}
|
|
41
40
|
/** Get logs for a specific credential */
|
|
42
|
-
function getLogsForCredential(database,
|
|
41
|
+
function getLogsForCredential(database, credentialName, limit_ = 50) {
|
|
43
42
|
const limit = Math.max(1, Math.min(limit_, 1000));
|
|
44
43
|
const rows = database
|
|
45
|
-
.prepare(`SELECT ${SELECT_COLUMNS} FROM audit_log WHERE
|
|
46
|
-
.all(
|
|
44
|
+
.prepare(`SELECT ${SELECT_COLUMNS} FROM audit_log WHERE credential_name = ? ORDER BY timestamp DESC LIMIT ?`)
|
|
45
|
+
.all(credentialName, limit);
|
|
47
46
|
return rows.map((row) => rowToEntry(row));
|
|
48
47
|
}
|
|
49
48
|
/** Prune old audit log entries */
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SQL queries for credentials repository.
|
|
3
|
+
*
|
|
4
|
+
* Name-only primary key, opaque blob storage.
|
|
3
5
|
*/
|
|
4
|
-
export declare const UPSERT_CREDENTIAL = "\n INSERT INTO credentials (
|
|
5
|
-
export declare const
|
|
6
|
-
export declare const
|
|
7
|
-
export declare const
|
|
8
|
-
export declare const
|
|
6
|
+
export declare const UPSERT_CREDENTIAL = "\n INSERT INTO credentials (name, encrypted_data, salt, iv, auth_tag, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(name) DO UPDATE SET\n encrypted_data = excluded.encrypted_data, salt = excluded.salt, iv = excluded.iv, auth_tag = excluded.auth_tag,\n updated_at = excluded.updated_at\n RETURNING created_at, updated_at";
|
|
7
|
+
export declare const UPDATE_CREDENTIAL_IF_UPDATED_AT_MATCHES = "\n UPDATE credentials\n SET encrypted_data = ?, salt = ?, iv = ?, auth_tag = ?, updated_at = ?\n WHERE name = ? AND updated_at = ?";
|
|
8
|
+
export declare const SELECT_CREDENTIAL = "\n SELECT name, encrypted_data, salt, iv, auth_tag, created_at, updated_at\n FROM credentials WHERE name = ?";
|
|
9
|
+
export declare const SELECT_ALL_METADATA = "\n SELECT name, created_at, updated_at\n FROM credentials ORDER BY name";
|
|
10
|
+
export declare const SELECT_METADATA_PAGINATED = "\n SELECT name, created_at, updated_at\n FROM credentials\n WHERE name > ?\n ORDER BY name\n LIMIT ?";
|
|
11
|
+
export declare const SELECT_METADATA_FIRST_PAGE = "\n SELECT name, created_at, updated_at\n FROM credentials\n ORDER BY name\n LIMIT ?";
|
|
12
|
+
export declare const DELETE_CREDENTIAL = "\n DELETE FROM credentials WHERE name = ?";
|
|
@@ -1,19 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SQL queries for credentials repository.
|
|
3
|
+
*
|
|
4
|
+
* Name-only primary key, opaque blob storage.
|
|
3
5
|
*/
|
|
4
6
|
export const UPSERT_CREDENTIAL = `
|
|
5
|
-
INSERT INTO credentials (
|
|
6
|
-
VALUES (?, ?, ?, ?, ?, ?,
|
|
7
|
-
ON CONFLICT(
|
|
8
|
-
|
|
9
|
-
updated_at = excluded.updated_at
|
|
7
|
+
INSERT INTO credentials (name, encrypted_data, salt, iv, auth_tag, created_at, updated_at)
|
|
8
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
9
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
10
|
+
encrypted_data = excluded.encrypted_data, salt = excluded.salt, iv = excluded.iv, auth_tag = excluded.auth_tag,
|
|
11
|
+
updated_at = excluded.updated_at
|
|
12
|
+
RETURNING created_at, updated_at`;
|
|
13
|
+
export const UPDATE_CREDENTIAL_IF_UPDATED_AT_MATCHES = `
|
|
14
|
+
UPDATE credentials
|
|
15
|
+
SET encrypted_data = ?, salt = ?, iv = ?, auth_tag = ?, updated_at = ?
|
|
16
|
+
WHERE name = ? AND updated_at = ?`;
|
|
10
17
|
export const SELECT_CREDENTIAL = `
|
|
11
|
-
SELECT
|
|
12
|
-
FROM credentials WHERE
|
|
18
|
+
SELECT name, encrypted_data, salt, iv, auth_tag, created_at, updated_at
|
|
19
|
+
FROM credentials WHERE name = ?`;
|
|
13
20
|
export const SELECT_ALL_METADATA = `
|
|
14
|
-
SELECT
|
|
15
|
-
FROM credentials ORDER BY
|
|
21
|
+
SELECT name, created_at, updated_at
|
|
22
|
+
FROM credentials ORDER BY name`;
|
|
23
|
+
export const SELECT_METADATA_PAGINATED = `
|
|
24
|
+
SELECT name, created_at, updated_at
|
|
25
|
+
FROM credentials
|
|
26
|
+
WHERE name > ?
|
|
27
|
+
ORDER BY name
|
|
28
|
+
LIMIT ?`;
|
|
29
|
+
export const SELECT_METADATA_FIRST_PAGE = `
|
|
30
|
+
SELECT name, created_at, updated_at
|
|
31
|
+
FROM credentials
|
|
32
|
+
ORDER BY name
|
|
33
|
+
LIMIT ?`;
|
|
16
34
|
export const DELETE_CREDENTIAL = `
|
|
17
|
-
DELETE FROM credentials WHERE
|
|
18
|
-
export const UPDATE_EXPIRES_AT = `
|
|
19
|
-
UPDATE credentials SET expires_at = ?, updated_at = ? WHERE agent = ? AND name = ?`;
|
|
35
|
+
DELETE FROM credentials WHERE name = ?`;
|
|
@@ -1,32 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Credentials repository.
|
|
3
3
|
*
|
|
4
|
-
* Manages encrypted credential storage.
|
|
4
|
+
* Manages encrypted credential storage with name-only keys.
|
|
5
|
+
* Credentials are fully opaque blobs.
|
|
5
6
|
*/
|
|
6
7
|
import type Database from "better-sqlite3";
|
|
7
|
-
import type { CredentialType } from "axshared";
|
|
8
8
|
import { type CredentialMetadata, type CredentialRecord } from "./parse-credential-row.js";
|
|
9
|
-
/**
|
|
9
|
+
/** Timestamps returned after storing a credential */
|
|
10
|
+
interface UpsertTimestamps {
|
|
11
|
+
createdAt: Date;
|
|
12
|
+
updatedAt: Date;
|
|
13
|
+
}
|
|
14
|
+
/** Store or update a credential, returning the resulting timestamps */
|
|
10
15
|
declare function upsertCredential(database: Database.Database, credential: {
|
|
11
|
-
agent: string;
|
|
12
16
|
name: string;
|
|
13
|
-
type: CredentialType;
|
|
14
|
-
provider?: string;
|
|
15
17
|
encryptedData: Buffer;
|
|
16
18
|
salt: Buffer;
|
|
17
19
|
iv: Buffer;
|
|
18
20
|
authTag: Buffer;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
}): UpsertTimestamps;
|
|
22
|
+
/**
|
|
23
|
+
* Update a credential only if its updated_at matches the expected value.
|
|
24
|
+
*
|
|
25
|
+
* Used to avoid overwriting concurrent PUT/DELETE changes when performing
|
|
26
|
+
* background refreshes during GET.
|
|
27
|
+
*/
|
|
28
|
+
declare function updateCredentialIfUnchanged(database: Database.Database, credential: {
|
|
29
|
+
name: string;
|
|
30
|
+
encryptedData: Buffer;
|
|
31
|
+
salt: Buffer;
|
|
32
|
+
iv: Buffer;
|
|
33
|
+
authTag: Buffer;
|
|
34
|
+
expectedUpdatedAt: Date;
|
|
35
|
+
}): {
|
|
36
|
+
updated: boolean;
|
|
37
|
+
updatedAt: Date;
|
|
38
|
+
};
|
|
39
|
+
/** Get a credential by name */
|
|
40
|
+
declare function getCredential(database: Database.Database, name: string): CredentialRecord | undefined;
|
|
23
41
|
/** List all credentials (metadata only) */
|
|
24
42
|
declare function listCredentials(database: Database.Database): CredentialMetadata[];
|
|
25
43
|
/** List credentials accessible by an API key's read access list */
|
|
26
44
|
declare function listCredentialsForApiKey(database: Database.Database, readAccess: string[]): CredentialMetadata[];
|
|
27
45
|
/** Delete a credential */
|
|
28
|
-
declare function deleteCredential(database: Database.Database,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
export { deleteCredential, getCredential, listCredentials, listCredentialsForApiKey, updateExpiresAt, upsertCredential, };
|
|
46
|
+
declare function deleteCredential(database: Database.Database, name: string): boolean;
|
|
47
|
+
export { deleteCredential, getCredential, listCredentials, listCredentialsForApiKey, updateCredentialIfUnchanged, upsertCredential, };
|
|
48
|
+
export { listCredentialsPaginated } from "./list-credentials-paginated.js";
|
|
32
49
|
export type { CredentialMetadata, CredentialRecord, } from "./parse-credential-row.js";
|
|
@@ -1,24 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Credentials repository.
|
|
3
3
|
*
|
|
4
|
-
* Manages encrypted credential storage.
|
|
4
|
+
* Manages encrypted credential storage with name-only keys.
|
|
5
|
+
* Credentials are fully opaque blobs.
|
|
5
6
|
*/
|
|
6
7
|
import * as SQL from "./credentials-queries.js";
|
|
7
8
|
import { rowToMetadata, rowToRecord, } from "./parse-credential-row.js";
|
|
8
|
-
/** Store or update a credential */
|
|
9
|
+
/** Store or update a credential, returning the resulting timestamps */
|
|
9
10
|
function upsertCredential(database, credential) {
|
|
10
11
|
const now = Date.now();
|
|
11
|
-
|
|
12
|
-
const expiresAt = credential.expiresAt?.getTime() ?? null;
|
|
13
|
-
// eslint-disable-next-line unicorn/no-null -- SQLite requires null for NULL values
|
|
14
|
-
const provider = credential.provider ?? null;
|
|
15
|
-
database
|
|
12
|
+
const row = database
|
|
16
13
|
.prepare(SQL.UPSERT_CREDENTIAL)
|
|
17
|
-
.
|
|
14
|
+
.get(credential.name, credential.encryptedData, credential.salt, credential.iv, credential.authTag, now, now);
|
|
15
|
+
return {
|
|
16
|
+
createdAt: new Date(row.created_at),
|
|
17
|
+
updatedAt: new Date(row.updated_at),
|
|
18
|
+
};
|
|
18
19
|
}
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Update a credential only if its updated_at matches the expected value.
|
|
22
|
+
*
|
|
23
|
+
* Used to avoid overwriting concurrent PUT/DELETE changes when performing
|
|
24
|
+
* background refreshes during GET.
|
|
25
|
+
*/
|
|
26
|
+
function updateCredentialIfUnchanged(database, credential) {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
const changes = database
|
|
29
|
+
.prepare(SQL.UPDATE_CREDENTIAL_IF_UPDATED_AT_MATCHES)
|
|
30
|
+
.run(credential.encryptedData, credential.salt, credential.iv, credential.authTag, now, credential.name, credential.expectedUpdatedAt.getTime()).changes;
|
|
31
|
+
return { updated: changes > 0, updatedAt: new Date(now) };
|
|
32
|
+
}
|
|
33
|
+
/** Get a credential by name */
|
|
34
|
+
function getCredential(database, name) {
|
|
35
|
+
const row = database.prepare(SQL.SELECT_CREDENTIAL).get(name);
|
|
22
36
|
return row ? rowToRecord(row) : undefined;
|
|
23
37
|
}
|
|
24
38
|
/** List all credentials (metadata only) */
|
|
@@ -30,16 +44,20 @@ function listCredentials(database) {
|
|
|
30
44
|
function listCredentialsForApiKey(database, readAccess) {
|
|
31
45
|
if (readAccess.includes("*"))
|
|
32
46
|
return listCredentials(database);
|
|
33
|
-
|
|
47
|
+
if (readAccess.length === 0)
|
|
48
|
+
return [];
|
|
49
|
+
const placeholders = readAccess.map(() => "?").join(", ");
|
|
50
|
+
const sql = `
|
|
51
|
+
SELECT name, created_at, updated_at
|
|
52
|
+
FROM credentials
|
|
53
|
+
WHERE name IN (${placeholders})
|
|
54
|
+
ORDER BY name`;
|
|
55
|
+
const rows = database.prepare(sql).all(...readAccess);
|
|
56
|
+
return rows.map((row) => rowToMetadata(row));
|
|
34
57
|
}
|
|
35
58
|
/** Delete a credential */
|
|
36
|
-
function deleteCredential(database,
|
|
37
|
-
return database.prepare(SQL.DELETE_CREDENTIAL).run(
|
|
38
|
-
}
|
|
39
|
-
/** Update expiration time after refresh */
|
|
40
|
-
function updateExpiresAt(database, agent, name, expiresAt) {
|
|
41
|
-
// eslint-disable-next-line unicorn/no-null -- SQLite requires null for NULL values
|
|
42
|
-
const expires = expiresAt?.getTime() ?? null;
|
|
43
|
-
database.prepare(SQL.UPDATE_EXPIRES_AT).run(expires, Date.now(), agent, name);
|
|
59
|
+
function deleteCredential(database, name) {
|
|
60
|
+
return database.prepare(SQL.DELETE_CREDENTIAL).run(name).changes > 0;
|
|
44
61
|
}
|
|
45
|
-
export { deleteCredential, getCredential, listCredentials, listCredentialsForApiKey,
|
|
62
|
+
export { deleteCredential, getCredential, listCredentials, listCredentialsForApiKey, updateCredentialIfUnchanged, upsertCredential, };
|
|
63
|
+
export { listCredentialsPaginated } from "./list-credentials-paginated.js";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paginated credential listing with SQL-level filtering.
|
|
3
|
+
*
|
|
4
|
+
* Both wildcard and specific access lists use SQL-level pagination,
|
|
5
|
+
* avoiding full table scans of the credentials table.
|
|
6
|
+
*/
|
|
7
|
+
import type Database from "better-sqlite3";
|
|
8
|
+
import { type CredentialMetadata } from "./parse-credential-row.js";
|
|
9
|
+
/** Pagination options for listing credentials */
|
|
10
|
+
interface PaginationOptions {
|
|
11
|
+
cursor: string | undefined;
|
|
12
|
+
limit: number;
|
|
13
|
+
}
|
|
14
|
+
/** Paginated result with optional next cursor */
|
|
15
|
+
interface PaginatedResult {
|
|
16
|
+
credentials: CredentialMetadata[];
|
|
17
|
+
nextCursor: string | undefined;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* List credentials with cursor-based pagination.
|
|
21
|
+
*
|
|
22
|
+
* Fetches limit+1 rows to detect whether more results exist.
|
|
23
|
+
* Both wildcard and specific access lists use SQL-level pagination.
|
|
24
|
+
*/
|
|
25
|
+
declare function listCredentialsPaginated(database: Database.Database, readAccess: string[], options: PaginationOptions): PaginatedResult;
|
|
26
|
+
export { listCredentialsPaginated };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paginated credential listing with SQL-level filtering.
|
|
3
|
+
*
|
|
4
|
+
* Both wildcard and specific access lists use SQL-level pagination,
|
|
5
|
+
* avoiding full table scans of the credentials table.
|
|
6
|
+
*/
|
|
7
|
+
import * as SQL from "./credentials-queries.js";
|
|
8
|
+
import { rowToMetadata, } from "./parse-credential-row.js";
|
|
9
|
+
/**
|
|
10
|
+
* Build a parameterized query for fetching metadata filtered by an IN clause.
|
|
11
|
+
*
|
|
12
|
+
* Generates `WHERE name IN (?, ?, ...)` with optional cursor and limit.
|
|
13
|
+
* All filtering and pagination happen in SQL — no full table scan needed.
|
|
14
|
+
*/
|
|
15
|
+
function buildFilteredQuery(names, cursor, limit) {
|
|
16
|
+
const placeholders = names.map(() => "?").join(", ");
|
|
17
|
+
const cursorClause = cursor ? " AND name > ?" : "";
|
|
18
|
+
const sql = `
|
|
19
|
+
SELECT name, created_at, updated_at
|
|
20
|
+
FROM credentials
|
|
21
|
+
WHERE name IN (${placeholders})${cursorClause}
|
|
22
|
+
ORDER BY name
|
|
23
|
+
LIMIT ?`;
|
|
24
|
+
const parameters = [...names];
|
|
25
|
+
if (cursor)
|
|
26
|
+
parameters.push(cursor);
|
|
27
|
+
parameters.push(limit);
|
|
28
|
+
return { sql, parameters };
|
|
29
|
+
}
|
|
30
|
+
/** Extract pagination result from a limit+1 fetch */
|
|
31
|
+
function paginateResults(rows, limit) {
|
|
32
|
+
if (rows.length > limit) {
|
|
33
|
+
const credentials = rows.slice(0, limit);
|
|
34
|
+
const lastCredential = credentials.at(-1);
|
|
35
|
+
return {
|
|
36
|
+
credentials,
|
|
37
|
+
nextCursor: lastCredential?.name,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return { credentials: rows, nextCursor: undefined };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* List credentials with cursor-based pagination.
|
|
44
|
+
*
|
|
45
|
+
* Fetches limit+1 rows to detect whether more results exist.
|
|
46
|
+
* Both wildcard and specific access lists use SQL-level pagination.
|
|
47
|
+
*/
|
|
48
|
+
function listCredentialsPaginated(database, readAccess, options) {
|
|
49
|
+
const fetchLimit = options.limit + 1;
|
|
50
|
+
if (readAccess.includes("*")) {
|
|
51
|
+
const rows = options.cursor
|
|
52
|
+
? database
|
|
53
|
+
.prepare(SQL.SELECT_METADATA_PAGINATED)
|
|
54
|
+
.all(options.cursor, fetchLimit)
|
|
55
|
+
: database
|
|
56
|
+
.prepare(SQL.SELECT_METADATA_FIRST_PAGE)
|
|
57
|
+
.all(fetchLimit);
|
|
58
|
+
const credentials = rows.map((row) => rowToMetadata(row));
|
|
59
|
+
return paginateResults(credentials, options.limit);
|
|
60
|
+
}
|
|
61
|
+
if (readAccess.length === 0) {
|
|
62
|
+
return { credentials: [], nextCursor: undefined };
|
|
63
|
+
}
|
|
64
|
+
const { sql, parameters } = buildFilteredQuery(readAccess, options.cursor, fetchLimit);
|
|
65
|
+
const rows = database.prepare(sql).all(...parameters);
|
|
66
|
+
const credentials = rows.map((row) => rowToMetadata(row));
|
|
67
|
+
return paginateResults(credentials, options.limit);
|
|
68
|
+
}
|
|
69
|
+
export { listCredentialsPaginated };
|
|
@@ -1,31 +1,24 @@
|
|
|
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
6
|
import type { CredentialRow, MetadataRow } from "../types.js";
|
|
6
7
|
/** Credential record stored in database */
|
|
7
8
|
interface CredentialRecord {
|
|
8
|
-
agent: string;
|
|
9
9
|
name: string;
|
|
10
|
-
type: CredentialType;
|
|
11
|
-
provider: string | undefined;
|
|
12
10
|
encryptedData: Buffer;
|
|
13
11
|
salt: Buffer;
|
|
14
12
|
iv: Buffer;
|
|
15
13
|
authTag: Buffer;
|
|
16
14
|
createdAt: Date;
|
|
17
15
|
updatedAt: Date;
|
|
18
|
-
expiresAt: Date | undefined;
|
|
19
16
|
}
|
|
20
17
|
/** Credential metadata (without encrypted data) */
|
|
21
18
|
interface CredentialMetadata {
|
|
22
|
-
agent: string;
|
|
23
19
|
name: string;
|
|
24
|
-
type: CredentialType;
|
|
25
|
-
provider: string | undefined;
|
|
26
20
|
createdAt: Date;
|
|
27
21
|
updatedAt: Date;
|
|
28
|
-
expiresAt: Date | undefined;
|
|
29
22
|
}
|
|
30
23
|
/** Convert database row to record */
|
|
31
24
|
declare function rowToRecord(row: CredentialRow): CredentialRecord;
|