axvault 1.0.0 → 1.2.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 +38 -6
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +20 -3
- package/dist/commands/credential.js +4 -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 +35 -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.d.ts +6 -1
- package/dist/db/repositories/credentials.js +15 -9
- package/dist/db/types.d.ts +4 -1
- package/dist/handlers/get-credential.js +28 -25
- package/dist/handlers/list-credentials.js +1 -0
- package/dist/handlers/put-credential.js +20 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- 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/refresh-manager.d.ts +3 -1
- package/dist/refresh/refresh-manager.js +5 -3
- package/package.json +3 -3
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 = 4;
|
|
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 = 4;
|
|
7
7
|
/** Run all pending migrations */
|
|
8
8
|
function runMigrations(database) {
|
|
9
9
|
const version = getSchemaVersion(database);
|
|
@@ -13,6 +13,12 @@ function runMigrations(database) {
|
|
|
13
13
|
if (version < 2) {
|
|
14
14
|
migrateToV2(database);
|
|
15
15
|
}
|
|
16
|
+
if (version < 3) {
|
|
17
|
+
migrateToV3(database);
|
|
18
|
+
}
|
|
19
|
+
if (version < 4) {
|
|
20
|
+
migrateToV4(database);
|
|
21
|
+
}
|
|
16
22
|
}
|
|
17
23
|
/** Get current schema version */
|
|
18
24
|
function getSchemaVersion(database) {
|
|
@@ -93,4 +99,32 @@ function migrateToV2(database) {
|
|
|
93
99
|
setSchemaVersion(database, 2);
|
|
94
100
|
})();
|
|
95
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Migration to version 3: Add type column to credentials
|
|
104
|
+
*
|
|
105
|
+
* Explicit credential type ("oauth" or "api-key") instead of inferring from data.
|
|
106
|
+
* Existing credentials without type must be re-uploaded.
|
|
107
|
+
*/
|
|
108
|
+
function migrateToV3(database) {
|
|
109
|
+
database.transaction(() => {
|
|
110
|
+
database.exec(`
|
|
111
|
+
ALTER TABLE credentials ADD COLUMN type TEXT
|
|
112
|
+
`);
|
|
113
|
+
setSchemaVersion(database, 3);
|
|
114
|
+
})();
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Migration to version 4: Add grant_access column to api_keys
|
|
118
|
+
*
|
|
119
|
+
* The grant permission controls which credentials a key can delegate access to.
|
|
120
|
+
* Existing keys get an empty grant_access array (no delegation rights).
|
|
121
|
+
*/
|
|
122
|
+
function migrateToV4(database) {
|
|
123
|
+
database.transaction(() => {
|
|
124
|
+
database.exec(`
|
|
125
|
+
ALTER TABLE api_keys ADD COLUMN grant_access TEXT NOT NULL DEFAULT '[]'
|
|
126
|
+
`);
|
|
127
|
+
setSchemaVersion(database, 4);
|
|
128
|
+
})();
|
|
129
|
+
}
|
|
96
130
|
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;
|
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
* Manages encrypted credential storage.
|
|
5
5
|
*/
|
|
6
6
|
import type Database from "better-sqlite3";
|
|
7
|
+
/** Valid credential types */
|
|
8
|
+
type CredentialType = "oauth" | "api-key";
|
|
7
9
|
/** Credential record stored in database */
|
|
8
10
|
interface CredentialRecord {
|
|
9
11
|
agent: string;
|
|
10
12
|
name: string;
|
|
13
|
+
type: CredentialType;
|
|
11
14
|
encryptedData: Buffer;
|
|
12
15
|
salt: Buffer;
|
|
13
16
|
iv: Buffer;
|
|
@@ -20,6 +23,7 @@ interface CredentialRecord {
|
|
|
20
23
|
interface CredentialMetadata {
|
|
21
24
|
agent: string;
|
|
22
25
|
name: string;
|
|
26
|
+
type: CredentialType;
|
|
23
27
|
createdAt: Date;
|
|
24
28
|
updatedAt: Date;
|
|
25
29
|
expiresAt: Date | undefined;
|
|
@@ -28,6 +32,7 @@ interface CredentialMetadata {
|
|
|
28
32
|
declare function upsertCredential(database: Database.Database, credential: {
|
|
29
33
|
agent: string;
|
|
30
34
|
name: string;
|
|
35
|
+
type: CredentialType;
|
|
31
36
|
encryptedData: Buffer;
|
|
32
37
|
salt: Buffer;
|
|
33
38
|
iv: Buffer;
|
|
@@ -45,4 +50,4 @@ declare function deleteCredential(database: Database.Database, agent: string, na
|
|
|
45
50
|
/** Update expiration time after refresh */
|
|
46
51
|
declare function updateExpiresAt(database: Database.Database, agent: string, name: string, expiresAt: Date | undefined): void;
|
|
47
52
|
export { deleteCredential, getCredential, listCredentials, listCredentialsForApiKey, updateExpiresAt, upsertCredential, };
|
|
48
|
-
export type { CredentialMetadata, CredentialRecord };
|
|
53
|
+
export type { CredentialMetadata, CredentialRecord, CredentialType };
|
|
@@ -3,14 +3,19 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Manages encrypted credential storage.
|
|
5
5
|
*/
|
|
6
|
+
/** Validate credential type from database */
|
|
7
|
+
function validateType(type) {
|
|
8
|
+
if (type !== "oauth" && type !== "api-key") {
|
|
9
|
+
throw new Error(`Invalid credential type: ${type}`);
|
|
10
|
+
}
|
|
11
|
+
return type;
|
|
12
|
+
}
|
|
6
13
|
/** Convert database row to record */
|
|
7
14
|
function rowToRecord(row) {
|
|
8
|
-
if (!row.salt) {
|
|
9
|
-
throw new Error("Credential missing salt - database may need migration");
|
|
10
|
-
}
|
|
11
15
|
return {
|
|
12
16
|
agent: row.agent,
|
|
13
17
|
name: row.name,
|
|
18
|
+
type: validateType(row.type),
|
|
14
19
|
encryptedData: row.encrypted_data,
|
|
15
20
|
salt: row.salt,
|
|
16
21
|
iv: row.iv,
|
|
@@ -25,6 +30,7 @@ function rowToMetadata(row) {
|
|
|
25
30
|
return {
|
|
26
31
|
agent: row.agent,
|
|
27
32
|
name: row.name,
|
|
33
|
+
type: validateType(row.type),
|
|
28
34
|
createdAt: new Date(row.created_at),
|
|
29
35
|
updatedAt: new Date(row.updated_at),
|
|
30
36
|
expiresAt: row.expires_at ? new Date(row.expires_at) : undefined,
|
|
@@ -35,25 +41,25 @@ function upsertCredential(database, credential) {
|
|
|
35
41
|
const now = Date.now();
|
|
36
42
|
/* eslint-disable unicorn/no-null -- SQLite requires null for NULL values */
|
|
37
43
|
database
|
|
38
|
-
.prepare(`INSERT INTO credentials (agent, name, encrypted_data, salt, iv, auth_tag, created_at, updated_at, expires_at)
|
|
39
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
44
|
+
.prepare(`INSERT INTO credentials (agent, name, type, encrypted_data, salt, iv, auth_tag, created_at, updated_at, expires_at)
|
|
45
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
40
46
|
ON CONFLICT(agent, name) DO UPDATE SET
|
|
41
|
-
encrypted_data = excluded.encrypted_data, salt = excluded.salt, iv = excluded.iv, auth_tag = excluded.auth_tag,
|
|
47
|
+
type = excluded.type, encrypted_data = excluded.encrypted_data, salt = excluded.salt, iv = excluded.iv, auth_tag = excluded.auth_tag,
|
|
42
48
|
updated_at = excluded.updated_at, expires_at = excluded.expires_at`)
|
|
43
|
-
.run(credential.agent, credential.name, credential.encryptedData, credential.salt, credential.iv, credential.authTag, now, now, credential.expiresAt?.getTime() ?? null);
|
|
49
|
+
.run(credential.agent, credential.name, credential.type, credential.encryptedData, credential.salt, credential.iv, credential.authTag, now, now, credential.expiresAt?.getTime() ?? null);
|
|
44
50
|
/* eslint-enable unicorn/no-null */
|
|
45
51
|
}
|
|
46
52
|
/** Get a credential by agent and name */
|
|
47
53
|
function getCredential(database, agent, name) {
|
|
48
54
|
const row = database
|
|
49
|
-
.prepare(`SELECT agent, name, encrypted_data, salt, iv, auth_tag, created_at, updated_at, expires_at FROM credentials WHERE agent = ? AND name = ?`)
|
|
55
|
+
.prepare(`SELECT agent, name, type, encrypted_data, salt, iv, auth_tag, created_at, updated_at, expires_at FROM credentials WHERE agent = ? AND name = ?`)
|
|
50
56
|
.get(agent, name);
|
|
51
57
|
return row ? rowToRecord(row) : undefined;
|
|
52
58
|
}
|
|
53
59
|
/** List all credentials (metadata only) */
|
|
54
60
|
function listCredentials(database) {
|
|
55
61
|
const rows = database
|
|
56
|
-
.prepare(`SELECT agent, name, created_at, updated_at, expires_at FROM credentials ORDER BY agent, name`)
|
|
62
|
+
.prepare(`SELECT agent, name, type, created_at, updated_at, expires_at FROM credentials ORDER BY agent, name`)
|
|
57
63
|
.all();
|
|
58
64
|
return rows.map((row) => rowToMetadata(row));
|
|
59
65
|
}
|
package/dist/db/types.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface ApiKeyRow {
|
|
|
8
8
|
key_hash: string;
|
|
9
9
|
read_access: string;
|
|
10
10
|
write_access: string;
|
|
11
|
+
grant_access: string;
|
|
11
12
|
created_at: number;
|
|
12
13
|
last_used_at: number | null;
|
|
13
14
|
}
|
|
@@ -26,8 +27,9 @@ export interface AuditLogRow {
|
|
|
26
27
|
export interface CredentialRow {
|
|
27
28
|
agent: string;
|
|
28
29
|
name: string;
|
|
30
|
+
type: string;
|
|
29
31
|
encrypted_data: Buffer;
|
|
30
|
-
salt: Buffer
|
|
32
|
+
salt: Buffer;
|
|
31
33
|
iv: Buffer;
|
|
32
34
|
auth_tag: Buffer;
|
|
33
35
|
created_at: number;
|
|
@@ -38,6 +40,7 @@ export interface CredentialRow {
|
|
|
38
40
|
export interface MetadataRow {
|
|
39
41
|
agent: string;
|
|
40
42
|
name: string;
|
|
43
|
+
type: string;
|
|
41
44
|
created_at: number;
|
|
42
45
|
updated_at: number;
|
|
43
46
|
expires_at: number | null;
|
|
@@ -30,28 +30,7 @@ function createGetCredentialHandler(database, config) {
|
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
32
|
// Fetch credential from database
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
credential = getCredential(database, agent, name);
|
|
36
|
-
}
|
|
37
|
-
catch (error) {
|
|
38
|
-
// Handle legacy credentials stored without salt (pre-v2 migration)
|
|
39
|
-
if (error instanceof Error && error.message.includes("missing salt")) {
|
|
40
|
-
logAccess(database, {
|
|
41
|
-
apiKeyId: apiKey.id,
|
|
42
|
-
action: "read",
|
|
43
|
-
agent,
|
|
44
|
-
name,
|
|
45
|
-
success: false,
|
|
46
|
-
errorMessage: "Legacy credential requires re-upload",
|
|
47
|
-
});
|
|
48
|
-
response.status(410).json({
|
|
49
|
-
error: "This credential was stored with legacy encryption and must be deleted and re-uploaded",
|
|
50
|
-
});
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
throw error;
|
|
54
|
-
}
|
|
33
|
+
const credential = getCredential(database, agent, name);
|
|
55
34
|
if (!credential) {
|
|
56
35
|
logAccess(database, {
|
|
57
36
|
apiKeyId: apiKey.id,
|
|
@@ -90,10 +69,11 @@ function createGetCredentialHandler(database, config) {
|
|
|
90
69
|
let wasRefreshed = false;
|
|
91
70
|
let refreshFailed = false;
|
|
92
71
|
if (config.refreshThresholdSeconds > 0 &&
|
|
72
|
+
credential.type === "oauth" &&
|
|
93
73
|
isRefreshable(data) &&
|
|
94
74
|
needsRefresh(data, config.refreshThresholdSeconds)) {
|
|
95
75
|
try {
|
|
96
|
-
const refreshResult = await refreshWithMutex(database, agent, name, data, apiKey.id, credential.updatedAt, { timeoutMs: config.refreshTimeoutMs });
|
|
76
|
+
const refreshResult = await refreshWithMutex(database, agent, name, credential.type, data, apiKey.id, credential.updatedAt, { timeoutMs: config.refreshTimeoutMs });
|
|
97
77
|
if (refreshResult.ok) {
|
|
98
78
|
finalData = refreshResult.data;
|
|
99
79
|
finalExpiresAt = refreshResult.expiresAt;
|
|
@@ -101,8 +81,30 @@ function createGetCredentialHandler(database, config) {
|
|
|
101
81
|
wasRefreshed = true;
|
|
102
82
|
}
|
|
103
83
|
else {
|
|
104
|
-
// Refresh failed -
|
|
105
|
-
|
|
84
|
+
// Refresh failed - re-fetch to check if credential was deleted/modified
|
|
85
|
+
const currentCredential = getCredential(database, agent, name);
|
|
86
|
+
if (!currentCredential) {
|
|
87
|
+
// Credential was deleted during refresh - don't leak old data
|
|
88
|
+
logAccess(database, {
|
|
89
|
+
apiKeyId: apiKey.id,
|
|
90
|
+
action: "read",
|
|
91
|
+
agent,
|
|
92
|
+
name,
|
|
93
|
+
success: false,
|
|
94
|
+
errorMessage: "Credential deleted during refresh",
|
|
95
|
+
});
|
|
96
|
+
response.status(404).json({ error: "Credential not found" });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (currentCredential.updatedAt.getTime() !==
|
|
100
|
+
credential.updatedAt.getTime()) {
|
|
101
|
+
// Credential was modified during refresh - return fresh data
|
|
102
|
+
const freshData = decryptCredential(currentCredential);
|
|
103
|
+
finalData = freshData;
|
|
104
|
+
finalExpiresAt = currentCredential.expiresAt;
|
|
105
|
+
finalUpdatedAt = currentCredential.updatedAt;
|
|
106
|
+
}
|
|
107
|
+
// Otherwise credential unchanged, return original with warning
|
|
106
108
|
refreshFailed = true;
|
|
107
109
|
}
|
|
108
110
|
}
|
|
@@ -134,6 +136,7 @@ function createGetCredentialHandler(database, config) {
|
|
|
134
136
|
response.json({
|
|
135
137
|
agent,
|
|
136
138
|
name,
|
|
139
|
+
type: credential.type,
|
|
137
140
|
data: finalData,
|
|
138
141
|
expiresAt: finalExpiresAt?.toISOString(),
|
|
139
142
|
updatedAt: finalUpdatedAt.toISOString(),
|
|
@@ -26,7 +26,10 @@ function createPutCredentialHandler(database) {
|
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
const body = request.body;
|
|
29
|
-
if (!body ||
|
|
29
|
+
if (!body ||
|
|
30
|
+
typeof body.data !== "object" ||
|
|
31
|
+
body.data === null ||
|
|
32
|
+
Array.isArray(body.data)) {
|
|
30
33
|
logAccess(database, {
|
|
31
34
|
apiKeyId: apiKey.id,
|
|
32
35
|
action: "write",
|
|
@@ -40,6 +43,21 @@ function createPutCredentialHandler(database) {
|
|
|
40
43
|
.json({ error: "Request body must include 'data' object" });
|
|
41
44
|
return;
|
|
42
45
|
}
|
|
46
|
+
if (body.type !== "oauth" && body.type !== "api-key") {
|
|
47
|
+
logAccess(database, {
|
|
48
|
+
apiKeyId: apiKey.id,
|
|
49
|
+
action: "write",
|
|
50
|
+
agent,
|
|
51
|
+
name,
|
|
52
|
+
success: false,
|
|
53
|
+
errorMessage: "Invalid type",
|
|
54
|
+
});
|
|
55
|
+
response.status(400).json({
|
|
56
|
+
error: 'Request body must include \'type\' ("oauth" or "api-key")',
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const credentialType = body.type;
|
|
43
61
|
try {
|
|
44
62
|
const encrypted = encryptCredential(body.data);
|
|
45
63
|
let expiresAt;
|
|
@@ -61,6 +79,7 @@ function createPutCredentialHandler(database) {
|
|
|
61
79
|
upsertCredential(database, {
|
|
62
80
|
agent,
|
|
63
81
|
name,
|
|
82
|
+
type: credentialType,
|
|
64
83
|
...encrypted,
|
|
65
84
|
expiresAt,
|
|
66
85
|
});
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
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.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse and validate access list options from CLI flags.
|
|
3
|
+
*/
|
|
4
|
+
interface ParsedAccessOptions {
|
|
5
|
+
addRead: string[];
|
|
6
|
+
addWrite: string[];
|
|
7
|
+
addGrant: string[];
|
|
8
|
+
removeRead: string[];
|
|
9
|
+
removeWrite: string[];
|
|
10
|
+
removeGrant: string[];
|
|
11
|
+
}
|
|
12
|
+
interface AccessOptions {
|
|
13
|
+
addRead?: string;
|
|
14
|
+
addWrite?: string;
|
|
15
|
+
addGrant?: string;
|
|
16
|
+
removeRead?: string;
|
|
17
|
+
removeWrite?: string;
|
|
18
|
+
removeGrant?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Parse all access list options and validate them.
|
|
22
|
+
* Returns parsed entries or logs error and sets exit code.
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseAccessOptions(options: AccessOptions): ParsedAccessOptions | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Compute updated access list by adding and removing entries.
|
|
27
|
+
*/
|
|
28
|
+
export declare function computeUpdatedAccess(current: string[], toAdd: string[], toRemove: string[]): string[];
|
|
29
|
+
/**
|
|
30
|
+
* Normalize all access lists and print warnings.
|
|
31
|
+
*/
|
|
32
|
+
export declare function normalizeAllAccess(readAccess: string[], writeAccess: string[], grantAccess: string[]): {
|
|
33
|
+
read: string[];
|
|
34
|
+
write: string[];
|
|
35
|
+
grant: string[];
|
|
36
|
+
};
|
|
37
|
+
export {};
|