axvault 1.6.0 → 1.7.1

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 CHANGED
@@ -2,6 +2,57 @@
2
2
 
3
3
  Remote credential storage server for a╳kit.
4
4
 
5
+ ## Quick start
6
+
7
+ ```bash
8
+ export AXVAULT_ENCRYPTION_KEY="your-secret-key-minimum-32-chars!"
9
+ axvault init
10
+ axvault serve
11
+ ```
12
+
13
+ In another shell, create an API key:
14
+
15
+ ```bash
16
+ axvault key create --name "Admin" --read "*" --write "*" --grant "*"
17
+ ```
18
+
19
+ Add `--verbose` to commands like `init`, `serve`, `key revoke`, `key update`, and
20
+ `credential delete` to see status output.
21
+
22
+ ## Pipeline examples
23
+
24
+ ### Extract key IDs
25
+
26
+ ```bash
27
+ axvault key list | tail -n +2 | cut -f1
28
+ ```
29
+
30
+ ### Count credentials by type
31
+
32
+ ```bash
33
+ axvault credential list | tail -n +2 | cut -f3 | sort | uniq -c | sort -rn
34
+ ```
35
+
36
+ ### List credential paths for a single agent
37
+
38
+ ```bash
39
+ axvault credential list | tail -n +2 | awk -F'\t' '$1 == "claude" {print $1 "/" $2}'
40
+ ```
41
+
42
+ ## Agent Rule
43
+
44
+ Add to your `CLAUDE.md` or `AGENTS.md`:
45
+
46
+ ```markdown
47
+ # Rule: `axvault` Usage
48
+
49
+ Run `npx -y axvault --help` to learn available options.
50
+
51
+ Use `axvault` when you need to initialize the vault database, manage API keys,
52
+ or list/delete stored credentials. It outputs TSV tables for list commands, so
53
+ you can pipe them into standard Unix tools.
54
+ ```
55
+
5
56
  ## Configuration
6
57
 
7
58
  ### Environment Variables
@@ -100,9 +151,11 @@ axvault key update k_a1b2c3d4e5f6 --add-read "codex/ci" --remove-write "claude/d
100
151
  ### Revoke a Key
101
152
 
102
153
  ```bash
103
- axvault key revoke k_a1b2c3d4e5f6
154
+ axvault key revoke k_a1b2c3d4e5f6 --force
104
155
  ```
105
156
 
157
+ This command requires `--force` or `--yes` to confirm.
158
+
106
159
  ### Container Deployments
107
160
 
108
161
  #### Running the Container
@@ -222,13 +275,13 @@ curl -X DELETE https://vault.example.com/api/v1/credentials/claude/prod \
222
275
 
223
276
  ## Auto-Refresh
224
277
 
225
- 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.
278
+ 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 containing a refresh token field are eligible for auto-refresh. Supported field names: `refresh_token` (standard OAuth), `refreshToken` (Claude), `refresh` (OpenCode).
226
279
 
227
280
  ### Access Control Note
228
281
 
229
282
  Auto-refresh is a server-side maintenance operation that occurs transparently during credential retrieval. Read-only API keys can trigger refresh because:
230
283
 
231
- - The refresh uses the credential's own `refresh_token` (already authorized by the token owner)
284
+ - The refresh uses the credential's own refresh token (already authorized by the token owner)
232
285
  - The credential's identity and ownership remain unchanged
233
286
  - Only token values and expiry timestamps are updated
234
287
  - This prevents wasteful repeated refreshes and rate limit issues
package/dist/cli.js CHANGED
@@ -40,21 +40,25 @@ Examples:
40
40
  # List all API keys
41
41
  axvault key list
42
42
 
43
+ # Extract key IDs for scripting (pipeline example)
44
+ axvault key list | tail -n +2 | cut -f1
45
+
43
46
  # Update an API key's permissions
44
47
  axvault key update k_abc123def456 --add-read "claude/new"
45
48
 
46
49
  # Revoke an API key
47
- axvault key revoke k_abc123def456
50
+ axvault key revoke k_abc123def456 --force
48
51
 
49
52
  # List all stored credentials
50
53
  axvault credential list
51
54
 
52
55
  # Delete a credential
53
- axvault credential delete claude/work`);
56
+ axvault credential delete claude/work --force`);
54
57
  program
55
58
  .command("init")
56
59
  .description("Initialize database and configuration")
57
60
  .option("--db-path <path>", "Database file path")
61
+ .option("-v, --verbose", "Enable verbose output")
58
62
  .action(handleInit);
59
63
  program
60
64
  .command("serve")
@@ -64,6 +68,7 @@ program
64
68
  .option("--db-path <path>", "Database file path")
65
69
  .option("--refresh-threshold <seconds>", "Refresh credentials expiring within this many seconds (0 to disable)")
66
70
  .option("--refresh-timeout <ms>", "Timeout for refresh operations in milliseconds")
71
+ .option("-v, --verbose", "Enable verbose output")
67
72
  .action(handleServe);
68
73
  // API key management commands
69
74
  const keyCommand = program
@@ -90,6 +95,9 @@ keyCommand
90
95
  .command("revoke")
91
96
  .description("Revoke an API key")
92
97
  .argument("<id>", "API key ID (e.g., k_abc123def456)")
98
+ .option("-f, --force", "Confirm destructive action")
99
+ .option("-y, --yes", "Alias for --force")
100
+ .option("-v, --verbose", "Enable verbose output")
93
101
  .option("--db-path <path>", "Database file path")
94
102
  .action(handleKeyRevoke);
95
103
  keyCommand
@@ -103,6 +111,7 @@ keyCommand
103
111
  .option("--remove-write <access>", "Remove write access entries")
104
112
  .option("--remove-grant <access>", "Remove grant access entries")
105
113
  .option("--json", "Output as JSON")
114
+ .option("-v, --verbose", "Enable verbose output")
106
115
  .option("--db-path <path>", "Database file path")
107
116
  .action(handleKeyUpdate);
108
117
  // Credential management commands
@@ -120,6 +129,9 @@ credentialCommand
120
129
  .command("delete")
121
130
  .description("Delete a credential")
122
131
  .argument("<path>", "Credential path (agent/name, e.g., claude/work)")
132
+ .option("-f, --force", "Confirm destructive action")
133
+ .option("-y, --yes", "Alias for --force")
134
+ .option("-v, --verbose", "Enable verbose output")
123
135
  .option("--db-path <path>", "Database file path")
124
136
  .action(handleCredentialDelete);
125
137
  await program.parseAsync(process.argv);
@@ -7,6 +7,9 @@ interface CredentialListOptions {
7
7
  }
8
8
  interface CredentialDeleteOptions {
9
9
  dbPath?: string;
10
+ force?: boolean;
11
+ yes?: boolean;
12
+ verbose?: boolean;
10
13
  }
11
14
  export declare function handleCredentialList(options: CredentialListOptions): void;
12
15
  export declare function handleCredentialDelete(path: string, options: CredentialDeleteOptions): void;
@@ -91,6 +91,12 @@ export function handleCredentialDelete(path, options) {
91
91
  process.exitCode = 2;
92
92
  return;
93
93
  }
94
+ if (!options.force && !options.yes) {
95
+ console.error("Error: Deleting a credential is destructive. Re-run with --force or --yes to confirm.");
96
+ console.error("Try 'axvault credential delete --help' for more information.");
97
+ process.exitCode = 2;
98
+ return;
99
+ }
94
100
  const { agent, name } = parsed;
95
101
  try {
96
102
  const config = getServerConfig(options);
@@ -98,7 +104,9 @@ export function handleCredentialDelete(path, options) {
98
104
  runMigrations(database);
99
105
  const deleted = deleteCredential(database, agent, name);
100
106
  if (deleted) {
101
- console.error(`Deleted credential: ${sanitizeForTsv(agent)}/${sanitizeForTsv(name)}`);
107
+ if (options.verbose) {
108
+ console.error(`Deleted credential: ${sanitizeForTsv(agent)}/${sanitizeForTsv(name)}`);
109
+ }
102
110
  }
103
111
  else {
104
112
  console.error(`Error: Credential not found: ${sanitizeForTsv(agent)}/${sanitizeForTsv(name)}`);
@@ -3,6 +3,7 @@
3
3
  */
4
4
  interface InitOptions {
5
5
  dbPath?: string;
6
+ verbose?: boolean;
6
7
  }
7
8
  export declare function handleInit(options: InitOptions): void;
8
9
  export {};
@@ -7,13 +7,15 @@ import { getServerConfig } from "../config.js";
7
7
  import { closeDatabase, getDatabase } from "../db/client.js";
8
8
  import { CURRENT_VERSION, getSchemaVersion, runMigrations, } from "../db/migrations.js";
9
9
  export function handleInit(options) {
10
- const config = getServerConfig(options);
10
+ const config = getServerConfig({ dbPath: options.dbPath });
11
+ const verbose = options.verbose === true;
11
12
  // Ensure data directory exists
12
13
  const dataDirectory = path.dirname(config.databasePath);
13
14
  if (!existsSync(dataDirectory)) {
14
15
  try {
15
16
  mkdirSync(dataDirectory, { recursive: true });
16
- console.error(`Created data directory: ${dataDirectory}`);
17
+ if (verbose)
18
+ console.error(`Created data directory: ${dataDirectory}`);
17
19
  }
18
20
  catch (error) {
19
21
  const message = error instanceof Error ? error.message : String(error);
@@ -28,15 +30,17 @@ export function handleInit(options) {
28
30
  const versionBefore = getSchemaVersion(database);
29
31
  runMigrations(database);
30
32
  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})`);
33
+ if (verbose) {
34
+ if (versionBefore === 0) {
35
+ console.error(`Database initialized at: ${config.databasePath}`);
36
+ console.error(`Schema version: ${versionAfter}`);
37
+ }
38
+ else if (versionBefore < versionAfter) {
39
+ console.error(`Database migrated from v${versionBefore} to v${versionAfter}`);
40
+ }
41
+ else {
42
+ console.error(`Database already at version ${versionAfter} (current: ${CURRENT_VERSION})`);
43
+ }
40
44
  }
41
45
  }
42
46
  catch (error) {
@@ -3,6 +3,9 @@
3
3
  */
4
4
  interface KeyRevokeOptions {
5
5
  dbPath?: string;
6
+ force?: boolean;
7
+ yes?: boolean;
8
+ verbose?: boolean;
6
9
  }
7
10
  export declare function handleKeyRevoke(id: string, options: KeyRevokeOptions): void;
8
11
  export {};
@@ -24,13 +24,21 @@ export function handleKeyRevoke(id, options) {
24
24
  process.exitCode = 2;
25
25
  return;
26
26
  }
27
+ if (!options.force && !options.yes) {
28
+ console.error("Error: Revoking an API key is destructive. Re-run with --force or --yes to confirm.");
29
+ console.error("Try 'axvault key revoke --help' for more information.");
30
+ process.exitCode = 2;
31
+ return;
32
+ }
27
33
  try {
28
34
  const config = getServerConfig(options);
29
35
  const database = getDatabase(config.databasePath);
30
36
  runMigrations(database);
31
37
  const deleted = deleteApiKey(database, trimmedId);
32
38
  if (deleted) {
33
- console.error(`Revoked API key: ${sanitizeForTsv(trimmedId)}`);
39
+ if (options.verbose) {
40
+ console.error(`Revoked API key: ${sanitizeForTsv(trimmedId)}`);
41
+ }
34
42
  }
35
43
  else {
36
44
  console.error(`Error: API key not found: ${sanitizeForTsv(trimmedId)}`);
@@ -10,6 +10,7 @@ interface KeyUpdateOptions {
10
10
  removeWrite?: string;
11
11
  removeGrant?: string;
12
12
  json?: boolean;
13
+ verbose?: boolean;
13
14
  }
14
15
  export declare function handleKeyUpdate(id: string, options: KeyUpdateOptions): void;
15
16
  export {};
@@ -76,7 +76,7 @@ export function handleKeyUpdate(id, options) {
76
76
  process.exitCode = 1;
77
77
  return;
78
78
  }
79
- outputKeyDetails(updatedKey, options.json ?? false);
79
+ outputKeyDetails(updatedKey, options.json ?? false, options.verbose ?? false);
80
80
  }
81
81
  catch (error) {
82
82
  console.error(`Error: Failed to update API key: ${getErrorMessage(error)}`);
@@ -87,7 +87,7 @@ export function handleKeyUpdate(id, options) {
87
87
  }
88
88
  }
89
89
  /** Output key details in JSON or human-readable format */
90
- function outputKeyDetails(key, json) {
90
+ function outputKeyDetails(key, json, verbose) {
91
91
  if (json) {
92
92
  console.log(JSON.stringify({
93
93
  id: key.id,
@@ -99,7 +99,7 @@ function outputKeyDetails(key, json) {
99
99
  lastUsedAt: formatDateForJson(key.lastUsedAt),
100
100
  }, undefined, 2));
101
101
  }
102
- else {
102
+ else if (verbose) {
103
103
  console.error(`Updated API key: ${sanitizeForTsv(key.name)}`);
104
104
  console.error(`ID: ${sanitizeForTsv(key.id)}`);
105
105
  console.error(`Read access: ${sanitizeForTsv(formatAccessList(key.readAccess))}`);
@@ -7,6 +7,7 @@ interface ServeOptions {
7
7
  dbPath?: string;
8
8
  refreshThreshold?: string;
9
9
  refreshTimeout?: string;
10
+ verbose?: boolean;
10
11
  }
11
12
  export declare function handleServe(options: ServeOptions): Promise<void>;
12
13
  export {};
@@ -10,9 +10,12 @@ import { createHealthRouter, createCredentialRouter, } from "../server/routes.js
10
10
  import { createServer } from "../server/server.js";
11
11
  export async function handleServe(options) {
12
12
  let config;
13
+ const verbose = options.verbose === true;
13
14
  try {
14
15
  config = getServerConfig({
15
- ...options,
16
+ port: options.port,
17
+ host: options.host,
18
+ dbPath: options.dbPath,
16
19
  refreshThresholdSeconds: options.refreshThreshold,
17
20
  refreshTimeoutMs: options.refreshTimeout,
18
21
  });
@@ -35,7 +38,8 @@ export async function handleServe(options) {
35
38
  if (!existsSync(dataDirectory)) {
36
39
  try {
37
40
  mkdirSync(dataDirectory, { recursive: true });
38
- console.error(`Created data directory: ${dataDirectory}`);
41
+ if (verbose)
42
+ console.error(`Created data directory: ${dataDirectory}`);
39
43
  }
40
44
  catch (error) {
41
45
  const message = error instanceof Error ? error.message : String(error);
@@ -66,7 +70,8 @@ export async function handleServe(options) {
66
70
  ]);
67
71
  // Graceful shutdown handler
68
72
  const shutdown = () => {
69
- console.error("Shutting down...");
73
+ if (verbose)
74
+ console.error("Shutting down...");
70
75
  server.stop().then(() => {
71
76
  closeDatabase();
72
77
  // eslint-disable-next-line unicorn/no-process-exit -- CLI graceful shutdown
@@ -4,26 +4,11 @@
4
4
  * Extracted from refresh-manager to reduce complexity.
5
5
  */
6
6
  import { type Credentials } from "axauth";
7
- /**
8
- * Check if credential is refreshable (has refresh_token or refresh).
9
- *
10
- * Detects credentials that have a refresh token field. This is a local
11
- * check only - the actual refresh is performed by axauth which spawns
12
- * the agent subprocess. The agent handles its own credential format
13
- * (e.g., OpenCode uses `refresh` instead of `refresh_token`).
14
- *
15
- * Returns false for non-object data to avoid TypeError from `in` operator.
16
- *
17
- * Supports both standard OAuth field names and OpenCode field names:
18
- * - `refresh_token`: Standard OAuth
19
- * - `refresh`: OpenCode format
20
- */
21
- declare function isRefreshable(data: unknown): data is Record<string, unknown>;
22
7
  /**
23
8
  * Check if credential needs refresh based on expiry.
24
9
  *
25
10
  * Only returns true if:
26
- * 1. Credential data is an object with refresh_token (required for axauth refresh)
11
+ * 1. Credential data is an object with refresh token (required for axauth refresh)
27
12
  * 2. Credential has expiry info that is within threshold
28
13
  *
29
14
  * @param data - Decrypted credential data (accepts unknown for safety)
@@ -35,17 +20,9 @@ declare function needsRefresh(data: unknown, thresholdSeconds: number): boolean;
35
20
  * Map vault credential data to axauth Credentials type.
36
21
  *
37
22
  * @param agent - Agent CLI name
38
- * @param data - Decrypted credential data from vault (must have refresh_token)
23
+ * @param data - Decrypted credential data from vault (must have refresh token)
39
24
  * @returns Credentials object for axauth, or undefined if not mappable
40
25
  */
41
26
  declare function toAxauthCredentials(agent: string, data: Record<string, unknown>): Credentials | undefined;
42
- /**
43
- * Extract expiry date from refreshed credentials.
44
- *
45
- * Looks for common expiry fields in order of preference:
46
- * - `expiry_date`: Google OAuth (milliseconds)
47
- * - `expires_at`: Generic OAuth (seconds or milliseconds)
48
- * - `expires`: OpenCode format (milliseconds)
49
- */
50
- declare function extractExpiryDate(data: Record<string, unknown>): Date | undefined;
51
- export { extractExpiryDate, isRefreshable, needsRefresh, toAxauthCredentials };
27
+ export { extractExpiryDate, isRefreshable } from "axauth";
28
+ export { needsRefresh, toAxauthCredentials };
@@ -4,42 +4,14 @@
4
4
  * Extracted from refresh-manager to reduce complexity.
5
5
  */
6
6
  import { AGENT_CLIS } from "axshared";
7
- import { isCredentialExpired } from "axauth";
7
+ import { isCredentialExpired, isRefreshable } from "axauth";
8
8
  /** Valid agent CLI names - sourced from axshared for consistency */
9
9
  const VALID_AGENTS = new Set(AGENT_CLIS);
10
- /**
11
- * Check if credential is refreshable (has refresh_token or refresh).
12
- *
13
- * Detects credentials that have a refresh token field. This is a local
14
- * check only - the actual refresh is performed by axauth which spawns
15
- * the agent subprocess. The agent handles its own credential format
16
- * (e.g., OpenCode uses `refresh` instead of `refresh_token`).
17
- *
18
- * Returns false for non-object data to avoid TypeError from `in` operator.
19
- *
20
- * Supports both standard OAuth field names and OpenCode field names:
21
- * - `refresh_token`: Standard OAuth
22
- * - `refresh`: OpenCode format
23
- */
24
- function isRefreshable(data) {
25
- if (typeof data !== "object" || data === null)
26
- return false;
27
- const record = data;
28
- // Check for standard OAuth field name
29
- if ("refresh_token" in record && typeof record.refresh_token === "string") {
30
- return true;
31
- }
32
- // Check for OpenCode field name
33
- if ("refresh" in record && typeof record.refresh === "string") {
34
- return true;
35
- }
36
- return false;
37
- }
38
10
  /**
39
11
  * Check if credential needs refresh based on expiry.
40
12
  *
41
13
  * Only returns true if:
42
- * 1. Credential data is an object with refresh_token (required for axauth refresh)
14
+ * 1. Credential data is an object with refresh token (required for axauth refresh)
43
15
  * 2. Credential has expiry info that is within threshold
44
16
  *
45
17
  * @param data - Decrypted credential data (accepts unknown for safety)
@@ -47,7 +19,7 @@ function isRefreshable(data) {
47
19
  * @returns true if refresh needed, false otherwise
48
20
  */
49
21
  function needsRefresh(data, thresholdSeconds) {
50
- // Must be an object with refresh_token to be refreshable
22
+ // Must be an object with refresh token to be refreshable
51
23
  if (!isRefreshable(data))
52
24
  return false;
53
25
  const expired = isCredentialExpired(data, thresholdSeconds);
@@ -58,7 +30,7 @@ function needsRefresh(data, thresholdSeconds) {
58
30
  * Map vault credential data to axauth Credentials type.
59
31
  *
60
32
  * @param agent - Agent CLI name
61
- * @param data - Decrypted credential data from vault (must have refresh_token)
33
+ * @param data - Decrypted credential data from vault (must have refresh token)
62
34
  * @returns Credentials object for axauth, or undefined if not mappable
63
35
  */
64
36
  function toAxauthCredentials(agent, data) {
@@ -72,35 +44,6 @@ function toAxauthCredentials(agent, data) {
72
44
  data,
73
45
  };
74
46
  }
75
- /**
76
- * Timestamp threshold to distinguish seconds from milliseconds.
77
- * Values below this are assumed to be Unix timestamps in seconds.
78
- */
79
- const SECONDS_THRESHOLD = 10_000_000_000;
80
- /**
81
- * Extract expiry date from refreshed credentials.
82
- *
83
- * Looks for common expiry fields in order of preference:
84
- * - `expiry_date`: Google OAuth (milliseconds)
85
- * - `expires_at`: Generic OAuth (seconds or milliseconds)
86
- * - `expires`: OpenCode format (milliseconds)
87
- */
88
- function extractExpiryDate(data) {
89
- // Google OAuth uses expiry_date (milliseconds)
90
- if (typeof data.expiry_date === "number") {
91
- return new Date(data.expiry_date);
92
- }
93
- // Generic OAuth uses expires_at (seconds or milliseconds)
94
- if (typeof data.expires_at === "number") {
95
- const ts = data.expires_at < SECONDS_THRESHOLD
96
- ? data.expires_at * 1000
97
- : data.expires_at;
98
- return new Date(ts);
99
- }
100
- // OpenCode uses expires (milliseconds)
101
- if (typeof data.expires === "number") {
102
- return new Date(data.expires);
103
- }
104
- return undefined;
105
- }
106
- export { extractExpiryDate, isRefreshable, needsRefresh, toAxauthCredentials };
47
+ // Re-export from axauth for convenience
48
+ export { extractExpiryDate, isRefreshable } from "axauth";
49
+ export { needsRefresh, toAxauthCredentials };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "axvault",
3
3
  "author": "Łukasz Jerciński",
4
4
  "license": "MIT",
5
- "version": "1.6.0",
5
+ "version": "1.7.1",
6
6
  "description": "Remote credential storage server for axkit",
7
7
  "repository": {
8
8
  "type": "git",
@@ -49,9 +49,9 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@commander-js/extra-typings": "^14.0.0",
52
- "axauth": "^1.11.2",
53
- "axshared": "1.9.0",
54
- "better-sqlite3": "^12.6.0",
52
+ "axauth": "^2.1.0",
53
+ "axshared": "2.0.0",
54
+ "better-sqlite3": "^12.6.2",
55
55
  "commander": "^14.0.2",
56
56
  "express": "^5.2.1"
57
57
  },
@@ -78,14 +78,14 @@
78
78
  "@total-typescript/ts-reset": "^0.6.1",
79
79
  "@types/better-sqlite3": "^7.6.13",
80
80
  "@types/express": "^5.0.6",
81
- "@types/node": "^25.0.8",
81
+ "@types/node": "^25.0.9",
82
82
  "@vitest/coverage-v8": "^4.0.17",
83
83
  "eslint": "^9.39.2",
84
- "eslint-config-axkit": "^1.0.0",
84
+ "eslint-config-axkit": "^1.1.0",
85
85
  "fta-check": "^1.5.1",
86
86
  "fta-cli": "^3.0.0",
87
87
  "knip": "^5.81.0",
88
- "prettier": "3.7.4",
88
+ "prettier": "3.8.0",
89
89
  "semantic-release": "^25.0.2",
90
90
  "typescript": "^5.9.3",
91
91
  "vitest": "^4.0.17"