axvault 1.1.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 +34 -5
- 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 +18 -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/types.d.ts +1 -0
- 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/package.json +1 -1
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
|
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";
|
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);
|
|
@@ -16,6 +16,9 @@ function runMigrations(database) {
|
|
|
16
16
|
if (version < 3) {
|
|
17
17
|
migrateToV3(database);
|
|
18
18
|
}
|
|
19
|
+
if (version < 4) {
|
|
20
|
+
migrateToV4(database);
|
|
21
|
+
}
|
|
19
22
|
}
|
|
20
23
|
/** Get current schema version */
|
|
21
24
|
function getSchemaVersion(database) {
|
|
@@ -110,4 +113,18 @@ function migrateToV3(database) {
|
|
|
110
113
|
setSchemaVersion(database, 3);
|
|
111
114
|
})();
|
|
112
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
|
+
}
|
|
113
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;
|
package/dist/db/types.d.ts
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 {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse and validate access list options from CLI flags.
|
|
3
|
+
*/
|
|
4
|
+
import { getAccessListErrorMessage, normalizeAccessList, parseAccessList, } from "./format.js";
|
|
5
|
+
/**
|
|
6
|
+
* Parse all access list options and validate them.
|
|
7
|
+
* Returns parsed entries or logs error and sets exit code.
|
|
8
|
+
*/
|
|
9
|
+
export function parseAccessOptions(options) {
|
|
10
|
+
const addReadResult = parseAccessList(options.addRead);
|
|
11
|
+
const addWriteResult = parseAccessList(options.addWrite);
|
|
12
|
+
const addGrantResult = parseAccessList(options.addGrant);
|
|
13
|
+
const removeReadResult = parseAccessList(options.removeRead);
|
|
14
|
+
const removeWriteResult = parseAccessList(options.removeWrite);
|
|
15
|
+
const removeGrantResult = parseAccessList(options.removeGrant);
|
|
16
|
+
if (addReadResult.error) {
|
|
17
|
+
console.error(`Error in --add-read: ${getAccessListErrorMessage(addReadResult.error)}`);
|
|
18
|
+
process.exitCode = 2;
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
if (addWriteResult.error) {
|
|
22
|
+
console.error(`Error in --add-write: ${getAccessListErrorMessage(addWriteResult.error)}`);
|
|
23
|
+
process.exitCode = 2;
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
if (addGrantResult.error) {
|
|
27
|
+
console.error(`Error in --add-grant: ${getAccessListErrorMessage(addGrantResult.error)}`);
|
|
28
|
+
process.exitCode = 2;
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
if (removeReadResult.error) {
|
|
32
|
+
console.error(`Error in --remove-read: ${getAccessListErrorMessage(removeReadResult.error)}`);
|
|
33
|
+
process.exitCode = 2;
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
if (removeWriteResult.error) {
|
|
37
|
+
console.error(`Error in --remove-write: ${getAccessListErrorMessage(removeWriteResult.error)}`);
|
|
38
|
+
process.exitCode = 2;
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
if (removeGrantResult.error) {
|
|
42
|
+
console.error(`Error in --remove-grant: ${getAccessListErrorMessage(removeGrantResult.error)}`);
|
|
43
|
+
process.exitCode = 2;
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
addRead: addReadResult.entries,
|
|
48
|
+
addWrite: addWriteResult.entries,
|
|
49
|
+
addGrant: addGrantResult.entries,
|
|
50
|
+
removeRead: removeReadResult.entries,
|
|
51
|
+
removeWrite: removeWriteResult.entries,
|
|
52
|
+
removeGrant: removeGrantResult.entries,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Compute updated access list by adding and removing entries.
|
|
57
|
+
*/
|
|
58
|
+
export function computeUpdatedAccess(current, toAdd, toRemove) {
|
|
59
|
+
const set = new Set(current);
|
|
60
|
+
for (const entry of toAdd)
|
|
61
|
+
set.add(entry);
|
|
62
|
+
for (const entry of toRemove)
|
|
63
|
+
set.delete(entry);
|
|
64
|
+
return [...set];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Normalize all access lists and print warnings.
|
|
68
|
+
*/
|
|
69
|
+
export function normalizeAllAccess(readAccess, writeAccess, grantAccess) {
|
|
70
|
+
const readNorm = normalizeAccessList(readAccess, "read");
|
|
71
|
+
const writeNorm = normalizeAccessList(writeAccess, "write");
|
|
72
|
+
const grantNorm = normalizeAccessList(grantAccess, "grant");
|
|
73
|
+
if (readNorm.warning)
|
|
74
|
+
console.warn(`Warning: ${readNorm.warning}`);
|
|
75
|
+
if (writeNorm.warning)
|
|
76
|
+
console.warn(`Warning: ${writeNorm.warning}`);
|
|
77
|
+
if (grantNorm.warning)
|
|
78
|
+
console.warn(`Warning: ${grantNorm.warning}`);
|
|
79
|
+
return {
|
|
80
|
+
read: readNorm.normalized,
|
|
81
|
+
write: writeNorm.normalized,
|
|
82
|
+
grant: grantNorm.normalized,
|
|
83
|
+
};
|
|
84
|
+
}
|