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/README.md
CHANGED
|
@@ -32,17 +32,27 @@ Setting `--refresh-threshold 0` disables automatic credential refresh.
|
|
|
32
32
|
|
|
33
33
|
## API Keys
|
|
34
34
|
|
|
35
|
-
API keys control access to the credential API. Each key has configurable
|
|
35
|
+
API keys control access to the credential API. Each key has configurable permissions:
|
|
36
|
+
|
|
37
|
+
- **Read**: retrieve credentials
|
|
38
|
+
- **Write**: store and delete credentials
|
|
39
|
+
- **Grant**: delegate access to other keys (enforcement coming in future release)
|
|
36
40
|
|
|
37
41
|
### Create an API Key
|
|
38
42
|
|
|
39
43
|
```bash
|
|
40
|
-
# Full access
|
|
44
|
+
# Full access (read, write, and grant)
|
|
45
|
+
axvault key create --name "Admin" --read "*" --write "*" --grant "*"
|
|
46
|
+
|
|
47
|
+
# Read/write access only
|
|
41
48
|
axvault key create --name "CI Pipeline" --read "*" --write "*"
|
|
42
49
|
|
|
43
50
|
# Restricted access
|
|
44
|
-
axvault key create --name "Claude Reader" --read "claude
|
|
51
|
+
axvault key create --name "Claude Reader" --read "claude/work,claude/ci"
|
|
45
52
|
axvault key create --name "Deploy Script" --write "claude/prod,codex/prod"
|
|
53
|
+
|
|
54
|
+
# Grant-only key (for delegation, does not allow direct read/write)
|
|
55
|
+
axvault key create --name "Issuer" --grant "claude/work,claude/ci"
|
|
46
56
|
```
|
|
47
57
|
|
|
48
58
|
The command outputs metadata to stderr and the secret key to stdout for easy piping:
|
|
@@ -53,6 +63,7 @@ Created API key: CI Pipeline
|
|
|
53
63
|
ID: k_a1b2c3d4e5f6
|
|
54
64
|
Read access: *
|
|
55
65
|
Write access: *
|
|
66
|
+
Grant access: (none)
|
|
56
67
|
|
|
57
68
|
Save this key securely - it cannot be retrieved later.
|
|
58
69
|
|
|
@@ -68,6 +79,24 @@ To copy directly to clipboard: `axvault key create --name "My Key" --read "*" |
|
|
|
68
79
|
axvault key list
|
|
69
80
|
```
|
|
70
81
|
|
|
82
|
+
### Update a Key
|
|
83
|
+
|
|
84
|
+
Modify an existing key's permissions:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Add read access for new credentials
|
|
88
|
+
axvault key update k_a1b2c3d4e5f6 --add-read "gemini/prod"
|
|
89
|
+
|
|
90
|
+
# Remove write access
|
|
91
|
+
axvault key update k_a1b2c3d4e5f6 --remove-write "claude/test"
|
|
92
|
+
|
|
93
|
+
# Add grant permissions
|
|
94
|
+
axvault key update k_a1b2c3d4e5f6 --add-grant "claude/work"
|
|
95
|
+
|
|
96
|
+
# Multiple changes at once
|
|
97
|
+
axvault key update k_a1b2c3d4e5f6 --add-read "codex/ci" --remove-write "claude/dev"
|
|
98
|
+
```
|
|
99
|
+
|
|
71
100
|
### Revoke a Key
|
|
72
101
|
|
|
73
102
|
```bash
|
|
@@ -148,10 +177,10 @@ Exec into the container to manage API keys:
|
|
|
148
177
|
|
|
149
178
|
```bash
|
|
150
179
|
# Podman
|
|
151
|
-
sudo podman exec axvault /nodejs/bin/node node_modules/axvault/bin/axvault key create --name "My Key" --write "*"
|
|
180
|
+
sudo podman exec axvault /nodejs/bin/node node_modules/axvault/bin/axvault key create --name "My Key" --read "*" --write "*"
|
|
152
181
|
|
|
153
182
|
# Docker
|
|
154
|
-
docker exec axvault /nodejs/bin/node node_modules/axvault/bin/axvault key create --name "My Key" --write "*"
|
|
183
|
+
docker exec axvault /nodejs/bin/node node_modules/axvault/bin/axvault key create --name "My Key" --read "*" --write "*"
|
|
155
184
|
```
|
|
156
185
|
|
|
157
186
|
## Credentials API
|
|
@@ -163,13 +192,17 @@ curl -X PUT https://vault.example.com/api/v1/credentials/claude/prod \
|
|
|
163
192
|
-H "Authorization: Bearer <api_key>" \
|
|
164
193
|
-H "Content-Type: application/json" \
|
|
165
194
|
-d '{
|
|
166
|
-
"type": "oauth",
|
|
195
|
+
"type": "oauth-credentials",
|
|
167
196
|
"data": {"access_token": "...", "refresh_token": "..."},
|
|
168
197
|
"expiresAt": "2025-12-31T23:59:59Z"
|
|
169
198
|
}'
|
|
170
199
|
```
|
|
171
200
|
|
|
172
|
-
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)
|
|
173
206
|
|
|
174
207
|
### Retrieve a Credential
|
|
175
208
|
|
|
@@ -187,7 +220,7 @@ curl -X DELETE https://vault.example.com/api/v1/credentials/claude/prod \
|
|
|
187
220
|
|
|
188
221
|
## Auto-Refresh
|
|
189
222
|
|
|
190
|
-
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.
|
|
191
224
|
|
|
192
225
|
### Access Control Note
|
|
193
226
|
|
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { Command } from "@commander-js/extra-typings";
|
|
|
8
8
|
import packageJson from "../package.json" with { type: "json" };
|
|
9
9
|
import { handleCredentialDelete, handleCredentialList, } from "./commands/credential.js";
|
|
10
10
|
import { handleInit } from "./commands/init.js";
|
|
11
|
-
import { handleKeyCreate, handleKeyList, handleKeyRevoke, } from "./commands/key.js";
|
|
11
|
+
import { handleKeyCreate, handleKeyList, handleKeyRevoke, handleKeyUpdate, } from "./commands/key.js";
|
|
12
12
|
import { handleServe } from "./commands/serve.js";
|
|
13
13
|
const program = new Command()
|
|
14
14
|
.name(packageJson.name)
|
|
@@ -35,11 +35,14 @@ Examples:
|
|
|
35
35
|
axvault key create --name "Uploader" --write "claude/backups"
|
|
36
36
|
|
|
37
37
|
# Create an admin API key with full access
|
|
38
|
-
axvault key create --name "Admin" --read "*" --write "*"
|
|
38
|
+
axvault key create --name "Admin" --read "*" --write "*" --grant "*"
|
|
39
39
|
|
|
40
40
|
# List all API keys
|
|
41
41
|
axvault key list
|
|
42
42
|
|
|
43
|
+
# Update an API key's permissions
|
|
44
|
+
axvault key update k_abc123def456 --add-read "claude/new"
|
|
45
|
+
|
|
43
46
|
# Revoke an API key
|
|
44
47
|
axvault key revoke k_abc123def456
|
|
45
48
|
|
|
@@ -73,6 +76,7 @@ keyCommand
|
|
|
73
76
|
.requiredOption("-n, --name <name>", "Name for the API key")
|
|
74
77
|
.option("-r, --read <access>", "Comma-separated read access list (e.g., 'claude/work,codex/ci' or '*')")
|
|
75
78
|
.option("-w, --write <access>", "Comma-separated write access list (e.g., 'claude/ci' or '*')")
|
|
79
|
+
.option("-g, --grant <access>", "Comma-separated grant access list (can delegate these to other keys)")
|
|
76
80
|
.option("--json", "Output as JSON")
|
|
77
81
|
.option("--db-path <path>", "Database file path")
|
|
78
82
|
.action(handleKeyCreate);
|
|
@@ -88,6 +92,19 @@ keyCommand
|
|
|
88
92
|
.argument("<id>", "API key ID (e.g., k_abc123def456)")
|
|
89
93
|
.option("--db-path <path>", "Database file path")
|
|
90
94
|
.action(handleKeyRevoke);
|
|
95
|
+
keyCommand
|
|
96
|
+
.command("update")
|
|
97
|
+
.description("Update an API key's permissions")
|
|
98
|
+
.argument("<id>", "API key ID (e.g., k_abc123def456)")
|
|
99
|
+
.option("--add-read <access>", "Add read access entries")
|
|
100
|
+
.option("--add-write <access>", "Add write access entries")
|
|
101
|
+
.option("--add-grant <access>", "Add grant access entries")
|
|
102
|
+
.option("--remove-read <access>", "Remove read access entries")
|
|
103
|
+
.option("--remove-write <access>", "Remove write access entries")
|
|
104
|
+
.option("--remove-grant <access>", "Remove grant access entries")
|
|
105
|
+
.option("--json", "Output as JSON")
|
|
106
|
+
.option("--db-path <path>", "Database file path")
|
|
107
|
+
.action(handleKeyUpdate);
|
|
91
108
|
// Credential management commands
|
|
92
109
|
const credentialCommand = program
|
|
93
110
|
.command("credential")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API key create command handler.
|
|
3
|
+
*/
|
|
4
|
+
interface KeyCreateOptions {
|
|
5
|
+
dbPath?: string;
|
|
6
|
+
name: string;
|
|
7
|
+
read?: string;
|
|
8
|
+
write?: string;
|
|
9
|
+
grant?: string;
|
|
10
|
+
json?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function handleKeyCreate(options: KeyCreateOptions): void;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API key create command handler.
|
|
3
|
+
*/
|
|
4
|
+
import { getServerConfig } from "../config.js";
|
|
5
|
+
import { closeDatabase, getDatabase } from "../db/client.js";
|
|
6
|
+
import { runMigrations } from "../db/migrations.js";
|
|
7
|
+
import { createApiKey } from "../db/repositories/api-keys.js";
|
|
8
|
+
import { containsControlChars, formatAccessList, getAccessListErrorMessage, getErrorMessage, 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
|
+
const grantResult = parseAccessList(options.grant);
|
|
20
|
+
if (readResult.error) {
|
|
21
|
+
console.error(`Error: ${getAccessListErrorMessage(readResult.error)}`);
|
|
22
|
+
process.exitCode = 2;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (writeResult.error) {
|
|
26
|
+
console.error(`Error: ${getAccessListErrorMessage(writeResult.error)}`);
|
|
27
|
+
process.exitCode = 2;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (grantResult.error) {
|
|
31
|
+
console.error(`Error: ${getAccessListErrorMessage(grantResult.error)}`);
|
|
32
|
+
process.exitCode = 2;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// Normalize wildcards (warn if mixed with specific entries)
|
|
36
|
+
const readNorm = normalizeAccessList(readResult.entries, "read");
|
|
37
|
+
const writeNorm = normalizeAccessList(writeResult.entries, "write");
|
|
38
|
+
const grantNorm = normalizeAccessList(grantResult.entries, "grant");
|
|
39
|
+
if (readNorm.warning)
|
|
40
|
+
console.warn(`Warning: ${readNorm.warning}`);
|
|
41
|
+
if (writeNorm.warning)
|
|
42
|
+
console.warn(`Warning: ${writeNorm.warning}`);
|
|
43
|
+
if (grantNorm.warning)
|
|
44
|
+
console.warn(`Warning: ${grantNorm.warning}`);
|
|
45
|
+
const readAccess = readNorm.normalized;
|
|
46
|
+
const writeAccess = writeNorm.normalized;
|
|
47
|
+
const grantAccess = grantNorm.normalized;
|
|
48
|
+
// Validate: at least one access must be specified
|
|
49
|
+
if (readAccess.length === 0 &&
|
|
50
|
+
writeAccess.length === 0 &&
|
|
51
|
+
grantAccess.length === 0) {
|
|
52
|
+
console.error("Error: At least one of --read, --write, or --grant must be specified.");
|
|
53
|
+
console.error("Try 'axvault key create --help' for more information.");
|
|
54
|
+
process.exitCode = 2;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const config = getServerConfig(options);
|
|
59
|
+
const database = getDatabase(config.databasePath);
|
|
60
|
+
runMigrations(database);
|
|
61
|
+
const apiKey = createApiKey(database, {
|
|
62
|
+
name: options.name,
|
|
63
|
+
readAccess,
|
|
64
|
+
writeAccess,
|
|
65
|
+
grantAccess,
|
|
66
|
+
});
|
|
67
|
+
if (options.json) {
|
|
68
|
+
console.warn("Warning: JSON output contains the secret key. Avoid logging in CI.");
|
|
69
|
+
console.log(JSON.stringify({
|
|
70
|
+
id: apiKey.id,
|
|
71
|
+
name: apiKey.name,
|
|
72
|
+
key: apiKey.key,
|
|
73
|
+
readAccess: apiKey.readAccess,
|
|
74
|
+
writeAccess: apiKey.writeAccess,
|
|
75
|
+
grantAccess: apiKey.grantAccess,
|
|
76
|
+
createdAt: apiKey.createdAt.toISOString(),
|
|
77
|
+
}, undefined, 2));
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
console.error(`Created API key: ${sanitizeForTsv(apiKey.name)}`);
|
|
81
|
+
console.error(`ID: ${sanitizeForTsv(apiKey.id)}`);
|
|
82
|
+
console.error(`Read access: ${sanitizeForTsv(formatAccessList(apiKey.readAccess))}`);
|
|
83
|
+
console.error(`Write access: ${sanitizeForTsv(formatAccessList(apiKey.writeAccess))}`);
|
|
84
|
+
console.error(`Grant access: ${sanitizeForTsv(formatAccessList(apiKey.grantAccess))}`);
|
|
85
|
+
console.error("");
|
|
86
|
+
// Output the secret key to stdout for piping
|
|
87
|
+
console.log(apiKey.key);
|
|
88
|
+
console.error("");
|
|
89
|
+
console.error("Save this key securely - it cannot be retrieved later.");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
console.error(`Error: Failed to create API key: ${getErrorMessage(error)}`);
|
|
94
|
+
process.exitCode = 1;
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
closeDatabase();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API key list command handler.
|
|
3
|
+
*/
|
|
4
|
+
import { getServerConfig } from "../config.js";
|
|
5
|
+
import { closeDatabase, getDatabase } from "../db/client.js";
|
|
6
|
+
import { runMigrations } from "../db/migrations.js";
|
|
7
|
+
import { listApiKeys } from "../db/repositories/api-keys.js";
|
|
8
|
+
import { formatDateForJson, formatKeyRow, getErrorMessage, } from "../lib/format.js";
|
|
9
|
+
export function handleKeyList(options) {
|
|
10
|
+
try {
|
|
11
|
+
const config = getServerConfig(options);
|
|
12
|
+
const database = getDatabase(config.databasePath);
|
|
13
|
+
runMigrations(database);
|
|
14
|
+
const keys = listApiKeys(database);
|
|
15
|
+
if (options.json) {
|
|
16
|
+
const output = keys.map((key) => ({
|
|
17
|
+
id: key.id,
|
|
18
|
+
name: key.name,
|
|
19
|
+
readAccess: key.readAccess,
|
|
20
|
+
writeAccess: key.writeAccess,
|
|
21
|
+
grantAccess: key.grantAccess,
|
|
22
|
+
createdAt: key.createdAt.toISOString(),
|
|
23
|
+
lastUsedAt: formatDateForJson(key.lastUsedAt),
|
|
24
|
+
}));
|
|
25
|
+
console.log(JSON.stringify(output, undefined, 2));
|
|
26
|
+
}
|
|
27
|
+
else if (keys.length === 0) {
|
|
28
|
+
console.error("No API keys found.\nCreate one with: axvault key create --name <name> [--read <access>] [--write <access>] [--grant <access>]");
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
console.log("ID\tNAME\tREAD ACCESS\tWRITE ACCESS\tGRANT ACCESS\tLAST USED");
|
|
32
|
+
for (const key of keys)
|
|
33
|
+
console.log(formatKeyRow(key));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
console.error(`Error: Failed to list API keys: ${getErrorMessage(error)}`);
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
closeDatabase();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API key revoke command handler.
|
|
3
|
+
*/
|
|
4
|
+
import { getServerConfig } from "../config.js";
|
|
5
|
+
import { closeDatabase, getDatabase } from "../db/client.js";
|
|
6
|
+
import { runMigrations } from "../db/migrations.js";
|
|
7
|
+
import { deleteApiKey } from "../db/repositories/api-keys.js";
|
|
8
|
+
import { containsControlChars, getErrorMessage, isValidKeyId, sanitizeForTsv, } from "../lib/format.js";
|
|
9
|
+
export function handleKeyRevoke(id, options) {
|
|
10
|
+
// Reject inputs containing control characters BEFORE trimming
|
|
11
|
+
// (prevents silent bypass where \n or \t would be removed by trim)
|
|
12
|
+
if (containsControlChars(id)) {
|
|
13
|
+
console.error("Error: Invalid API key ID: contains control characters.");
|
|
14
|
+
console.error("API key IDs have format: k_<12 hex chars> (e.g., k_abc123def456)");
|
|
15
|
+
process.exitCode = 2;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
// Trim whitespace from ID (common copy-paste issue)
|
|
19
|
+
const trimmedId = id.trim();
|
|
20
|
+
// Validate ID format (k_ + 12 hex chars)
|
|
21
|
+
if (!isValidKeyId(trimmedId)) {
|
|
22
|
+
console.error(`Error: Invalid API key ID format: ${trimmedId}`);
|
|
23
|
+
console.error("API key IDs have format: k_<12 hex chars> (e.g., k_abc123def456)");
|
|
24
|
+
process.exitCode = 2;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const config = getServerConfig(options);
|
|
29
|
+
const database = getDatabase(config.databasePath);
|
|
30
|
+
runMigrations(database);
|
|
31
|
+
const deleted = deleteApiKey(database, trimmedId);
|
|
32
|
+
if (deleted) {
|
|
33
|
+
console.error(`Revoked API key: ${sanitizeForTsv(trimmedId)}`);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
console.error(`Error: API key not found: ${sanitizeForTsv(trimmedId)}`);
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
console.error(`Error: Failed to revoke API key: ${getErrorMessage(error)}`);
|
|
42
|
+
process.exitCode = 1;
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
closeDatabase();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API key update command handler.
|
|
3
|
+
*/
|
|
4
|
+
interface KeyUpdateOptions {
|
|
5
|
+
dbPath?: string;
|
|
6
|
+
addRead?: string;
|
|
7
|
+
addWrite?: string;
|
|
8
|
+
addGrant?: string;
|
|
9
|
+
removeRead?: string;
|
|
10
|
+
removeWrite?: string;
|
|
11
|
+
removeGrant?: string;
|
|
12
|
+
json?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function handleKeyUpdate(id: string, options: KeyUpdateOptions): void;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API key update command handler.
|
|
3
|
+
*/
|
|
4
|
+
import { getServerConfig } from "../config.js";
|
|
5
|
+
import { closeDatabase, getDatabase } from "../db/client.js";
|
|
6
|
+
import { runMigrations } from "../db/migrations.js";
|
|
7
|
+
import { findApiKeyById, updateApiKeyAccess, } from "../db/repositories/api-keys.js";
|
|
8
|
+
import { containsControlChars, formatAccessList, formatDateForJson, getErrorMessage, isValidKeyId, sanitizeForTsv, } from "../lib/format.js";
|
|
9
|
+
import { computeUpdatedAccess, normalizeAllAccess, parseAccessOptions, } from "../lib/parse-access-options.js";
|
|
10
|
+
export function handleKeyUpdate(id, options) {
|
|
11
|
+
// Validate key ID
|
|
12
|
+
if (containsControlChars(id)) {
|
|
13
|
+
console.error("Error: Invalid API key ID: contains control characters.");
|
|
14
|
+
console.error("API key IDs have format: k_<12 hex chars> (e.g., k_abc123def456)");
|
|
15
|
+
process.exitCode = 2;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const trimmedId = id.trim();
|
|
19
|
+
if (!isValidKeyId(trimmedId)) {
|
|
20
|
+
console.error(`Error: Invalid API key ID format: ${trimmedId}`);
|
|
21
|
+
console.error("API key IDs have format: k_<12 hex chars> (e.g., k_abc123def456)");
|
|
22
|
+
process.exitCode = 2;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
// Parse and validate access options
|
|
26
|
+
const parsed = parseAccessOptions(options);
|
|
27
|
+
if (!parsed)
|
|
28
|
+
return; // Error already logged
|
|
29
|
+
// Check if any updates were requested
|
|
30
|
+
const hasUpdates = parsed.addRead.length > 0 ||
|
|
31
|
+
parsed.addWrite.length > 0 ||
|
|
32
|
+
parsed.addGrant.length > 0 ||
|
|
33
|
+
parsed.removeRead.length > 0 ||
|
|
34
|
+
parsed.removeWrite.length > 0 ||
|
|
35
|
+
parsed.removeGrant.length > 0;
|
|
36
|
+
if (!hasUpdates) {
|
|
37
|
+
console.error("Error: No updates specified.");
|
|
38
|
+
console.error("Try 'axvault key update --help' for more information.");
|
|
39
|
+
process.exitCode = 2;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const config = getServerConfig(options);
|
|
44
|
+
const database = getDatabase(config.databasePath);
|
|
45
|
+
runMigrations(database);
|
|
46
|
+
// Find existing key
|
|
47
|
+
const existingKey = findApiKeyById(database, trimmedId);
|
|
48
|
+
if (!existingKey) {
|
|
49
|
+
console.error(`Error: API key not found: ${sanitizeForTsv(trimmedId)}`);
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// Compute new access lists
|
|
54
|
+
const newReadAccess = computeUpdatedAccess(existingKey.readAccess, parsed.addRead, parsed.removeRead);
|
|
55
|
+
const newWriteAccess = computeUpdatedAccess(existingKey.writeAccess, parsed.addWrite, parsed.removeWrite);
|
|
56
|
+
const newGrantAccess = computeUpdatedAccess(existingKey.grantAccess, parsed.addGrant, parsed.removeGrant);
|
|
57
|
+
// Normalize and update
|
|
58
|
+
const normalized = normalizeAllAccess(newReadAccess, newWriteAccess, newGrantAccess);
|
|
59
|
+
// Validate: at least one access must remain after update
|
|
60
|
+
if (normalized.read.length === 0 &&
|
|
61
|
+
normalized.write.length === 0 &&
|
|
62
|
+
normalized.grant.length === 0) {
|
|
63
|
+
console.error("Error: Update would leave the key with no permissions. At least one of read, write, or grant access must remain.");
|
|
64
|
+
process.exitCode = 2;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
updateApiKeyAccess(database, trimmedId, {
|
|
68
|
+
readAccess: normalized.read,
|
|
69
|
+
writeAccess: normalized.write,
|
|
70
|
+
grantAccess: normalized.grant,
|
|
71
|
+
});
|
|
72
|
+
// Fetch updated key for output
|
|
73
|
+
const updatedKey = findApiKeyById(database, trimmedId);
|
|
74
|
+
if (!updatedKey) {
|
|
75
|
+
console.error("Error: Failed to fetch updated key.");
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
outputKeyDetails(updatedKey, options.json ?? false);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
console.error(`Error: Failed to update API key: ${getErrorMessage(error)}`);
|
|
83
|
+
process.exitCode = 1;
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
closeDatabase();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Output key details in JSON or human-readable format */
|
|
90
|
+
function outputKeyDetails(key, json) {
|
|
91
|
+
if (json) {
|
|
92
|
+
console.log(JSON.stringify({
|
|
93
|
+
id: key.id,
|
|
94
|
+
name: key.name,
|
|
95
|
+
readAccess: key.readAccess,
|
|
96
|
+
writeAccess: key.writeAccess,
|
|
97
|
+
grantAccess: key.grantAccess,
|
|
98
|
+
createdAt: key.createdAt.toISOString(),
|
|
99
|
+
lastUsedAt: formatDateForJson(key.lastUsedAt),
|
|
100
|
+
}, undefined, 2));
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
console.error(`Updated API key: ${sanitizeForTsv(key.name)}`);
|
|
104
|
+
console.error(`ID: ${sanitizeForTsv(key.id)}`);
|
|
105
|
+
console.error(`Read access: ${sanitizeForTsv(formatAccessList(key.readAccess))}`);
|
|
106
|
+
console.error(`Write access: ${sanitizeForTsv(formatAccessList(key.writeAccess))}`);
|
|
107
|
+
console.error(`Grant access: ${sanitizeForTsv(formatAccessList(key.grantAccess))}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
package/dist/commands/key.d.ts
CHANGED
|
@@ -1,21 +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
|
-
write?: string;
|
|
9
|
-
json?: boolean;
|
|
10
|
-
}
|
|
11
|
-
interface KeyListOptions {
|
|
12
|
-
dbPath?: string;
|
|
13
|
-
json?: boolean;
|
|
14
|
-
}
|
|
15
|
-
interface KeyRevokeOptions {
|
|
16
|
-
dbPath?: string;
|
|
17
|
-
}
|
|
18
|
-
export declare function handleKeyCreate(options: KeyCreateOptions): void;
|
|
19
|
-
export declare function handleKeyList(options: KeyListOptions): void;
|
|
20
|
-
export declare function handleKeyRevoke(id: string, options: KeyRevokeOptions): void;
|
|
21
|
-
export {};
|
|
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";
|