axvault 1.2.0 → 1.4.0
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 +7 -3
- package/dist/commands/key-list.js +3 -1
- package/dist/db/migrations.d.ts +1 -1
- package/dist/db/migrations.js +39 -1
- package/dist/db/repositories/api-key-utilities.d.ts +26 -0
- package/dist/db/repositories/api-key-utilities.js +43 -0
- package/dist/db/repositories/api-keys.d.ts +3 -7
- package/dist/db/repositories/api-keys.js +12 -27
- package/dist/db/repositories/credentials-queries.d.ts +8 -0
- package/dist/db/repositories/credentials-queries.js +19 -0
- package/dist/db/repositories/credentials.d.ts +3 -25
- package/dist/db/repositories/credentials.js +12 -54
- package/dist/db/repositories/parse-credential-row.d.ts +33 -0
- package/dist/db/repositories/parse-credential-row.js +39 -0
- package/dist/db/types.d.ts +1 -0
- package/dist/handlers/get-credential.js +2 -1
- package/dist/handlers/put-credential.js +5 -3
- package/dist/lib/format.d.ts +1 -0
- package/dist/lib/format.js +11 -1
- package/dist/refresh/check-refresh.js +2 -2
- package/dist/refresh/refresh-manager.d.ts +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -192,13 +192,17 @@ curl -X PUT https://vault.example.com/api/v1/credentials/claude/prod \
|
|
|
192
192
|
-H "Authorization: Bearer <api_key>" \
|
|
193
193
|
-H "Content-Type: application/json" \
|
|
194
194
|
-d '{
|
|
195
|
-
"type": "oauth",
|
|
195
|
+
"type": "oauth-credentials",
|
|
196
196
|
"data": {"access_token": "...", "refresh_token": "..."},
|
|
197
197
|
"expiresAt": "2025-12-31T23:59:59Z"
|
|
198
198
|
}'
|
|
199
199
|
```
|
|
200
200
|
|
|
201
|
-
The `type` field is required and must be
|
|
201
|
+
The `type` field is required and must be one of:
|
|
202
|
+
|
|
203
|
+
- `"oauth-credentials"` — Full OAuth with refresh_token (eligible for auto-refresh)
|
|
204
|
+
- `"oauth-token"` — Long-lived OAuth token like `CLAUDE_CODE_OAUTH_TOKEN` (static)
|
|
205
|
+
- `"api-key"` — API key (static)
|
|
202
206
|
|
|
203
207
|
### Retrieve a Credential
|
|
204
208
|
|
|
@@ -216,7 +220,7 @@ curl -X DELETE https://vault.example.com/api/v1/credentials/claude/prod \
|
|
|
216
220
|
|
|
217
221
|
## Auto-Refresh
|
|
218
222
|
|
|
219
|
-
axvault automatically refreshes
|
|
223
|
+
axvault automatically refreshes `oauth-credentials` type credentials that are near expiration when they are retrieved. This behavior is controlled by the refresh threshold setting. Only credentials with a `refresh_token` in their data are eligible for auto-refresh.
|
|
220
224
|
|
|
221
225
|
### Access Control Note
|
|
222
226
|
|
|
@@ -16,6 +16,8 @@ export function handleKeyList(options) {
|
|
|
16
16
|
const output = keys.map((key) => ({
|
|
17
17
|
id: key.id,
|
|
18
18
|
name: key.name,
|
|
19
|
+
// eslint-disable-next-line unicorn/no-null -- JSON requires null for missing values
|
|
20
|
+
keyPrefix: key.keyPrefix ?? null,
|
|
19
21
|
readAccess: key.readAccess,
|
|
20
22
|
writeAccess: key.writeAccess,
|
|
21
23
|
grantAccess: key.grantAccess,
|
|
@@ -28,7 +30,7 @@ export function handleKeyList(options) {
|
|
|
28
30
|
console.error("No API keys found.\nCreate one with: axvault key create --name <name> [--read <access>] [--write <access>] [--grant <access>]");
|
|
29
31
|
}
|
|
30
32
|
else {
|
|
31
|
-
console.log("ID\tNAME\tREAD ACCESS\tWRITE ACCESS\tGRANT ACCESS\tLAST USED");
|
|
33
|
+
console.log("ID\tNAME\tKEY\tREAD ACCESS\tWRITE ACCESS\tGRANT ACCESS\tLAST USED");
|
|
32
34
|
for (const key of keys)
|
|
33
35
|
console.log(formatKeyRow(key));
|
|
34
36
|
}
|
package/dist/db/migrations.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Uses a simple version-based migration system.
|
|
5
5
|
*/
|
|
6
6
|
import type Database from "better-sqlite3";
|
|
7
|
-
declare const CURRENT_VERSION =
|
|
7
|
+
declare const CURRENT_VERSION = 6;
|
|
8
8
|
/** Run all pending migrations */
|
|
9
9
|
declare function runMigrations(database: Database.Database): void;
|
|
10
10
|
/** Get current schema version */
|
package/dist/db/migrations.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Uses a simple version-based migration system.
|
|
5
5
|
*/
|
|
6
|
-
const CURRENT_VERSION =
|
|
6
|
+
const CURRENT_VERSION = 6;
|
|
7
7
|
/** Run all pending migrations */
|
|
8
8
|
function runMigrations(database) {
|
|
9
9
|
const version = getSchemaVersion(database);
|
|
@@ -19,6 +19,12 @@ function runMigrations(database) {
|
|
|
19
19
|
if (version < 4) {
|
|
20
20
|
migrateToV4(database);
|
|
21
21
|
}
|
|
22
|
+
if (version < 5) {
|
|
23
|
+
migrateToV5(database);
|
|
24
|
+
}
|
|
25
|
+
if (version < 6) {
|
|
26
|
+
migrateToV6(database);
|
|
27
|
+
}
|
|
22
28
|
}
|
|
23
29
|
/** Get current schema version */
|
|
24
30
|
function getSchemaVersion(database) {
|
|
@@ -127,4 +133,36 @@ function migrateToV4(database) {
|
|
|
127
133
|
setSchemaVersion(database, 4);
|
|
128
134
|
})();
|
|
129
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Migration to version 5: Rename credential type "oauth" to "oauth-credentials"
|
|
138
|
+
*
|
|
139
|
+
* Distinguishes refreshable OAuth credentials (oauth-credentials) from
|
|
140
|
+
* long-lived OAuth tokens like CLAUDE_CODE_OAUTH_TOKEN (oauth-token).
|
|
141
|
+
*
|
|
142
|
+
* - `oauth-credentials`: Full OAuth flow with accessToken, refreshToken, expiresAt (refreshable)
|
|
143
|
+
* - `oauth-token`: Long-lived OAuth token for CI/CD (static, no refresh)
|
|
144
|
+
* - `api-key`: API key (static)
|
|
145
|
+
*/
|
|
146
|
+
function migrateToV5(database) {
|
|
147
|
+
database.transaction(() => {
|
|
148
|
+
database.exec(`
|
|
149
|
+
UPDATE credentials SET type = 'oauth-credentials' WHERE type = 'oauth'
|
|
150
|
+
`);
|
|
151
|
+
setSchemaVersion(database, 5);
|
|
152
|
+
})();
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Migration to version 6: Add key_prefix column to api_keys
|
|
156
|
+
*
|
|
157
|
+
* Stores first 8 characters of the key secret for identification.
|
|
158
|
+
* Existing keys will have NULL prefix (key value not recoverable).
|
|
159
|
+
*/
|
|
160
|
+
function migrateToV6(database) {
|
|
161
|
+
database.transaction(() => {
|
|
162
|
+
database.exec(`
|
|
163
|
+
ALTER TABLE api_keys ADD COLUMN key_prefix TEXT
|
|
164
|
+
`);
|
|
165
|
+
setSchemaVersion(database, 6);
|
|
166
|
+
})();
|
|
167
|
+
}
|
|
130
168
|
export { CURRENT_VERSION, getSchemaVersion, runMigrations };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API key utility functions.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions for key generation and hashing.
|
|
5
|
+
*/
|
|
6
|
+
/** Generate a new API key ID */
|
|
7
|
+
declare function generateKeyId(): string;
|
|
8
|
+
/** Generate a new API key secret */
|
|
9
|
+
declare function generateKeySecret(): string;
|
|
10
|
+
/** Hash an API key for storage */
|
|
11
|
+
declare function hashApiKey(key: string): string;
|
|
12
|
+
/** Extract key prefix for display (axv_sk_ + first 8 hex chars of secret) */
|
|
13
|
+
declare function extractKeyPrefix(key: string): string;
|
|
14
|
+
/** Minimal interface for access checking */
|
|
15
|
+
interface AccessLists {
|
|
16
|
+
readAccess: string[];
|
|
17
|
+
writeAccess: string[];
|
|
18
|
+
grantAccess: string[];
|
|
19
|
+
}
|
|
20
|
+
/** Check if API key has read access to a credential */
|
|
21
|
+
declare function hasReadAccess(apiKey: AccessLists, agent: string, name: string): boolean;
|
|
22
|
+
/** Check if API key has write access to a credential */
|
|
23
|
+
declare function hasWriteAccess(apiKey: AccessLists, agent: string, name: string): boolean;
|
|
24
|
+
/** Check if API key has grant access to a credential */
|
|
25
|
+
declare function hasGrantAccess(apiKey: AccessLists, agent: string, name: string): boolean;
|
|
26
|
+
export { extractKeyPrefix, generateKeyId, generateKeySecret, hashApiKey, hasGrantAccess, hasReadAccess, hasWriteAccess, };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API key utility functions.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions for key generation and hashing.
|
|
5
|
+
*/
|
|
6
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
7
|
+
/** Key prefix for identification (e.g., "axv_sk_01234567") */
|
|
8
|
+
const KEY_PREFIX_LENGTH = 8;
|
|
9
|
+
/** Generate a new API key ID */
|
|
10
|
+
function generateKeyId() {
|
|
11
|
+
return `k_${randomBytes(6).toString("hex")}`;
|
|
12
|
+
}
|
|
13
|
+
/** Generate a new API key secret */
|
|
14
|
+
function generateKeySecret() {
|
|
15
|
+
return `axv_sk_${randomBytes(16).toString("hex")}`;
|
|
16
|
+
}
|
|
17
|
+
/** Hash an API key for storage */
|
|
18
|
+
function hashApiKey(key) {
|
|
19
|
+
return createHash("sha256").update(key).digest("hex");
|
|
20
|
+
}
|
|
21
|
+
/** Extract key prefix for display (axv_sk_ + first 8 hex chars of secret) */
|
|
22
|
+
function extractKeyPrefix(key) {
|
|
23
|
+
// Key format: axv_sk_ + 32 hex chars
|
|
24
|
+
return key.slice(0, 7 + KEY_PREFIX_LENGTH); // "axv_sk_" (7) + 8 chars
|
|
25
|
+
}
|
|
26
|
+
/** Check if access list includes the given credential path */
|
|
27
|
+
function hasAccess(accessList, agent, name) {
|
|
28
|
+
const path = `${agent}/${name}`;
|
|
29
|
+
return accessList.includes("*") || accessList.includes(path);
|
|
30
|
+
}
|
|
31
|
+
/** Check if API key has read access to a credential */
|
|
32
|
+
function hasReadAccess(apiKey, agent, name) {
|
|
33
|
+
return hasAccess(apiKey.readAccess, agent, name);
|
|
34
|
+
}
|
|
35
|
+
/** Check if API key has write access to a credential */
|
|
36
|
+
function hasWriteAccess(apiKey, agent, name) {
|
|
37
|
+
return hasAccess(apiKey.writeAccess, agent, name);
|
|
38
|
+
}
|
|
39
|
+
/** Check if API key has grant access to a credential */
|
|
40
|
+
function hasGrantAccess(apiKey, agent, name) {
|
|
41
|
+
return hasAccess(apiKey.grantAccess, agent, name);
|
|
42
|
+
}
|
|
43
|
+
export { extractKeyPrefix, generateKeyId, generateKeySecret, hashApiKey, hasGrantAccess, hasReadAccess, hasWriteAccess, };
|
|
@@ -9,6 +9,7 @@ interface ApiKeyRecord {
|
|
|
9
9
|
id: string;
|
|
10
10
|
name: string;
|
|
11
11
|
keyHash: string;
|
|
12
|
+
keyPrefix: string | undefined;
|
|
12
13
|
readAccess: string[];
|
|
13
14
|
writeAccess: string[];
|
|
14
15
|
grantAccess: string[];
|
|
@@ -36,17 +37,12 @@ declare function listApiKeys(database: Database.Database): ApiKeyRecord[];
|
|
|
36
37
|
declare function updateLastUsed(database: Database.Database, id: string): void;
|
|
37
38
|
/** Delete an API key */
|
|
38
39
|
declare function deleteApiKey(database: Database.Database, id: string): boolean;
|
|
39
|
-
/** Check if API key has read access to a credential */
|
|
40
|
-
declare function hasReadAccess(apiKey: ApiKeyRecord, agent: string, name: string): boolean;
|
|
41
|
-
/** Check if API key has write access to a credential */
|
|
42
|
-
declare function hasWriteAccess(apiKey: ApiKeyRecord, agent: string, name: string): boolean;
|
|
43
|
-
/** Check if API key has grant access to a credential */
|
|
44
|
-
declare function hasGrantAccess(apiKey: ApiKeyRecord, agent: string, name: string): boolean;
|
|
45
40
|
/** Update an API key's access permissions */
|
|
46
41
|
declare function updateApiKeyAccess(database: Database.Database, id: string, options: {
|
|
47
42
|
readAccess?: string[];
|
|
48
43
|
writeAccess?: string[];
|
|
49
44
|
grantAccess?: string[];
|
|
50
45
|
}): boolean;
|
|
51
|
-
export { createApiKey, deleteApiKey, findApiKeyById, findApiKeyByKey,
|
|
46
|
+
export { createApiKey, deleteApiKey, findApiKeyById, findApiKeyByKey, listApiKeys, updateApiKeyAccess, updateLastUsed, };
|
|
47
|
+
export { hasGrantAccess, hasReadAccess, hasWriteAccess, } from "./api-key-utilities.js";
|
|
52
48
|
export type { ApiKeyRecord, ApiKeyWithSecret };
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Manages API keys with read/write access lists for credentials.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { extractKeyPrefix, generateKeyId, generateKeySecret, hashApiKey, } from "./api-key-utilities.js";
|
|
7
7
|
/** Convert database row to record */
|
|
8
8
|
function rowToRecord(row) {
|
|
9
9
|
return {
|
|
10
10
|
id: row.id,
|
|
11
11
|
name: row.name,
|
|
12
12
|
keyHash: row.key_hash,
|
|
13
|
+
keyPrefix: row.key_prefix ?? undefined,
|
|
13
14
|
readAccess: JSON.parse(row.read_access),
|
|
14
15
|
writeAccess: JSON.parse(row.write_access),
|
|
15
16
|
grantAccess: JSON.parse(row.grant_access),
|
|
@@ -17,26 +18,24 @@ function rowToRecord(row) {
|
|
|
17
18
|
lastUsedAt: row.last_used_at ? new Date(row.last_used_at) : undefined,
|
|
18
19
|
};
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
-
function hashApiKey(key) {
|
|
22
|
-
return createHash("sha256").update(key).digest("hex");
|
|
23
|
-
}
|
|
24
|
-
const SELECT_COLUMNS = `id, name, key_hash, read_access, write_access, grant_access, created_at, last_used_at`;
|
|
21
|
+
const SELECT_COLUMNS = `id, name, key_hash, key_prefix, read_access, write_access, grant_access, created_at, last_used_at`;
|
|
25
22
|
/** Create a new API key */
|
|
26
23
|
function createApiKey(database, options) {
|
|
27
|
-
const id =
|
|
28
|
-
const key =
|
|
24
|
+
const id = generateKeyId();
|
|
25
|
+
const key = generateKeySecret();
|
|
29
26
|
const keyHash = hashApiKey(key);
|
|
27
|
+
const keyPrefix = extractKeyPrefix(key);
|
|
30
28
|
const now = Date.now();
|
|
31
29
|
database
|
|
32
|
-
.prepare(`INSERT INTO api_keys (id, name, key_hash, read_access, write_access, grant_access, created_at)
|
|
33
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
34
|
-
.run(id, options.name, keyHash, JSON.stringify(options.readAccess), JSON.stringify(options.writeAccess), JSON.stringify(options.grantAccess), now);
|
|
30
|
+
.prepare(`INSERT INTO api_keys (id, name, key_hash, key_prefix, read_access, write_access, grant_access, created_at)
|
|
31
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
32
|
+
.run(id, options.name, keyHash, keyPrefix, JSON.stringify(options.readAccess), JSON.stringify(options.writeAccess), JSON.stringify(options.grantAccess), now);
|
|
35
33
|
return {
|
|
36
34
|
id,
|
|
37
35
|
name: options.name,
|
|
38
36
|
key,
|
|
39
37
|
keyHash,
|
|
38
|
+
keyPrefix,
|
|
40
39
|
readAccess: options.readAccess,
|
|
41
40
|
writeAccess: options.writeAccess,
|
|
42
41
|
grantAccess: options.grantAccess,
|
|
@@ -75,21 +74,6 @@ function updateLastUsed(database, id) {
|
|
|
75
74
|
function deleteApiKey(database, id) {
|
|
76
75
|
return (database.prepare(`DELETE FROM api_keys WHERE id = ?`).run(id).changes > 0);
|
|
77
76
|
}
|
|
78
|
-
/** Check if API key has read access to a credential */
|
|
79
|
-
function hasReadAccess(apiKey, agent, name) {
|
|
80
|
-
const path = `${agent}/${name}`;
|
|
81
|
-
return apiKey.readAccess.includes("*") || apiKey.readAccess.includes(path);
|
|
82
|
-
}
|
|
83
|
-
/** Check if API key has write access to a credential */
|
|
84
|
-
function hasWriteAccess(apiKey, agent, name) {
|
|
85
|
-
const path = `${agent}/${name}`;
|
|
86
|
-
return apiKey.writeAccess.includes("*") || apiKey.writeAccess.includes(path);
|
|
87
|
-
}
|
|
88
|
-
/** Check if API key has grant access to a credential */
|
|
89
|
-
function hasGrantAccess(apiKey, agent, name) {
|
|
90
|
-
const path = `${agent}/${name}`;
|
|
91
|
-
return apiKey.grantAccess.includes("*") || apiKey.grantAccess.includes(path);
|
|
92
|
-
}
|
|
93
77
|
/** Update an API key's access permissions */
|
|
94
78
|
function updateApiKeyAccess(database, id, options) {
|
|
95
79
|
const updates = [];
|
|
@@ -112,4 +96,5 @@ function updateApiKeyAccess(database, id, options) {
|
|
|
112
96
|
const sql = `UPDATE api_keys SET ${updates.join(", ")} WHERE id = ?`;
|
|
113
97
|
return database.prepare(sql).run(...values).changes > 0;
|
|
114
98
|
}
|
|
115
|
-
export { createApiKey, deleteApiKey, findApiKeyById, findApiKeyByKey,
|
|
99
|
+
export { createApiKey, deleteApiKey, findApiKeyById, findApiKeyByKey, listApiKeys, updateApiKeyAccess, updateLastUsed, };
|
|
100
|
+
export { hasGrantAccess, hasReadAccess, hasWriteAccess, } from "./api-key-utilities.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL queries for credentials repository.
|
|
3
|
+
*/
|
|
4
|
+
export declare const UPSERT_CREDENTIAL = "\n INSERT INTO credentials (agent, name, type, encrypted_data, salt, iv, auth_tag, created_at, updated_at, expires_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(agent, name) DO UPDATE SET\n type = excluded.type, encrypted_data = excluded.encrypted_data, salt = excluded.salt, iv = excluded.iv, auth_tag = excluded.auth_tag,\n updated_at = excluded.updated_at, expires_at = excluded.expires_at";
|
|
5
|
+
export declare const SELECT_CREDENTIAL = "\n SELECT agent, name, type, encrypted_data, salt, iv, auth_tag, created_at, updated_at, expires_at\n FROM credentials WHERE agent = ? AND name = ?";
|
|
6
|
+
export declare const SELECT_ALL_METADATA = "\n SELECT agent, name, type, created_at, updated_at, expires_at\n FROM credentials ORDER BY agent, name";
|
|
7
|
+
export declare const DELETE_CREDENTIAL = "\n DELETE FROM credentials WHERE agent = ? AND name = ?";
|
|
8
|
+
export declare const UPDATE_EXPIRES_AT = "\n UPDATE credentials SET expires_at = ?, updated_at = ? WHERE agent = ? AND name = ?";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL queries for credentials repository.
|
|
3
|
+
*/
|
|
4
|
+
export const UPSERT_CREDENTIAL = `
|
|
5
|
+
INSERT INTO credentials (agent, name, type, encrypted_data, salt, iv, auth_tag, created_at, updated_at, expires_at)
|
|
6
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
7
|
+
ON CONFLICT(agent, name) DO UPDATE SET
|
|
8
|
+
type = excluded.type, encrypted_data = excluded.encrypted_data, salt = excluded.salt, iv = excluded.iv, auth_tag = excluded.auth_tag,
|
|
9
|
+
updated_at = excluded.updated_at, expires_at = excluded.expires_at`;
|
|
10
|
+
export const SELECT_CREDENTIAL = `
|
|
11
|
+
SELECT agent, name, type, encrypted_data, salt, iv, auth_tag, created_at, updated_at, expires_at
|
|
12
|
+
FROM credentials WHERE agent = ? AND name = ?`;
|
|
13
|
+
export const SELECT_ALL_METADATA = `
|
|
14
|
+
SELECT agent, name, type, created_at, updated_at, expires_at
|
|
15
|
+
FROM credentials ORDER BY agent, name`;
|
|
16
|
+
export const DELETE_CREDENTIAL = `
|
|
17
|
+
DELETE FROM credentials WHERE agent = ? AND name = ?`;
|
|
18
|
+
export const UPDATE_EXPIRES_AT = `
|
|
19
|
+
UPDATE credentials SET expires_at = ?, updated_at = ? WHERE agent = ? AND name = ?`;
|
|
@@ -4,30 +4,8 @@
|
|
|
4
4
|
* Manages encrypted credential storage.
|
|
5
5
|
*/
|
|
6
6
|
import type Database from "better-sqlite3";
|
|
7
|
-
|
|
8
|
-
type
|
|
9
|
-
/** Credential record stored in database */
|
|
10
|
-
interface CredentialRecord {
|
|
11
|
-
agent: string;
|
|
12
|
-
name: string;
|
|
13
|
-
type: CredentialType;
|
|
14
|
-
encryptedData: Buffer;
|
|
15
|
-
salt: Buffer;
|
|
16
|
-
iv: Buffer;
|
|
17
|
-
authTag: Buffer;
|
|
18
|
-
createdAt: Date;
|
|
19
|
-
updatedAt: Date;
|
|
20
|
-
expiresAt: Date | undefined;
|
|
21
|
-
}
|
|
22
|
-
/** Credential metadata (without encrypted data) */
|
|
23
|
-
interface CredentialMetadata {
|
|
24
|
-
agent: string;
|
|
25
|
-
name: string;
|
|
26
|
-
type: CredentialType;
|
|
27
|
-
createdAt: Date;
|
|
28
|
-
updatedAt: Date;
|
|
29
|
-
expiresAt: Date | undefined;
|
|
30
|
-
}
|
|
7
|
+
import type { CredentialType } from "axshared";
|
|
8
|
+
import { type CredentialMetadata, type CredentialRecord } from "./parse-credential-row.js";
|
|
31
9
|
/** Store or update a credential */
|
|
32
10
|
declare function upsertCredential(database: Database.Database, credential: {
|
|
33
11
|
agent: string;
|
|
@@ -50,4 +28,4 @@ declare function deleteCredential(database: Database.Database, agent: string, na
|
|
|
50
28
|
/** Update expiration time after refresh */
|
|
51
29
|
declare function updateExpiresAt(database: Database.Database, agent: string, name: string, expiresAt: Date | undefined): void;
|
|
52
30
|
export { deleteCredential, getCredential, listCredentials, listCredentialsForApiKey, updateExpiresAt, upsertCredential, };
|
|
53
|
-
export type { CredentialMetadata, CredentialRecord,
|
|
31
|
+
export type { CredentialMetadata, CredentialRecord, } from "./parse-credential-row.js";
|
|
@@ -3,64 +3,25 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Manages encrypted credential storage.
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
if (type !== "oauth" && type !== "api-key") {
|
|
9
|
-
throw new Error(`Invalid credential type: ${type}`);
|
|
10
|
-
}
|
|
11
|
-
return type;
|
|
12
|
-
}
|
|
13
|
-
/** Convert database row to record */
|
|
14
|
-
function rowToRecord(row) {
|
|
15
|
-
return {
|
|
16
|
-
agent: row.agent,
|
|
17
|
-
name: row.name,
|
|
18
|
-
type: validateType(row.type),
|
|
19
|
-
encryptedData: row.encrypted_data,
|
|
20
|
-
salt: row.salt,
|
|
21
|
-
iv: row.iv,
|
|
22
|
-
authTag: row.auth_tag,
|
|
23
|
-
createdAt: new Date(row.created_at),
|
|
24
|
-
updatedAt: new Date(row.updated_at),
|
|
25
|
-
expiresAt: row.expires_at ? new Date(row.expires_at) : undefined,
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
/** Convert metadata row to metadata */
|
|
29
|
-
function rowToMetadata(row) {
|
|
30
|
-
return {
|
|
31
|
-
agent: row.agent,
|
|
32
|
-
name: row.name,
|
|
33
|
-
type: validateType(row.type),
|
|
34
|
-
createdAt: new Date(row.created_at),
|
|
35
|
-
updatedAt: new Date(row.updated_at),
|
|
36
|
-
expiresAt: row.expires_at ? new Date(row.expires_at) : undefined,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
6
|
+
import * as SQL from "./credentials-queries.js";
|
|
7
|
+
import { rowToMetadata, rowToRecord, } from "./parse-credential-row.js";
|
|
39
8
|
/** Store or update a credential */
|
|
40
9
|
function upsertCredential(database, credential) {
|
|
41
10
|
const now = Date.now();
|
|
42
|
-
|
|
11
|
+
// eslint-disable-next-line unicorn/no-null -- SQLite requires null for NULL values
|
|
12
|
+
const expiresAt = credential.expiresAt?.getTime() ?? null;
|
|
43
13
|
database
|
|
44
|
-
.prepare(
|
|
45
|
-
|
|
46
|
-
ON CONFLICT(agent, name) DO UPDATE SET
|
|
47
|
-
type = excluded.type, encrypted_data = excluded.encrypted_data, salt = excluded.salt, iv = excluded.iv, auth_tag = excluded.auth_tag,
|
|
48
|
-
updated_at = excluded.updated_at, expires_at = excluded.expires_at`)
|
|
49
|
-
.run(credential.agent, credential.name, credential.type, credential.encryptedData, credential.salt, credential.iv, credential.authTag, now, now, credential.expiresAt?.getTime() ?? null);
|
|
50
|
-
/* eslint-enable unicorn/no-null */
|
|
14
|
+
.prepare(SQL.UPSERT_CREDENTIAL)
|
|
15
|
+
.run(credential.agent, credential.name, credential.type, credential.encryptedData, credential.salt, credential.iv, credential.authTag, now, now, expiresAt);
|
|
51
16
|
}
|
|
52
17
|
/** Get a credential by agent and name */
|
|
53
18
|
function getCredential(database, agent, name) {
|
|
54
|
-
const row = database
|
|
55
|
-
.prepare(`SELECT agent, name, type, encrypted_data, salt, iv, auth_tag, created_at, updated_at, expires_at FROM credentials WHERE agent = ? AND name = ?`)
|
|
56
|
-
.get(agent, name);
|
|
19
|
+
const row = database.prepare(SQL.SELECT_CREDENTIAL).get(agent, name);
|
|
57
20
|
return row ? rowToRecord(row) : undefined;
|
|
58
21
|
}
|
|
59
22
|
/** List all credentials (metadata only) */
|
|
60
23
|
function listCredentials(database) {
|
|
61
|
-
const rows = database
|
|
62
|
-
.prepare(`SELECT agent, name, type, created_at, updated_at, expires_at FROM credentials ORDER BY agent, name`)
|
|
63
|
-
.all();
|
|
24
|
+
const rows = database.prepare(SQL.SELECT_ALL_METADATA).all();
|
|
64
25
|
return rows.map((row) => rowToMetadata(row));
|
|
65
26
|
}
|
|
66
27
|
/** List credentials accessible by an API key's read access list */
|
|
@@ -71,15 +32,12 @@ function listCredentialsForApiKey(database, readAccess) {
|
|
|
71
32
|
}
|
|
72
33
|
/** Delete a credential */
|
|
73
34
|
function deleteCredential(database, agent, name) {
|
|
74
|
-
return (
|
|
75
|
-
.prepare(`DELETE FROM credentials WHERE agent = ? AND name = ?`)
|
|
76
|
-
.run(agent, name).changes > 0);
|
|
35
|
+
return database.prepare(SQL.DELETE_CREDENTIAL).run(agent, name).changes > 0;
|
|
77
36
|
}
|
|
78
37
|
/** Update expiration time after refresh */
|
|
79
38
|
function updateExpiresAt(database, agent, name, expiresAt) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
.run(expiresAt?.getTime() ?? null, Date.now(), agent, name);
|
|
39
|
+
// eslint-disable-next-line unicorn/no-null -- SQLite requires null for NULL values
|
|
40
|
+
const expires = expiresAt?.getTime() ?? null;
|
|
41
|
+
database.prepare(SQL.UPDATE_EXPIRES_AT).run(expires, Date.now(), agent, name);
|
|
84
42
|
}
|
|
85
43
|
export { deleteCredential, getCredential, listCredentials, listCredentialsForApiKey, updateExpiresAt, upsertCredential, };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure functions for parsing credential database rows.
|
|
3
|
+
*/
|
|
4
|
+
import { CredentialType } from "axshared";
|
|
5
|
+
import type { CredentialRow, MetadataRow } from "../types.js";
|
|
6
|
+
/** Credential record stored in database */
|
|
7
|
+
interface CredentialRecord {
|
|
8
|
+
agent: string;
|
|
9
|
+
name: string;
|
|
10
|
+
type: CredentialType;
|
|
11
|
+
encryptedData: Buffer;
|
|
12
|
+
salt: Buffer;
|
|
13
|
+
iv: Buffer;
|
|
14
|
+
authTag: Buffer;
|
|
15
|
+
createdAt: Date;
|
|
16
|
+
updatedAt: Date;
|
|
17
|
+
expiresAt: Date | undefined;
|
|
18
|
+
}
|
|
19
|
+
/** Credential metadata (without encrypted data) */
|
|
20
|
+
interface CredentialMetadata {
|
|
21
|
+
agent: string;
|
|
22
|
+
name: string;
|
|
23
|
+
type: CredentialType;
|
|
24
|
+
createdAt: Date;
|
|
25
|
+
updatedAt: Date;
|
|
26
|
+
expiresAt: Date | undefined;
|
|
27
|
+
}
|
|
28
|
+
/** Convert database row to record */
|
|
29
|
+
declare function rowToRecord(row: CredentialRow): CredentialRecord;
|
|
30
|
+
/** Convert metadata row to metadata */
|
|
31
|
+
declare function rowToMetadata(row: MetadataRow): CredentialMetadata;
|
|
32
|
+
export { rowToMetadata, rowToRecord };
|
|
33
|
+
export type { CredentialMetadata, CredentialRecord };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure functions for parsing credential database rows.
|
|
3
|
+
*/
|
|
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
|
+
/** Convert database row to record */
|
|
14
|
+
function rowToRecord(row) {
|
|
15
|
+
return {
|
|
16
|
+
agent: row.agent,
|
|
17
|
+
name: row.name,
|
|
18
|
+
type: parseCredentialType(row.type),
|
|
19
|
+
encryptedData: row.encrypted_data,
|
|
20
|
+
salt: row.salt,
|
|
21
|
+
iv: row.iv,
|
|
22
|
+
authTag: row.auth_tag,
|
|
23
|
+
createdAt: new Date(row.created_at),
|
|
24
|
+
updatedAt: new Date(row.updated_at),
|
|
25
|
+
expiresAt: row.expires_at ? new Date(row.expires_at) : undefined,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Convert metadata row to metadata */
|
|
29
|
+
function rowToMetadata(row) {
|
|
30
|
+
return {
|
|
31
|
+
agent: row.agent,
|
|
32
|
+
name: row.name,
|
|
33
|
+
type: parseCredentialType(row.type),
|
|
34
|
+
createdAt: new Date(row.created_at),
|
|
35
|
+
updatedAt: new Date(row.updated_at),
|
|
36
|
+
expiresAt: row.expires_at ? new Date(row.expires_at) : undefined,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export { rowToMetadata, rowToRecord };
|
package/dist/db/types.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* GET /api/v1/credentials/:agent/:name handler.
|
|
3
3
|
*/
|
|
4
|
+
import { isRefreshableCredentialType } from "axshared";
|
|
4
5
|
import { hasReadAccess } from "../db/repositories/api-keys.js";
|
|
5
6
|
import { getCredential } from "../db/repositories/credentials.js";
|
|
6
7
|
import { logAccess } from "../db/repositories/audit-log.js";
|
|
@@ -69,7 +70,7 @@ function createGetCredentialHandler(database, config) {
|
|
|
69
70
|
let wasRefreshed = false;
|
|
70
71
|
let refreshFailed = false;
|
|
71
72
|
if (config.refreshThresholdSeconds > 0 &&
|
|
72
|
-
credential.type
|
|
73
|
+
isRefreshableCredentialType(credential.type) &&
|
|
73
74
|
isRefreshable(data) &&
|
|
74
75
|
needsRefresh(data, config.refreshThresholdSeconds)) {
|
|
75
76
|
try {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PUT /api/v1/credentials/:agent/:name handler.
|
|
3
3
|
*/
|
|
4
|
+
import { CredentialType, } from "axshared";
|
|
4
5
|
import { hasWriteAccess } from "../db/repositories/api-keys.js";
|
|
5
6
|
import { upsertCredential } from "../db/repositories/credentials.js";
|
|
6
7
|
import { logAccess } from "../db/repositories/audit-log.js";
|
|
@@ -43,7 +44,8 @@ function createPutCredentialHandler(database) {
|
|
|
43
44
|
.json({ error: "Request body must include 'data' object" });
|
|
44
45
|
return;
|
|
45
46
|
}
|
|
46
|
-
|
|
47
|
+
const typeResult = CredentialType.safeParse(body.type);
|
|
48
|
+
if (!typeResult.success) {
|
|
47
49
|
logAccess(database, {
|
|
48
50
|
apiKeyId: apiKey.id,
|
|
49
51
|
action: "write",
|
|
@@ -53,11 +55,11 @@ function createPutCredentialHandler(database) {
|
|
|
53
55
|
errorMessage: "Invalid type",
|
|
54
56
|
});
|
|
55
57
|
response.status(400).json({
|
|
56
|
-
error:
|
|
58
|
+
error: `Request body must include 'type' (${CredentialType.options.map((t) => `"${t}"`).join(", ")})`,
|
|
57
59
|
});
|
|
58
60
|
return;
|
|
59
61
|
}
|
|
60
|
-
const credentialType =
|
|
62
|
+
const credentialType = typeResult.data;
|
|
61
63
|
try {
|
|
62
64
|
const encrypted = encryptCredential(body.data);
|
|
63
65
|
let expiresAt;
|
package/dist/lib/format.d.ts
CHANGED
package/dist/lib/format.js
CHANGED
|
@@ -66,17 +66,27 @@ const KEY_ID_PATTERN = /^k_[0-9a-f]{12}$/u;
|
|
|
66
66
|
export function isValidKeyId(id) {
|
|
67
67
|
return KEY_ID_PATTERN.test(id);
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Format key prefix for display with redaction suffix.
|
|
71
|
+
* Returns "(unavailable)" for keys created before prefix storage.
|
|
72
|
+
*/
|
|
73
|
+
function formatKeyPrefix(prefix) {
|
|
74
|
+
if (!prefix)
|
|
75
|
+
return "(unavailable)";
|
|
76
|
+
return `${prefix}...`;
|
|
77
|
+
}
|
|
69
78
|
/**
|
|
70
79
|
* Format a single API key row for TSV output.
|
|
71
80
|
*/
|
|
72
81
|
export function formatKeyRow(key) {
|
|
73
82
|
const id = sanitizeForTsv(key.id);
|
|
74
83
|
const name = sanitizeForTsv(key.name);
|
|
84
|
+
const keyPrefix = sanitizeForTsv(formatKeyPrefix(key.keyPrefix));
|
|
75
85
|
const readAccess = sanitizeForTsv(formatAccessList(key.readAccess));
|
|
76
86
|
const writeAccess = sanitizeForTsv(formatAccessList(key.writeAccess));
|
|
77
87
|
const grantAccess = sanitizeForTsv(formatAccessList(key.grantAccess));
|
|
78
88
|
const lastUsed = formatRelativeTime(key.lastUsedAt);
|
|
79
|
-
return `${id}\t${name}\t${readAccess}\t${writeAccess}\t${grantAccess}\t${lastUsed}`;
|
|
89
|
+
return `${id}\t${name}\t${keyPrefix}\t${readAccess}\t${writeAccess}\t${grantAccess}\t${lastUsed}`;
|
|
80
90
|
}
|
|
81
91
|
/**
|
|
82
92
|
* Validate access list entry format.
|
|
@@ -50,10 +50,10 @@ function toAxauthCredentials(agent, data) {
|
|
|
50
50
|
if (!VALID_AGENTS.has(agent)) {
|
|
51
51
|
return undefined;
|
|
52
52
|
}
|
|
53
|
-
// Only OAuth credentials can be refreshed
|
|
53
|
+
// Only OAuth credentials with refresh tokens can be refreshed
|
|
54
54
|
return {
|
|
55
55
|
agent: agent,
|
|
56
|
-
type: "oauth",
|
|
56
|
+
type: "oauth-credentials",
|
|
57
57
|
data,
|
|
58
58
|
};
|
|
59
59
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* with axauth's refresh functionality.
|
|
6
6
|
*/
|
|
7
7
|
import type Database from "better-sqlite3";
|
|
8
|
-
import type { CredentialType } from "
|
|
8
|
+
import type { CredentialType } from "axshared";
|
|
9
9
|
/** Key for credential-specific mutex */
|
|
10
10
|
type CredentialKey = `${string}/${string}`;
|
|
11
11
|
/** Result type for the full refresh operation */
|
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.
|
|
5
|
+
"version": "1.4.0",
|
|
6
6
|
"description": "Remote credential storage server for axkit",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -49,9 +49,9 @@
|
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@commander-js/extra-typings": "^14.0.0",
|
|
52
|
-
"axauth": "^1.
|
|
53
|
-
"axshared": "
|
|
54
|
-
"better-sqlite3": "^12.
|
|
52
|
+
"axauth": "^1.9.0",
|
|
53
|
+
"axshared": "1.9.0",
|
|
54
|
+
"better-sqlite3": "^12.6.0",
|
|
55
55
|
"commander": "^14.0.2",
|
|
56
56
|
"express": "^5.2.1"
|
|
57
57
|
},
|
|
@@ -78,13 +78,13 @@
|
|
|
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.
|
|
81
|
+
"@types/node": "^25.0.5",
|
|
82
82
|
"@vitest/coverage-v8": "^4.0.16",
|
|
83
83
|
"eslint": "^9.39.2",
|
|
84
84
|
"eslint-config-axkit": "^1.0.0",
|
|
85
85
|
"fta-check": "^1.5.1",
|
|
86
86
|
"fta-cli": "^3.0.0",
|
|
87
|
-
"knip": "^5.80.
|
|
87
|
+
"knip": "^5.80.2",
|
|
88
88
|
"prettier": "3.7.4",
|
|
89
89
|
"semantic-release": "^25.0.2",
|
|
90
90
|
"typescript": "^5.9.3",
|