axvault 1.1.0 → 1.3.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 +41 -8
- package/dist/cli.js +19 -2
- package/dist/commands/key-create.d.ts +13 -0
- package/dist/commands/key-create.js +99 -0
- package/dist/commands/key-list.d.ts +9 -0
- package/dist/commands/key-list.js +43 -0
- package/dist/commands/key-revoke.d.ts +8 -0
- package/dist/commands/key-revoke.js +47 -0
- package/dist/commands/key-update.d.ts +15 -0
- package/dist/commands/key-update.js +109 -0
- package/dist/commands/key.d.ts +6 -18
- package/dist/commands/key.js +6 -154
- package/dist/db/migrations.d.ts +1 -1
- package/dist/db/migrations.js +39 -1
- package/dist/db/repositories/api-keys.d.ts +11 -1
- package/dist/db/repositories/api-keys.js +34 -5
- package/dist/db/repositories/audit-log.d.ts +1 -1
- 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 +2 -1
- package/dist/lib/parse-access-options.d.ts +37 -0
- package/dist/lib/parse-access-options.js +84 -0
- package/dist/refresh/check-refresh.js +2 -2
- package/dist/refresh/refresh-manager.d.ts +1 -1
- package/package.json +6 -6
package/dist/commands/key.js
CHANGED
|
@@ -1,157 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* API key management command handlers.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports handlers from separate modules for better maintainability.
|
|
3
5
|
*/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import { containsControlChars, formatAccessList, formatDateForJson, formatKeyRow, getAccessListErrorMessage, getErrorMessage, isValidKeyId, normalizeAccessList, parseAccessList, sanitizeForTsv, } from "../lib/format.js";
|
|
9
|
-
export function handleKeyCreate(options) {
|
|
10
|
-
// Validate key name (reject control characters)
|
|
11
|
-
if (containsControlChars(options.name)) {
|
|
12
|
-
console.error("Error: Key name contains control characters.");
|
|
13
|
-
process.exitCode = 2;
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
|
-
// Parse and validate access lists
|
|
17
|
-
const readResult = parseAccessList(options.read);
|
|
18
|
-
const writeResult = parseAccessList(options.write);
|
|
19
|
-
if (readResult.error) {
|
|
20
|
-
console.error(`Error: ${getAccessListErrorMessage(readResult.error)}`);
|
|
21
|
-
process.exitCode = 2;
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
if (writeResult.error) {
|
|
25
|
-
console.error(`Error: ${getAccessListErrorMessage(writeResult.error)}`);
|
|
26
|
-
process.exitCode = 2;
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
// Normalize wildcards (warn if mixed with specific entries)
|
|
30
|
-
const readNorm = normalizeAccessList(readResult.entries, "read");
|
|
31
|
-
const writeNorm = normalizeAccessList(writeResult.entries, "write");
|
|
32
|
-
if (readNorm.warning)
|
|
33
|
-
console.warn(`Warning: ${readNorm.warning}`);
|
|
34
|
-
if (writeNorm.warning)
|
|
35
|
-
console.warn(`Warning: ${writeNorm.warning}`);
|
|
36
|
-
const readAccess = readNorm.normalized;
|
|
37
|
-
const writeAccess = writeNorm.normalized;
|
|
38
|
-
// Validate: at least one access must be specified
|
|
39
|
-
if (readAccess.length === 0 && writeAccess.length === 0) {
|
|
40
|
-
console.error("Error: At least one of --read or --write must be specified.");
|
|
41
|
-
console.error("Try 'axvault key create --help' for more information.");
|
|
42
|
-
process.exitCode = 2;
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
try {
|
|
46
|
-
const config = getServerConfig(options);
|
|
47
|
-
const database = getDatabase(config.databasePath);
|
|
48
|
-
runMigrations(database);
|
|
49
|
-
const apiKey = createApiKey(database, {
|
|
50
|
-
name: options.name,
|
|
51
|
-
readAccess,
|
|
52
|
-
writeAccess,
|
|
53
|
-
});
|
|
54
|
-
if (options.json) {
|
|
55
|
-
console.warn("Warning: JSON output contains the secret key. Avoid logging in CI.");
|
|
56
|
-
console.log(JSON.stringify({
|
|
57
|
-
id: apiKey.id,
|
|
58
|
-
name: apiKey.name,
|
|
59
|
-
key: apiKey.key,
|
|
60
|
-
readAccess: apiKey.readAccess,
|
|
61
|
-
writeAccess: apiKey.writeAccess,
|
|
62
|
-
createdAt: apiKey.createdAt.toISOString(),
|
|
63
|
-
}, undefined, 2));
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
console.error(`Created API key: ${sanitizeForTsv(apiKey.name)}`);
|
|
67
|
-
console.error(`ID: ${sanitizeForTsv(apiKey.id)}`);
|
|
68
|
-
console.error(`Read access: ${sanitizeForTsv(formatAccessList(apiKey.readAccess))}`);
|
|
69
|
-
console.error(`Write access: ${sanitizeForTsv(formatAccessList(apiKey.writeAccess))}`);
|
|
70
|
-
console.error("");
|
|
71
|
-
// Output the secret key to stdout for piping
|
|
72
|
-
console.log(apiKey.key);
|
|
73
|
-
console.error("");
|
|
74
|
-
console.error("Save this key securely - it cannot be retrieved later.");
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
catch (error) {
|
|
78
|
-
console.error(`Error: Failed to create API key: ${getErrorMessage(error)}`);
|
|
79
|
-
process.exitCode = 1;
|
|
80
|
-
}
|
|
81
|
-
finally {
|
|
82
|
-
closeDatabase();
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
export function handleKeyList(options) {
|
|
86
|
-
try {
|
|
87
|
-
const config = getServerConfig(options);
|
|
88
|
-
const database = getDatabase(config.databasePath);
|
|
89
|
-
runMigrations(database);
|
|
90
|
-
const keys = listApiKeys(database);
|
|
91
|
-
if (options.json) {
|
|
92
|
-
const output = keys.map((key) => ({
|
|
93
|
-
id: key.id,
|
|
94
|
-
name: key.name,
|
|
95
|
-
readAccess: key.readAccess,
|
|
96
|
-
writeAccess: key.writeAccess,
|
|
97
|
-
createdAt: key.createdAt.toISOString(),
|
|
98
|
-
lastUsedAt: formatDateForJson(key.lastUsedAt),
|
|
99
|
-
}));
|
|
100
|
-
console.log(JSON.stringify(output, undefined, 2));
|
|
101
|
-
}
|
|
102
|
-
else if (keys.length === 0) {
|
|
103
|
-
console.error("No API keys found.\nCreate one with: axvault key create --name <name> [--read <access>] [--write <access>]");
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
console.log("ID\tNAME\tREAD ACCESS\tWRITE ACCESS\tLAST USED");
|
|
107
|
-
for (const key of keys)
|
|
108
|
-
console.log(formatKeyRow(key));
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
catch (error) {
|
|
112
|
-
console.error(`Error: Failed to list API keys: ${getErrorMessage(error)}`);
|
|
113
|
-
process.exitCode = 1;
|
|
114
|
-
}
|
|
115
|
-
finally {
|
|
116
|
-
closeDatabase();
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
export function handleKeyRevoke(id, options) {
|
|
120
|
-
// Reject inputs containing control characters BEFORE trimming
|
|
121
|
-
// (prevents silent bypass where \n or \t would be removed by trim)
|
|
122
|
-
if (containsControlChars(id)) {
|
|
123
|
-
console.error("Error: Invalid API key ID: contains control characters.");
|
|
124
|
-
console.error("API key IDs have format: k_<12 hex chars> (e.g., k_abc123def456)");
|
|
125
|
-
process.exitCode = 2;
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
// Trim whitespace from ID (common copy-paste issue)
|
|
129
|
-
const trimmedId = id.trim();
|
|
130
|
-
// Validate ID format (k_ + 12 hex chars)
|
|
131
|
-
if (!isValidKeyId(trimmedId)) {
|
|
132
|
-
console.error(`Error: Invalid API key ID format: ${trimmedId}`);
|
|
133
|
-
console.error("API key IDs have format: k_<12 hex chars> (e.g., k_abc123def456)");
|
|
134
|
-
process.exitCode = 2;
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
try {
|
|
138
|
-
const config = getServerConfig(options);
|
|
139
|
-
const database = getDatabase(config.databasePath);
|
|
140
|
-
runMigrations(database);
|
|
141
|
-
const deleted = deleteApiKey(database, trimmedId);
|
|
142
|
-
if (deleted) {
|
|
143
|
-
console.error(`Revoked API key: ${sanitizeForTsv(trimmedId)}`);
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
console.error(`Error: API key not found: ${sanitizeForTsv(trimmedId)}`);
|
|
147
|
-
process.exitCode = 1;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
catch (error) {
|
|
151
|
-
console.error(`Error: Failed to revoke API key: ${getErrorMessage(error)}`);
|
|
152
|
-
process.exitCode = 1;
|
|
153
|
-
}
|
|
154
|
-
finally {
|
|
155
|
-
closeDatabase();
|
|
156
|
-
}
|
|
157
|
-
}
|
|
6
|
+
export { handleKeyCreate } from "./key-create.js";
|
|
7
|
+
export { handleKeyList } from "./key-list.js";
|
|
8
|
+
export { handleKeyRevoke } from "./key-revoke.js";
|
|
9
|
+
export { handleKeyUpdate } from "./key-update.js";
|
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 = 5;
|
|
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 = 5;
|
|
7
7
|
/** Run all pending migrations */
|
|
8
8
|
function runMigrations(database) {
|
|
9
9
|
const version = getSchemaVersion(database);
|
|
@@ -16,6 +16,12 @@ function runMigrations(database) {
|
|
|
16
16
|
if (version < 3) {
|
|
17
17
|
migrateToV3(database);
|
|
18
18
|
}
|
|
19
|
+
if (version < 4) {
|
|
20
|
+
migrateToV4(database);
|
|
21
|
+
}
|
|
22
|
+
if (version < 5) {
|
|
23
|
+
migrateToV5(database);
|
|
24
|
+
}
|
|
19
25
|
}
|
|
20
26
|
/** Get current schema version */
|
|
21
27
|
function getSchemaVersion(database) {
|
|
@@ -110,4 +116,36 @@ function migrateToV3(database) {
|
|
|
110
116
|
setSchemaVersion(database, 3);
|
|
111
117
|
})();
|
|
112
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Migration to version 4: Add grant_access column to api_keys
|
|
121
|
+
*
|
|
122
|
+
* The grant permission controls which credentials a key can delegate access to.
|
|
123
|
+
* Existing keys get an empty grant_access array (no delegation rights).
|
|
124
|
+
*/
|
|
125
|
+
function migrateToV4(database) {
|
|
126
|
+
database.transaction(() => {
|
|
127
|
+
database.exec(`
|
|
128
|
+
ALTER TABLE api_keys ADD COLUMN grant_access TEXT NOT NULL DEFAULT '[]'
|
|
129
|
+
`);
|
|
130
|
+
setSchemaVersion(database, 4);
|
|
131
|
+
})();
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Migration to version 5: Rename credential type "oauth" to "oauth-credentials"
|
|
135
|
+
*
|
|
136
|
+
* Distinguishes refreshable OAuth credentials (oauth-credentials) from
|
|
137
|
+
* long-lived OAuth tokens like CLAUDE_CODE_OAUTH_TOKEN (oauth-token).
|
|
138
|
+
*
|
|
139
|
+
* - `oauth-credentials`: Full OAuth flow with accessToken, refreshToken, expiresAt (refreshable)
|
|
140
|
+
* - `oauth-token`: Long-lived OAuth token for CI/CD (static, no refresh)
|
|
141
|
+
* - `api-key`: API key (static)
|
|
142
|
+
*/
|
|
143
|
+
function migrateToV5(database) {
|
|
144
|
+
database.transaction(() => {
|
|
145
|
+
database.exec(`
|
|
146
|
+
UPDATE credentials SET type = 'oauth-credentials' WHERE type = 'oauth'
|
|
147
|
+
`);
|
|
148
|
+
setSchemaVersion(database, 5);
|
|
149
|
+
})();
|
|
150
|
+
}
|
|
113
151
|
export { CURRENT_VERSION, getSchemaVersion, runMigrations };
|
|
@@ -11,6 +11,7 @@ interface ApiKeyRecord {
|
|
|
11
11
|
keyHash: string;
|
|
12
12
|
readAccess: string[];
|
|
13
13
|
writeAccess: string[];
|
|
14
|
+
grantAccess: string[];
|
|
14
15
|
createdAt: Date;
|
|
15
16
|
lastUsedAt: Date | undefined;
|
|
16
17
|
}
|
|
@@ -23,6 +24,7 @@ declare function createApiKey(database: Database.Database, options: {
|
|
|
23
24
|
name: string;
|
|
24
25
|
readAccess: string[];
|
|
25
26
|
writeAccess: string[];
|
|
27
|
+
grantAccess: string[];
|
|
26
28
|
}): ApiKeyWithSecret;
|
|
27
29
|
/** Find API key by raw key value */
|
|
28
30
|
declare function findApiKeyByKey(database: Database.Database, key: string): ApiKeyRecord | undefined;
|
|
@@ -38,5 +40,13 @@ declare function deleteApiKey(database: Database.Database, id: string): boolean;
|
|
|
38
40
|
declare function hasReadAccess(apiKey: ApiKeyRecord, agent: string, name: string): boolean;
|
|
39
41
|
/** Check if API key has write access to a credential */
|
|
40
42
|
declare function hasWriteAccess(apiKey: ApiKeyRecord, agent: string, name: string): boolean;
|
|
41
|
-
|
|
43
|
+
/** Check if API key has grant access to a credential */
|
|
44
|
+
declare function hasGrantAccess(apiKey: ApiKeyRecord, agent: string, name: string): boolean;
|
|
45
|
+
/** Update an API key's access permissions */
|
|
46
|
+
declare function updateApiKeyAccess(database: Database.Database, id: string, options: {
|
|
47
|
+
readAccess?: string[];
|
|
48
|
+
writeAccess?: string[];
|
|
49
|
+
grantAccess?: string[];
|
|
50
|
+
}): boolean;
|
|
51
|
+
export { createApiKey, deleteApiKey, findApiKeyById, findApiKeyByKey, hasGrantAccess, hasReadAccess, hasWriteAccess, listApiKeys, updateApiKeyAccess, updateLastUsed, };
|
|
42
52
|
export type { ApiKeyRecord, ApiKeyWithSecret };
|
|
@@ -12,6 +12,7 @@ function rowToRecord(row) {
|
|
|
12
12
|
keyHash: row.key_hash,
|
|
13
13
|
readAccess: JSON.parse(row.read_access),
|
|
14
14
|
writeAccess: JSON.parse(row.write_access),
|
|
15
|
+
grantAccess: JSON.parse(row.grant_access),
|
|
15
16
|
createdAt: new Date(row.created_at),
|
|
16
17
|
lastUsedAt: row.last_used_at ? new Date(row.last_used_at) : undefined,
|
|
17
18
|
};
|
|
@@ -20,7 +21,7 @@ function rowToRecord(row) {
|
|
|
20
21
|
function hashApiKey(key) {
|
|
21
22
|
return createHash("sha256").update(key).digest("hex");
|
|
22
23
|
}
|
|
23
|
-
const SELECT_COLUMNS = `id, name, key_hash, read_access, write_access, created_at, last_used_at`;
|
|
24
|
+
const SELECT_COLUMNS = `id, name, key_hash, read_access, write_access, grant_access, created_at, last_used_at`;
|
|
24
25
|
/** Create a new API key */
|
|
25
26
|
function createApiKey(database, options) {
|
|
26
27
|
const id = `k_${randomBytes(6).toString("hex")}`;
|
|
@@ -28,9 +29,9 @@ function createApiKey(database, options) {
|
|
|
28
29
|
const keyHash = hashApiKey(key);
|
|
29
30
|
const now = Date.now();
|
|
30
31
|
database
|
|
31
|
-
.prepare(`INSERT INTO api_keys (id, name, key_hash, read_access, write_access, created_at)
|
|
32
|
-
VALUES (?, ?, ?, ?, ?, ?)`)
|
|
33
|
-
.run(id, options.name, keyHash, JSON.stringify(options.readAccess), JSON.stringify(options.writeAccess), now);
|
|
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);
|
|
34
35
|
return {
|
|
35
36
|
id,
|
|
36
37
|
name: options.name,
|
|
@@ -38,6 +39,7 @@ function createApiKey(database, options) {
|
|
|
38
39
|
keyHash,
|
|
39
40
|
readAccess: options.readAccess,
|
|
40
41
|
writeAccess: options.writeAccess,
|
|
42
|
+
grantAccess: options.grantAccess,
|
|
41
43
|
createdAt: new Date(now),
|
|
42
44
|
lastUsedAt: undefined,
|
|
43
45
|
};
|
|
@@ -83,4 +85,31 @@ function hasWriteAccess(apiKey, agent, name) {
|
|
|
83
85
|
const path = `${agent}/${name}`;
|
|
84
86
|
return apiKey.writeAccess.includes("*") || apiKey.writeAccess.includes(path);
|
|
85
87
|
}
|
|
86
|
-
|
|
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
|
+
/** Update an API key's access permissions */
|
|
94
|
+
function updateApiKeyAccess(database, id, options) {
|
|
95
|
+
const updates = [];
|
|
96
|
+
const values = [];
|
|
97
|
+
if (options.readAccess !== undefined) {
|
|
98
|
+
updates.push("read_access = ?");
|
|
99
|
+
values.push(JSON.stringify(options.readAccess));
|
|
100
|
+
}
|
|
101
|
+
if (options.writeAccess !== undefined) {
|
|
102
|
+
updates.push("write_access = ?");
|
|
103
|
+
values.push(JSON.stringify(options.writeAccess));
|
|
104
|
+
}
|
|
105
|
+
if (options.grantAccess !== undefined) {
|
|
106
|
+
updates.push("grant_access = ?");
|
|
107
|
+
values.push(JSON.stringify(options.grantAccess));
|
|
108
|
+
}
|
|
109
|
+
if (updates.length === 0)
|
|
110
|
+
return false;
|
|
111
|
+
values.push(id);
|
|
112
|
+
const sql = `UPDATE api_keys SET ${updates.join(", ")} WHERE id = ?`;
|
|
113
|
+
return database.prepare(sql).run(...values).changes > 0;
|
|
114
|
+
}
|
|
115
|
+
export { createApiKey, deleteApiKey, findApiKeyById, findApiKeyByKey, hasGrantAccess, hasReadAccess, hasWriteAccess, listApiKeys, updateApiKeyAccess, updateLastUsed, };
|
|
@@ -9,7 +9,7 @@ interface AuditLogEntry {
|
|
|
9
9
|
id: number;
|
|
10
10
|
timestamp: Date;
|
|
11
11
|
apiKeyId: string | undefined;
|
|
12
|
-
action: "auth" | "read" | "write" | "delete" | "refresh" | "list";
|
|
12
|
+
action: "auth" | "read" | "write" | "delete" | "refresh" | "list" | "grant";
|
|
13
13
|
agent: string | undefined;
|
|
14
14
|
name: string | undefined;
|
|
15
15
|
success: boolean;
|
|
@@ -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
|
@@ -74,8 +74,9 @@ export function formatKeyRow(key) {
|
|
|
74
74
|
const name = sanitizeForTsv(key.name);
|
|
75
75
|
const readAccess = sanitizeForTsv(formatAccessList(key.readAccess));
|
|
76
76
|
const writeAccess = sanitizeForTsv(formatAccessList(key.writeAccess));
|
|
77
|
+
const grantAccess = sanitizeForTsv(formatAccessList(key.grantAccess));
|
|
77
78
|
const lastUsed = formatRelativeTime(key.lastUsedAt);
|
|
78
|
-
return `${id}\t${name}\t${readAccess}\t${writeAccess}\t${lastUsed}`;
|
|
79
|
+
return `${id}\t${name}\t${readAccess}\t${writeAccess}\t${grantAccess}\t${lastUsed}`;
|
|
79
80
|
}
|
|
80
81
|
/**
|
|
81
82
|
* Validate access list entry format.
|