axvault 1.3.0 → 1.4.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.
@@ -16,6 +16,8 @@ export function handleKeyList(options) {
16
16
  const output = keys.map((key) => ({
17
17
  id: key.id,
18
18
  name: key.name,
19
+ // eslint-disable-next-line unicorn/no-null -- JSON requires null for missing values
20
+ keyPrefix: key.keyPrefix ?? null,
19
21
  readAccess: key.readAccess,
20
22
  writeAccess: key.writeAccess,
21
23
  grantAccess: key.grantAccess,
@@ -28,7 +30,7 @@ export function handleKeyList(options) {
28
30
  console.error("No API keys found.\nCreate one with: axvault key create --name <name> [--read <access>] [--write <access>] [--grant <access>]");
29
31
  }
30
32
  else {
31
- console.log("ID\tNAME\tREAD ACCESS\tWRITE ACCESS\tGRANT ACCESS\tLAST USED");
33
+ console.log("ID\tNAME\tKEY\tREAD ACCESS\tWRITE ACCESS\tGRANT ACCESS\tLAST USED");
32
34
  for (const key of keys)
33
35
  console.log(formatKeyRow(key));
34
36
  }
@@ -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 = 5;
7
+ declare const CURRENT_VERSION = 6;
8
8
  /** Run all pending migrations */
9
9
  declare function runMigrations(database: Database.Database): void;
10
10
  /** Get current schema version */
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Uses a simple version-based migration system.
5
5
  */
6
- const CURRENT_VERSION = 5;
6
+ const CURRENT_VERSION = 6;
7
7
  /** Run all pending migrations */
8
8
  function runMigrations(database) {
9
9
  const version = getSchemaVersion(database);
@@ -22,6 +22,9 @@ function runMigrations(database) {
22
22
  if (version < 5) {
23
23
  migrateToV5(database);
24
24
  }
25
+ if (version < 6) {
26
+ migrateToV6(database);
27
+ }
25
28
  }
26
29
  /** Get current schema version */
27
30
  function getSchemaVersion(database) {
@@ -148,4 +151,18 @@ function migrateToV5(database) {
148
151
  setSchemaVersion(database, 5);
149
152
  })();
150
153
  }
154
+ /**
155
+ * Migration to version 6: Add key_prefix column to api_keys
156
+ *
157
+ * Stores first 8 characters of the key secret for identification.
158
+ * Existing keys will have NULL prefix (key value not recoverable).
159
+ */
160
+ function migrateToV6(database) {
161
+ database.transaction(() => {
162
+ database.exec(`
163
+ ALTER TABLE api_keys ADD COLUMN key_prefix TEXT
164
+ `);
165
+ setSchemaVersion(database, 6);
166
+ })();
167
+ }
151
168
  export { CURRENT_VERSION, getSchemaVersion, runMigrations };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * API key utility functions.
3
+ *
4
+ * Pure functions for key generation and hashing.
5
+ */
6
+ /** Generate a new API key ID */
7
+ declare function generateKeyId(): string;
8
+ /** Generate a new API key secret */
9
+ declare function generateKeySecret(): string;
10
+ /** Hash an API key for storage */
11
+ declare function hashApiKey(key: string): string;
12
+ /** Extract key prefix for display (axv_sk_ + first 8 hex chars of secret) */
13
+ declare function extractKeyPrefix(key: string): string;
14
+ /** Minimal interface for access checking */
15
+ interface AccessLists {
16
+ readAccess: string[];
17
+ writeAccess: string[];
18
+ grantAccess: string[];
19
+ }
20
+ /** Check if API key has read access to a credential */
21
+ declare function hasReadAccess(apiKey: AccessLists, agent: string, name: string): boolean;
22
+ /** Check if API key has write access to a credential */
23
+ declare function hasWriteAccess(apiKey: AccessLists, agent: string, name: string): boolean;
24
+ /** Check if API key has grant access to a credential */
25
+ declare function hasGrantAccess(apiKey: AccessLists, agent: string, name: string): boolean;
26
+ export { extractKeyPrefix, generateKeyId, generateKeySecret, hashApiKey, hasGrantAccess, hasReadAccess, hasWriteAccess, };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * API key utility functions.
3
+ *
4
+ * Pure functions for key generation and hashing.
5
+ */
6
+ import { createHash, randomBytes } from "node:crypto";
7
+ /** Key prefix for identification (e.g., "axv_sk_01234567") */
8
+ const KEY_PREFIX_LENGTH = 8;
9
+ /** Generate a new API key ID */
10
+ function generateKeyId() {
11
+ return `k_${randomBytes(6).toString("hex")}`;
12
+ }
13
+ /** Generate a new API key secret */
14
+ function generateKeySecret() {
15
+ return `axv_sk_${randomBytes(16).toString("hex")}`;
16
+ }
17
+ /** Hash an API key for storage */
18
+ function hashApiKey(key) {
19
+ return createHash("sha256").update(key).digest("hex");
20
+ }
21
+ /** Extract key prefix for display (axv_sk_ + first 8 hex chars of secret) */
22
+ function extractKeyPrefix(key) {
23
+ // Key format: axv_sk_ + 32 hex chars
24
+ return key.slice(0, 7 + KEY_PREFIX_LENGTH); // "axv_sk_" (7) + 8 chars
25
+ }
26
+ /** Check if access list includes the given credential path */
27
+ function hasAccess(accessList, agent, name) {
28
+ const path = `${agent}/${name}`;
29
+ return accessList.includes("*") || accessList.includes(path);
30
+ }
31
+ /** Check if API key has read access to a credential */
32
+ function hasReadAccess(apiKey, agent, name) {
33
+ return hasAccess(apiKey.readAccess, agent, name);
34
+ }
35
+ /** Check if API key has write access to a credential */
36
+ function hasWriteAccess(apiKey, agent, name) {
37
+ return hasAccess(apiKey.writeAccess, agent, name);
38
+ }
39
+ /** Check if API key has grant access to a credential */
40
+ function hasGrantAccess(apiKey, agent, name) {
41
+ return hasAccess(apiKey.grantAccess, agent, name);
42
+ }
43
+ export { extractKeyPrefix, generateKeyId, generateKeySecret, hashApiKey, hasGrantAccess, hasReadAccess, hasWriteAccess, };
@@ -9,6 +9,7 @@ interface ApiKeyRecord {
9
9
  id: string;
10
10
  name: string;
11
11
  keyHash: string;
12
+ keyPrefix: string | undefined;
12
13
  readAccess: string[];
13
14
  writeAccess: string[];
14
15
  grantAccess: string[];
@@ -36,17 +37,12 @@ declare function listApiKeys(database: Database.Database): ApiKeyRecord[];
36
37
  declare function updateLastUsed(database: Database.Database, id: string): void;
37
38
  /** Delete an API key */
38
39
  declare function deleteApiKey(database: Database.Database, id: string): boolean;
39
- /** Check if API key has read access to a credential */
40
- declare function hasReadAccess(apiKey: ApiKeyRecord, agent: string, name: string): boolean;
41
- /** Check if API key has write access to a credential */
42
- declare function hasWriteAccess(apiKey: ApiKeyRecord, agent: string, name: string): boolean;
43
- /** Check if API key has grant access to a credential */
44
- declare function hasGrantAccess(apiKey: ApiKeyRecord, agent: string, name: string): boolean;
45
40
  /** Update an API key's access permissions */
46
41
  declare function updateApiKeyAccess(database: Database.Database, id: string, options: {
47
42
  readAccess?: string[];
48
43
  writeAccess?: string[];
49
44
  grantAccess?: string[];
50
45
  }): boolean;
51
- export { createApiKey, deleteApiKey, findApiKeyById, findApiKeyByKey, hasGrantAccess, hasReadAccess, hasWriteAccess, listApiKeys, updateApiKeyAccess, updateLastUsed, };
46
+ export { createApiKey, deleteApiKey, findApiKeyById, findApiKeyByKey, listApiKeys, updateApiKeyAccess, updateLastUsed, };
47
+ export { hasGrantAccess, hasReadAccess, hasWriteAccess, } from "./api-key-utilities.js";
52
48
  export type { ApiKeyRecord, ApiKeyWithSecret };
@@ -3,13 +3,14 @@
3
3
  *
4
4
  * Manages API keys with read/write access lists for credentials.
5
5
  */
6
- import { createHash, randomBytes } from "node:crypto";
6
+ import { extractKeyPrefix, generateKeyId, generateKeySecret, hashApiKey, } from "./api-key-utilities.js";
7
7
  /** Convert database row to record */
8
8
  function rowToRecord(row) {
9
9
  return {
10
10
  id: row.id,
11
11
  name: row.name,
12
12
  keyHash: row.key_hash,
13
+ keyPrefix: row.key_prefix ?? undefined,
13
14
  readAccess: JSON.parse(row.read_access),
14
15
  writeAccess: JSON.parse(row.write_access),
15
16
  grantAccess: JSON.parse(row.grant_access),
@@ -17,26 +18,24 @@ function rowToRecord(row) {
17
18
  lastUsedAt: row.last_used_at ? new Date(row.last_used_at) : undefined,
18
19
  };
19
20
  }
20
- /** Hash an API key for storage */
21
- function hashApiKey(key) {
22
- return createHash("sha256").update(key).digest("hex");
23
- }
24
- const SELECT_COLUMNS = `id, name, key_hash, read_access, write_access, grant_access, created_at, last_used_at`;
21
+ const SELECT_COLUMNS = `id, name, key_hash, key_prefix, read_access, write_access, grant_access, created_at, last_used_at`;
25
22
  /** Create a new API key */
26
23
  function createApiKey(database, options) {
27
- const id = `k_${randomBytes(6).toString("hex")}`;
28
- const key = `axv_sk_${randomBytes(16).toString("hex")}`;
24
+ const id = generateKeyId();
25
+ const key = generateKeySecret();
29
26
  const keyHash = hashApiKey(key);
27
+ const keyPrefix = extractKeyPrefix(key);
30
28
  const now = Date.now();
31
29
  database
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);
30
+ .prepare(`INSERT INTO api_keys (id, name, key_hash, key_prefix, read_access, write_access, grant_access, created_at)
31
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
32
+ .run(id, options.name, keyHash, keyPrefix, JSON.stringify(options.readAccess), JSON.stringify(options.writeAccess), JSON.stringify(options.grantAccess), now);
35
33
  return {
36
34
  id,
37
35
  name: options.name,
38
36
  key,
39
37
  keyHash,
38
+ keyPrefix,
40
39
  readAccess: options.readAccess,
41
40
  writeAccess: options.writeAccess,
42
41
  grantAccess: options.grantAccess,
@@ -75,21 +74,6 @@ function updateLastUsed(database, id) {
75
74
  function deleteApiKey(database, id) {
76
75
  return (database.prepare(`DELETE FROM api_keys WHERE id = ?`).run(id).changes > 0);
77
76
  }
78
- /** Check if API key has read access to a credential */
79
- function hasReadAccess(apiKey, agent, name) {
80
- const path = `${agent}/${name}`;
81
- return apiKey.readAccess.includes("*") || apiKey.readAccess.includes(path);
82
- }
83
- /** Check if API key has write access to a credential */
84
- function hasWriteAccess(apiKey, agent, name) {
85
- const path = `${agent}/${name}`;
86
- return apiKey.writeAccess.includes("*") || apiKey.writeAccess.includes(path);
87
- }
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
77
  /** Update an API key's access permissions */
94
78
  function updateApiKeyAccess(database, id, options) {
95
79
  const updates = [];
@@ -112,4 +96,5 @@ function updateApiKeyAccess(database, id, options) {
112
96
  const sql = `UPDATE api_keys SET ${updates.join(", ")} WHERE id = ?`;
113
97
  return database.prepare(sql).run(...values).changes > 0;
114
98
  }
115
- export { createApiKey, deleteApiKey, findApiKeyById, findApiKeyByKey, hasGrantAccess, hasReadAccess, hasWriteAccess, listApiKeys, updateApiKeyAccess, updateLastUsed, };
99
+ export { createApiKey, deleteApiKey, findApiKeyById, findApiKeyByKey, listApiKeys, updateApiKeyAccess, updateLastUsed, };
100
+ export { hasGrantAccess, hasReadAccess, hasWriteAccess, } from "./api-key-utilities.js";
@@ -6,6 +6,7 @@ export interface ApiKeyRow {
6
6
  id: string;
7
7
  name: string;
8
8
  key_hash: string;
9
+ key_prefix: string | null;
9
10
  read_access: string;
10
11
  write_access: string;
11
12
  grant_access: string;
@@ -37,6 +37,7 @@ export declare function isValidKeyId(id: string): boolean;
37
37
  export declare function formatKeyRow(key: {
38
38
  id: string;
39
39
  name: string;
40
+ keyPrefix?: string;
40
41
  readAccess: string[];
41
42
  writeAccess: string[];
42
43
  grantAccess: string[];
@@ -66,17 +66,27 @@ const KEY_ID_PATTERN = /^k_[0-9a-f]{12}$/u;
66
66
  export function isValidKeyId(id) {
67
67
  return KEY_ID_PATTERN.test(id);
68
68
  }
69
+ /**
70
+ * Format key prefix for display with redaction suffix.
71
+ * Returns "(unavailable)" for keys created before prefix storage.
72
+ */
73
+ function formatKeyPrefix(prefix) {
74
+ if (!prefix)
75
+ return "(unavailable)";
76
+ return `${prefix}...`;
77
+ }
69
78
  /**
70
79
  * Format a single API key row for TSV output.
71
80
  */
72
81
  export function formatKeyRow(key) {
73
82
  const id = sanitizeForTsv(key.id);
74
83
  const name = sanitizeForTsv(key.name);
84
+ const keyPrefix = sanitizeForTsv(formatKeyPrefix(key.keyPrefix));
75
85
  const readAccess = sanitizeForTsv(formatAccessList(key.readAccess));
76
86
  const writeAccess = sanitizeForTsv(formatAccessList(key.writeAccess));
77
87
  const grantAccess = sanitizeForTsv(formatAccessList(key.grantAccess));
78
88
  const lastUsed = formatRelativeTime(key.lastUsedAt);
79
- return `${id}\t${name}\t${readAccess}\t${writeAccess}\t${grantAccess}\t${lastUsed}`;
89
+ return `${id}\t${name}\t${keyPrefix}\t${readAccess}\t${writeAccess}\t${grantAccess}\t${lastUsed}`;
80
90
  }
81
91
  /**
82
92
  * Validate access list entry format.
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.3.0",
5
+ "version": "1.4.0",
6
6
  "description": "Remote credential storage server for axkit",
7
7
  "repository": {
8
8
  "type": "git",