axvault 1.0.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/LICENSE +21 -0
- package/README.md +213 -0
- package/bin/axvault +2 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +108 -0
- package/dist/commands/credential.d.ts +13 -0
- package/dist/commands/credential.js +113 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.js +50 -0
- package/dist/commands/key.d.ts +21 -0
- package/dist/commands/key.js +157 -0
- package/dist/commands/serve.d.ts +12 -0
- package/dist/commands/serve.js +93 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.js +44 -0
- package/dist/db/client.d.ts +13 -0
- package/dist/db/client.js +38 -0
- package/dist/db/migrations.d.ts +12 -0
- package/dist/db/migrations.js +96 -0
- package/dist/db/repositories/api-keys.d.ts +42 -0
- package/dist/db/repositories/api-keys.js +86 -0
- package/dist/db/repositories/audit-log.d.ts +37 -0
- package/dist/db/repositories/audit-log.js +58 -0
- package/dist/db/repositories/credentials.d.ts +48 -0
- package/dist/db/repositories/credentials.js +79 -0
- package/dist/db/types.d.ts +44 -0
- package/dist/db/types.js +4 -0
- package/dist/handlers/delete-credential.d.ts +15 -0
- package/dist/handlers/delete-credential.js +50 -0
- package/dist/handlers/get-credential.d.ts +21 -0
- package/dist/handlers/get-credential.js +143 -0
- package/dist/handlers/list-credentials.d.ts +12 -0
- package/dist/handlers/list-credentials.js +29 -0
- package/dist/handlers/put-credential.d.ts +15 -0
- package/dist/handlers/put-credential.js +94 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +14 -0
- package/dist/lib/encryption.d.ts +17 -0
- package/dist/lib/encryption.js +38 -0
- package/dist/lib/format.d.ts +92 -0
- package/dist/lib/format.js +216 -0
- package/dist/middleware/auth.d.ts +21 -0
- package/dist/middleware/auth.js +50 -0
- package/dist/middleware/validate-parameters.d.ts +10 -0
- package/dist/middleware/validate-parameters.js +26 -0
- package/dist/refresh/check-refresh.d.ts +40 -0
- package/dist/refresh/check-refresh.js +83 -0
- package/dist/refresh/log-refresh.d.ts +17 -0
- package/dist/refresh/log-refresh.js +35 -0
- package/dist/refresh/refresh-manager.d.ts +51 -0
- package/dist/refresh/refresh-manager.js +132 -0
- package/dist/server/routes.d.ts +12 -0
- package/dist/server/routes.js +44 -0
- package/dist/server/server.d.ts +18 -0
- package/dist/server/server.js +106 -0
- package/package.json +93 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Łukasz Jerciński
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# axvault
|
|
2
|
+
|
|
3
|
+
Remote credential storage server for a╳point.
|
|
4
|
+
|
|
5
|
+
## Configuration
|
|
6
|
+
|
|
7
|
+
### Environment Variables
|
|
8
|
+
|
|
9
|
+
| Variable | Description | Default |
|
|
10
|
+
| ---------------------------- | -------------------------------------------------------------------- | ------------------- |
|
|
11
|
+
| `AXVAULT_PORT` | Port to listen on | `3847` |
|
|
12
|
+
| `AXVAULT_HOST` | Host to bind to | `127.0.0.1` |
|
|
13
|
+
| `AXVAULT_DB_PATH` | Database file path | `./data/axvault.db` |
|
|
14
|
+
| `AXVAULT_ENCRYPTION_KEY` | Encryption key (min 32 chars, required) | — |
|
|
15
|
+
| `AXVAULT_REFRESH_THRESHOLD` | Refresh credentials expiring within this many seconds (0 to disable) | `3600` |
|
|
16
|
+
| `AXVAULT_REFRESH_TIMEOUT_MS` | Timeout for refresh operations in milliseconds | `30000` |
|
|
17
|
+
|
|
18
|
+
### CLI Flags
|
|
19
|
+
|
|
20
|
+
The `serve` command accepts flags that override environment variables:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
axvault serve \
|
|
24
|
+
--port 8080 \
|
|
25
|
+
--host 0.0.0.0 \
|
|
26
|
+
--db-path /data/vault.db \
|
|
27
|
+
--refresh-threshold 7200 \
|
|
28
|
+
--refresh-timeout 60000
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Setting `--refresh-threshold 0` disables automatic credential refresh.
|
|
32
|
+
|
|
33
|
+
## API Keys
|
|
34
|
+
|
|
35
|
+
API keys control access to the credential API. Each key has configurable read and write permissions.
|
|
36
|
+
|
|
37
|
+
### Create an API Key
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Full access
|
|
41
|
+
axvault key create --name "CI Pipeline" --read "*" --write "*"
|
|
42
|
+
|
|
43
|
+
# Restricted access
|
|
44
|
+
axvault key create --name "Claude Reader" --read "claude/*"
|
|
45
|
+
axvault key create --name "Deploy Script" --write "claude/prod,codex/prod"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The command outputs metadata to stderr and the secret key to stdout for easy piping:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
# stderr (visible in terminal):
|
|
52
|
+
Created API key: CI Pipeline
|
|
53
|
+
ID: k_a1b2c3d4e5f6
|
|
54
|
+
Read access: *
|
|
55
|
+
Write access: *
|
|
56
|
+
|
|
57
|
+
Save this key securely - it cannot be retrieved later.
|
|
58
|
+
|
|
59
|
+
# stdout (can be piped):
|
|
60
|
+
axv_sk_0123456789abcdef0123456789abcdef
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
To copy directly to clipboard: `axvault key create --name "My Key" --read "*" | pbcopy`
|
|
64
|
+
|
|
65
|
+
### List Keys
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
axvault key list
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Revoke a Key
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
axvault key revoke k_a1b2c3d4e5f6
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Container Deployments
|
|
78
|
+
|
|
79
|
+
#### Running the Container
|
|
80
|
+
|
|
81
|
+
The image uses an external UID pattern - no user is baked into the image. Specify the runtime user with `-u`/`--user`:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Docker
|
|
85
|
+
docker run -d \
|
|
86
|
+
--name axvault \
|
|
87
|
+
-p 3847:3847 \
|
|
88
|
+
-u 1000:1000 \
|
|
89
|
+
-e AXVAULT_ENCRYPTION_KEY="your-secret-key-minimum-32-chars!" \
|
|
90
|
+
-v /srv/axvault/data:/data \
|
|
91
|
+
ghcr.io/jercik/axvault:latest
|
|
92
|
+
|
|
93
|
+
# Podman
|
|
94
|
+
podman run -d \
|
|
95
|
+
--name axvault \
|
|
96
|
+
-p 3847:3847 \
|
|
97
|
+
--user 1000:1000 \
|
|
98
|
+
-e AXVAULT_ENCRYPTION_KEY="your-secret-key-minimum-32-chars!" \
|
|
99
|
+
-v /srv/axvault/data:/data:Z \
|
|
100
|
+
ghcr.io/jercik/axvault:latest
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### Volume Ownership
|
|
104
|
+
|
|
105
|
+
The data volume must be owned by the UID/GID the container runs as:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# Create directory and set ownership before first run
|
|
109
|
+
sudo mkdir -p /srv/axvault/data
|
|
110
|
+
sudo chown 1000:1000 /srv/axvault/data
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
For rootless Podman, use your user's UID or let Podman handle mapping automatically.
|
|
114
|
+
|
|
115
|
+
#### Quadlet (systemd)
|
|
116
|
+
|
|
117
|
+
Create `/etc/containers/systemd/axvault.container`:
|
|
118
|
+
|
|
119
|
+
```ini
|
|
120
|
+
[Unit]
|
|
121
|
+
Description=axvault credential server
|
|
122
|
+
|
|
123
|
+
[Container]
|
|
124
|
+
Image=ghcr.io/jercik/axvault:latest
|
|
125
|
+
PublishPort=3847:3847
|
|
126
|
+
User=1000
|
|
127
|
+
Group=1000
|
|
128
|
+
Environment=AXVAULT_ENCRYPTION_KEY=your-secret-key-minimum-32-chars!
|
|
129
|
+
Volume=/srv/axvault/data:/data:Z
|
|
130
|
+
|
|
131
|
+
[Service]
|
|
132
|
+
Restart=always
|
|
133
|
+
|
|
134
|
+
[Install]
|
|
135
|
+
WantedBy=multi-user.target
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Then reload and start:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
sudo systemctl daemon-reload
|
|
142
|
+
sudo systemctl start axvault
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### Managing Keys in Containers
|
|
146
|
+
|
|
147
|
+
Exec into the container to manage API keys:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
# Podman
|
|
151
|
+
sudo podman exec axvault /nodejs/bin/node node_modules/axvault/bin/axvault key create --name "My Key" --write "*"
|
|
152
|
+
|
|
153
|
+
# Docker
|
|
154
|
+
docker exec axvault /nodejs/bin/node node_modules/axvault/bin/axvault key create --name "My Key" --write "*"
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Credentials API
|
|
158
|
+
|
|
159
|
+
### Store a Credential
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
curl -X PUT https://vault.example.com/api/v1/credentials/claude/prod \
|
|
163
|
+
-H "Authorization: Bearer <api_key>" \
|
|
164
|
+
-H "Content-Type: application/json" \
|
|
165
|
+
-d '{
|
|
166
|
+
"data": {"accessToken": "...", "refreshToken": "..."},
|
|
167
|
+
"expiresAt": "2025-12-31T23:59:59Z"
|
|
168
|
+
}'
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Retrieve a Credential
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
curl https://vault.example.com/api/v1/credentials/claude/prod \
|
|
175
|
+
-H "Authorization: Bearer <api_key>"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Delete a Credential
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
curl -X DELETE https://vault.example.com/api/v1/credentials/claude/prod \
|
|
182
|
+
-H "Authorization: Bearer <api_key>"
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Auto-Refresh
|
|
186
|
+
|
|
187
|
+
axvault automatically refreshes OAuth credentials that are near expiration when they are retrieved. This behavior is controlled by the refresh threshold setting.
|
|
188
|
+
|
|
189
|
+
### Access Control Note
|
|
190
|
+
|
|
191
|
+
Auto-refresh is a server-side maintenance operation that occurs transparently during credential retrieval. Read-only API keys can trigger refresh because:
|
|
192
|
+
|
|
193
|
+
- The refresh uses the credential's own `refresh_token` (already authorized by the token owner)
|
|
194
|
+
- The credential's identity and ownership remain unchanged
|
|
195
|
+
- Only token values and expiry timestamps are updated
|
|
196
|
+
- This prevents wasteful repeated refreshes and rate limit issues
|
|
197
|
+
|
|
198
|
+
This follows the pattern used by credential vaults like HashiCorp Vault, where credential maintenance is handled transparently on reads.
|
|
199
|
+
|
|
200
|
+
### Response Headers
|
|
201
|
+
|
|
202
|
+
When retrieving credentials, the response may include these headers:
|
|
203
|
+
|
|
204
|
+
| Header | Value | Description |
|
|
205
|
+
| -------------------------- | ------ | ------------------------------------------------------------ |
|
|
206
|
+
| `X-Axvault-Refreshed` | `true` | Credential was successfully refreshed during this request |
|
|
207
|
+
| `X-Axvault-Refresh-Failed` | `true` | Refresh was attempted but failed; stale credentials returned |
|
|
208
|
+
|
|
209
|
+
When `X-Axvault-Refresh-Failed` is present, the response still returns HTTP 200 with the existing (potentially expired) credentials. Error details are logged to the audit log.
|
|
210
|
+
|
|
211
|
+
## License
|
|
212
|
+
|
|
213
|
+
MIT
|
package/bin/axvault
ADDED
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* axvault - Remote credential storage server for axpoint.
|
|
4
|
+
*
|
|
5
|
+
* Stores agent credentials and serves them via API.
|
|
6
|
+
*/
|
|
7
|
+
import { Command } from "@commander-js/extra-typings";
|
|
8
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
9
|
+
import { handleCredentialDelete, handleCredentialList, } from "./commands/credential.js";
|
|
10
|
+
import { handleInit } from "./commands/init.js";
|
|
11
|
+
import { handleKeyCreate, handleKeyList, handleKeyRevoke, } from "./commands/key.js";
|
|
12
|
+
import { handleServe } from "./commands/serve.js";
|
|
13
|
+
const program = new Command()
|
|
14
|
+
.name(packageJson.name)
|
|
15
|
+
.description(packageJson.description)
|
|
16
|
+
.version(packageJson.version, "-V, --version")
|
|
17
|
+
.showHelpAfterError("(add --help for additional information)")
|
|
18
|
+
.showSuggestionAfterError()
|
|
19
|
+
.helpCommand(false)
|
|
20
|
+
.addHelpText("after", String.raw `
|
|
21
|
+
Examples:
|
|
22
|
+
# Initialize database
|
|
23
|
+
axvault init
|
|
24
|
+
|
|
25
|
+
# Start server
|
|
26
|
+
axvault serve
|
|
27
|
+
|
|
28
|
+
# Start server on custom port
|
|
29
|
+
axvault serve --port 8080
|
|
30
|
+
|
|
31
|
+
# Create an API key with read access (at least one of --read/--write required)
|
|
32
|
+
axvault key create --name "CI Pipeline" --read "claude/work,codex/ci"
|
|
33
|
+
|
|
34
|
+
# Create a write-only API key
|
|
35
|
+
axvault key create --name "Uploader" --write "claude/backups"
|
|
36
|
+
|
|
37
|
+
# Create an admin API key with full access
|
|
38
|
+
axvault key create --name "Admin" --read "*" --write "*"
|
|
39
|
+
|
|
40
|
+
# List all API keys
|
|
41
|
+
axvault key list
|
|
42
|
+
|
|
43
|
+
# Revoke an API key
|
|
44
|
+
axvault key revoke k_abc123def456
|
|
45
|
+
|
|
46
|
+
# List all stored credentials
|
|
47
|
+
axvault credential list
|
|
48
|
+
|
|
49
|
+
# Delete a credential
|
|
50
|
+
axvault credential delete claude/work`);
|
|
51
|
+
program
|
|
52
|
+
.command("init")
|
|
53
|
+
.description("Initialize database and configuration")
|
|
54
|
+
.option("--db-path <path>", "Database file path")
|
|
55
|
+
.action(handleInit);
|
|
56
|
+
program
|
|
57
|
+
.command("serve")
|
|
58
|
+
.description("Start the vault server")
|
|
59
|
+
.option("-p, --port <port>", "Port to listen on")
|
|
60
|
+
.option("-H, --host <host>", "Host to bind to")
|
|
61
|
+
.option("--db-path <path>", "Database file path")
|
|
62
|
+
.option("--refresh-threshold <seconds>", "Refresh credentials expiring within this many seconds (0 to disable)")
|
|
63
|
+
.option("--refresh-timeout <ms>", "Timeout for refresh operations in milliseconds")
|
|
64
|
+
.action(handleServe);
|
|
65
|
+
// API key management commands
|
|
66
|
+
const keyCommand = program
|
|
67
|
+
.command("key")
|
|
68
|
+
.description("Manage API keys")
|
|
69
|
+
.helpCommand(false);
|
|
70
|
+
keyCommand
|
|
71
|
+
.command("create")
|
|
72
|
+
.description("Create a new API key")
|
|
73
|
+
.requiredOption("-n, --name <name>", "Name for the API key")
|
|
74
|
+
.option("-r, --read <access>", "Comma-separated read access list (e.g., 'claude/work,codex/ci' or '*')")
|
|
75
|
+
.option("-w, --write <access>", "Comma-separated write access list (e.g., 'claude/ci' or '*')")
|
|
76
|
+
.option("--json", "Output as JSON")
|
|
77
|
+
.option("--db-path <path>", "Database file path")
|
|
78
|
+
.action(handleKeyCreate);
|
|
79
|
+
keyCommand
|
|
80
|
+
.command("list")
|
|
81
|
+
.description("List all API keys")
|
|
82
|
+
.option("--json", "Output as JSON")
|
|
83
|
+
.option("--db-path <path>", "Database file path")
|
|
84
|
+
.action(handleKeyList);
|
|
85
|
+
keyCommand
|
|
86
|
+
.command("revoke")
|
|
87
|
+
.description("Revoke an API key")
|
|
88
|
+
.argument("<id>", "API key ID (e.g., k_abc123def456)")
|
|
89
|
+
.option("--db-path <path>", "Database file path")
|
|
90
|
+
.action(handleKeyRevoke);
|
|
91
|
+
// Credential management commands
|
|
92
|
+
const credentialCommand = program
|
|
93
|
+
.command("credential")
|
|
94
|
+
.description("Manage stored credentials")
|
|
95
|
+
.helpCommand(false);
|
|
96
|
+
credentialCommand
|
|
97
|
+
.command("list")
|
|
98
|
+
.description("List all stored credentials")
|
|
99
|
+
.option("--json", "Output as JSON")
|
|
100
|
+
.option("--db-path <path>", "Database file path")
|
|
101
|
+
.action(handleCredentialList);
|
|
102
|
+
credentialCommand
|
|
103
|
+
.command("delete")
|
|
104
|
+
.description("Delete a credential")
|
|
105
|
+
.argument("<path>", "Credential path (agent/name, e.g., claude/work)")
|
|
106
|
+
.option("--db-path <path>", "Database file path")
|
|
107
|
+
.action(handleCredentialDelete);
|
|
108
|
+
await program.parseAsync(process.argv);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential management command handlers.
|
|
3
|
+
*/
|
|
4
|
+
interface CredentialListOptions {
|
|
5
|
+
dbPath?: string;
|
|
6
|
+
json?: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface CredentialDeleteOptions {
|
|
9
|
+
dbPath?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function handleCredentialList(options: CredentialListOptions): void;
|
|
12
|
+
export declare function handleCredentialDelete(path: string, options: CredentialDeleteOptions): void;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential management command handlers.
|
|
3
|
+
*/
|
|
4
|
+
import { getServerConfig } from "../config.js";
|
|
5
|
+
import { closeDatabase, getDatabase } from "../db/client.js";
|
|
6
|
+
import { runMigrations } from "../db/migrations.js";
|
|
7
|
+
import { deleteCredential, listCredentials, } from "../db/repositories/credentials.js";
|
|
8
|
+
import { containsControlChars, formatDateForJson, formatExpiresAt, formatRelativeTime, getErrorMessage, sanitizeForTsv, } from "../lib/format.js";
|
|
9
|
+
/** Print credentials as TSV table */
|
|
10
|
+
function printCredentialTable(credentials) {
|
|
11
|
+
console.log("AGENT\tNAME\tEXPIRES\tUPDATED");
|
|
12
|
+
for (const cred of credentials) {
|
|
13
|
+
const agent = sanitizeForTsv(cred.agent);
|
|
14
|
+
const name = sanitizeForTsv(cred.name);
|
|
15
|
+
const expires = formatExpiresAt(cred.expiresAt);
|
|
16
|
+
const updated = formatRelativeTime(cred.updatedAt);
|
|
17
|
+
console.log(`${agent}\t${name}\t${expires}\t${updated}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Parse credential path (agent/name).
|
|
22
|
+
*
|
|
23
|
+
* Control characters are checked BEFORE trimming to prevent silent bypass
|
|
24
|
+
* (e.g., "\nclaude/work" would otherwise become "claude/work" after trim).
|
|
25
|
+
*/
|
|
26
|
+
function parseCredentialPath(path) {
|
|
27
|
+
// Reject inputs containing control characters BEFORE trimming
|
|
28
|
+
if (containsControlChars(path)) {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
message: `Invalid credential path: contains control characters.\nExpected format: agent/name (e.g., claude/work)`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const trimmed = path.trim();
|
|
35
|
+
const parts = trimmed.split("/");
|
|
36
|
+
if (parts.length !== 2) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
message: `Invalid credential path: ${trimmed}\nExpected format: agent/name (e.g., claude/work)`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const agent = parts[0]?.trim();
|
|
43
|
+
const name = parts[1]?.trim();
|
|
44
|
+
if (!agent || !name) {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
message: `Invalid credential path: ${trimmed}\nBoth agent and name are required.`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return { ok: true, agent, name };
|
|
51
|
+
}
|
|
52
|
+
export function handleCredentialList(options) {
|
|
53
|
+
try {
|
|
54
|
+
const config = getServerConfig(options);
|
|
55
|
+
const database = getDatabase(config.databasePath);
|
|
56
|
+
runMigrations(database);
|
|
57
|
+
const credentials = listCredentials(database);
|
|
58
|
+
if (options.json) {
|
|
59
|
+
const output = credentials.map((cred) => ({
|
|
60
|
+
agent: cred.agent,
|
|
61
|
+
name: cred.name,
|
|
62
|
+
createdAt: cred.createdAt.toISOString(),
|
|
63
|
+
updatedAt: cred.updatedAt.toISOString(),
|
|
64
|
+
expiresAt: formatDateForJson(cred.expiresAt),
|
|
65
|
+
}));
|
|
66
|
+
console.log(JSON.stringify(output, undefined, 2));
|
|
67
|
+
}
|
|
68
|
+
else if (credentials.length === 0) {
|
|
69
|
+
console.error("No credentials stored.");
|
|
70
|
+
console.error("Store credentials via the API: PUT /api/v1/credentials/:agent/:name");
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
printCredentialTable(credentials);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
console.error(`Error: Failed to list credentials: ${getErrorMessage(error)}`);
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
closeDatabase();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export function handleCredentialDelete(path, options) {
|
|
85
|
+
// Parse credential path first (before config parsing)
|
|
86
|
+
const parsed = parseCredentialPath(path);
|
|
87
|
+
if (!parsed.ok) {
|
|
88
|
+
console.error(`Error: ${parsed.message}`);
|
|
89
|
+
process.exitCode = 2;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const { agent, name } = parsed;
|
|
93
|
+
try {
|
|
94
|
+
const config = getServerConfig(options);
|
|
95
|
+
const database = getDatabase(config.databasePath);
|
|
96
|
+
runMigrations(database);
|
|
97
|
+
const deleted = deleteCredential(database, agent, name);
|
|
98
|
+
if (deleted) {
|
|
99
|
+
console.error(`Deleted credential: ${sanitizeForTsv(agent)}/${sanitizeForTsv(name)}`);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
console.error(`Error: Credential not found: ${sanitizeForTsv(agent)}/${sanitizeForTsv(name)}`);
|
|
103
|
+
process.exitCode = 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
console.error(`Error: Failed to delete credential: ${getErrorMessage(error)}`);
|
|
108
|
+
process.exitCode = 1;
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
closeDatabase();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Initialize database command handler.
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { getServerConfig } from "../config.js";
|
|
7
|
+
import { closeDatabase, getDatabase } from "../db/client.js";
|
|
8
|
+
import { CURRENT_VERSION, getSchemaVersion, runMigrations, } from "../db/migrations.js";
|
|
9
|
+
export function handleInit(options) {
|
|
10
|
+
const config = getServerConfig(options);
|
|
11
|
+
// Ensure data directory exists
|
|
12
|
+
const dataDirectory = path.dirname(config.databasePath);
|
|
13
|
+
if (!existsSync(dataDirectory)) {
|
|
14
|
+
try {
|
|
15
|
+
mkdirSync(dataDirectory, { recursive: true });
|
|
16
|
+
console.error(`Created data directory: ${dataDirectory}`);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
20
|
+
console.error(`Failed to create data directory '${dataDirectory}': ${message}`);
|
|
21
|
+
process.exitCode = 1;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// Initialize database
|
|
26
|
+
try {
|
|
27
|
+
const database = getDatabase(config.databasePath);
|
|
28
|
+
const versionBefore = getSchemaVersion(database);
|
|
29
|
+
runMigrations(database);
|
|
30
|
+
const versionAfter = getSchemaVersion(database);
|
|
31
|
+
if (versionBefore === 0) {
|
|
32
|
+
console.error(`Database initialized at: ${config.databasePath}`);
|
|
33
|
+
console.error(`Schema version: ${versionAfter}`);
|
|
34
|
+
}
|
|
35
|
+
else if (versionBefore < versionAfter) {
|
|
36
|
+
console.error(`Database migrated from v${versionBefore} to v${versionAfter}`);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
console.error(`Database already at version ${versionAfter} (current: ${CURRENT_VERSION})`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
44
|
+
console.error(`Failed to initialize database: ${message}`);
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
closeDatabase();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API key management command handlers.
|
|
3
|
+
*/
|
|
4
|
+
interface KeyCreateOptions {
|
|
5
|
+
dbPath?: string;
|
|
6
|
+
name: string;
|
|
7
|
+
read?: string;
|
|
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 {};
|