axvault 1.2.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.
package/README.md CHANGED
@@ -192,13 +192,17 @@ curl -X PUT https://vault.example.com/api/v1/credentials/claude/prod \
192
192
  -H "Authorization: Bearer <api_key>" \
193
193
  -H "Content-Type: application/json" \
194
194
  -d '{
195
- "type": "oauth",
195
+ "type": "oauth-credentials",
196
196
  "data": {"access_token": "...", "refresh_token": "..."},
197
197
  "expiresAt": "2025-12-31T23:59:59Z"
198
198
  }'
199
199
  ```
200
200
 
201
- The `type` field is required and must be either `"oauth"` (for refreshable tokens) or `"api-key"` (for static keys). Only `oauth` credentials are eligible for auto-refresh.
201
+ The `type` field is required and must be one of:
202
+
203
+ - `"oauth-credentials"` — Full OAuth with refresh_token (eligible for auto-refresh)
204
+ - `"oauth-token"` — Long-lived OAuth token like `CLAUDE_CODE_OAUTH_TOKEN` (static)
205
+ - `"api-key"` — API key (static)
202
206
 
203
207
  ### Retrieve a Credential
204
208
 
@@ -216,7 +220,7 @@ curl -X DELETE https://vault.example.com/api/v1/credentials/claude/prod \
216
220
 
217
221
  ## Auto-Refresh
218
222
 
219
- axvault automatically refreshes OAuth credentials that are near expiration when they are retrieved. This behavior is controlled by the refresh threshold setting.
223
+ axvault automatically refreshes `oauth-credentials` type credentials that are near expiration when they are retrieved. This behavior is controlled by the refresh threshold setting. Only credentials with a `refresh_token` in their data are eligible for auto-refresh.
220
224
 
221
225
  ### Access Control Note
222
226
 
@@ -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 = 4;
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 = 4;
6
+ const CURRENT_VERSION = 6;
7
7
  /** Run all pending migrations */
8
8
  function runMigrations(database) {
9
9
  const version = getSchemaVersion(database);
@@ -19,6 +19,12 @@ function runMigrations(database) {
19
19
  if (version < 4) {
20
20
  migrateToV4(database);
21
21
  }
22
+ if (version < 5) {
23
+ migrateToV5(database);
24
+ }
25
+ if (version < 6) {
26
+ migrateToV6(database);
27
+ }
22
28
  }
23
29
  /** Get current schema version */
24
30
  function getSchemaVersion(database) {
@@ -127,4 +133,36 @@ function migrateToV4(database) {
127
133
  setSchemaVersion(database, 4);
128
134
  })();
129
135
  }
136
+ /**
137
+ * Migration to version 5: Rename credential type "oauth" to "oauth-credentials"
138
+ *
139
+ * Distinguishes refreshable OAuth credentials (oauth-credentials) from
140
+ * long-lived OAuth tokens like CLAUDE_CODE_OAUTH_TOKEN (oauth-token).
141
+ *
142
+ * - `oauth-credentials`: Full OAuth flow with accessToken, refreshToken, expiresAt (refreshable)
143
+ * - `oauth-token`: Long-lived OAuth token for CI/CD (static, no refresh)
144
+ * - `api-key`: API key (static)
145
+ */
146
+ function migrateToV5(database) {
147
+ database.transaction(() => {
148
+ database.exec(`
149
+ UPDATE credentials SET type = 'oauth-credentials' WHERE type = 'oauth'
150
+ `);
151
+ setSchemaVersion(database, 5);
152
+ })();
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
+ }
130
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";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * SQL queries for credentials repository.
3
+ */
4
+ export declare const UPSERT_CREDENTIAL = "\n INSERT INTO credentials (agent, name, type, encrypted_data, salt, iv, auth_tag, created_at, updated_at, expires_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(agent, name) DO UPDATE SET\n type = excluded.type, encrypted_data = excluded.encrypted_data, salt = excluded.salt, iv = excluded.iv, auth_tag = excluded.auth_tag,\n updated_at = excluded.updated_at, expires_at = excluded.expires_at";
5
+ export declare const SELECT_CREDENTIAL = "\n SELECT agent, name, type, encrypted_data, salt, iv, auth_tag, created_at, updated_at, expires_at\n FROM credentials WHERE agent = ? AND name = ?";
6
+ export declare const SELECT_ALL_METADATA = "\n SELECT agent, name, type, created_at, updated_at, expires_at\n FROM credentials ORDER BY agent, name";
7
+ export declare const DELETE_CREDENTIAL = "\n DELETE FROM credentials WHERE agent = ? AND name = ?";
8
+ export declare const UPDATE_EXPIRES_AT = "\n UPDATE credentials SET expires_at = ?, updated_at = ? WHERE agent = ? AND name = ?";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * SQL queries for credentials repository.
3
+ */
4
+ export const UPSERT_CREDENTIAL = `
5
+ INSERT INTO credentials (agent, name, type, encrypted_data, salt, iv, auth_tag, created_at, updated_at, expires_at)
6
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
7
+ ON CONFLICT(agent, name) DO UPDATE SET
8
+ type = excluded.type, encrypted_data = excluded.encrypted_data, salt = excluded.salt, iv = excluded.iv, auth_tag = excluded.auth_tag,
9
+ updated_at = excluded.updated_at, expires_at = excluded.expires_at`;
10
+ export const SELECT_CREDENTIAL = `
11
+ SELECT agent, name, type, encrypted_data, salt, iv, auth_tag, created_at, updated_at, expires_at
12
+ FROM credentials WHERE agent = ? AND name = ?`;
13
+ export const SELECT_ALL_METADATA = `
14
+ SELECT agent, name, type, created_at, updated_at, expires_at
15
+ FROM credentials ORDER BY agent, name`;
16
+ export const DELETE_CREDENTIAL = `
17
+ DELETE FROM credentials WHERE agent = ? AND name = ?`;
18
+ export const UPDATE_EXPIRES_AT = `
19
+ UPDATE credentials SET expires_at = ?, updated_at = ? WHERE agent = ? AND name = ?`;
@@ -4,30 +4,8 @@
4
4
  * Manages encrypted credential storage.
5
5
  */
6
6
  import type Database from "better-sqlite3";
7
- /** Valid credential types */
8
- type CredentialType = "oauth" | "api-key";
9
- /** Credential record stored in database */
10
- interface CredentialRecord {
11
- agent: string;
12
- name: string;
13
- type: CredentialType;
14
- encryptedData: Buffer;
15
- salt: Buffer;
16
- iv: Buffer;
17
- authTag: Buffer;
18
- createdAt: Date;
19
- updatedAt: Date;
20
- expiresAt: Date | undefined;
21
- }
22
- /** Credential metadata (without encrypted data) */
23
- interface CredentialMetadata {
24
- agent: string;
25
- name: string;
26
- type: CredentialType;
27
- createdAt: Date;
28
- updatedAt: Date;
29
- expiresAt: Date | undefined;
30
- }
7
+ import type { CredentialType } from "axshared";
8
+ import { type CredentialMetadata, type CredentialRecord } from "./parse-credential-row.js";
31
9
  /** Store or update a credential */
32
10
  declare function upsertCredential(database: Database.Database, credential: {
33
11
  agent: string;
@@ -50,4 +28,4 @@ declare function deleteCredential(database: Database.Database, agent: string, na
50
28
  /** Update expiration time after refresh */
51
29
  declare function updateExpiresAt(database: Database.Database, agent: string, name: string, expiresAt: Date | undefined): void;
52
30
  export { deleteCredential, getCredential, listCredentials, listCredentialsForApiKey, updateExpiresAt, upsertCredential, };
53
- export type { CredentialMetadata, CredentialRecord, CredentialType };
31
+ export type { CredentialMetadata, CredentialRecord, } from "./parse-credential-row.js";
@@ -3,64 +3,25 @@
3
3
  *
4
4
  * Manages encrypted credential storage.
5
5
  */
6
- /** Validate credential type from database */
7
- function validateType(type) {
8
- if (type !== "oauth" && type !== "api-key") {
9
- throw new Error(`Invalid credential type: ${type}`);
10
- }
11
- return type;
12
- }
13
- /** Convert database row to record */
14
- function rowToRecord(row) {
15
- return {
16
- agent: row.agent,
17
- name: row.name,
18
- type: validateType(row.type),
19
- encryptedData: row.encrypted_data,
20
- salt: row.salt,
21
- iv: row.iv,
22
- authTag: row.auth_tag,
23
- createdAt: new Date(row.created_at),
24
- updatedAt: new Date(row.updated_at),
25
- expiresAt: row.expires_at ? new Date(row.expires_at) : undefined,
26
- };
27
- }
28
- /** Convert metadata row to metadata */
29
- function rowToMetadata(row) {
30
- return {
31
- agent: row.agent,
32
- name: row.name,
33
- type: validateType(row.type),
34
- createdAt: new Date(row.created_at),
35
- updatedAt: new Date(row.updated_at),
36
- expiresAt: row.expires_at ? new Date(row.expires_at) : undefined,
37
- };
38
- }
6
+ import * as SQL from "./credentials-queries.js";
7
+ import { rowToMetadata, rowToRecord, } from "./parse-credential-row.js";
39
8
  /** Store or update a credential */
40
9
  function upsertCredential(database, credential) {
41
10
  const now = Date.now();
42
- /* eslint-disable unicorn/no-null -- SQLite requires null for NULL values */
11
+ // eslint-disable-next-line unicorn/no-null -- SQLite requires null for NULL values
12
+ const expiresAt = credential.expiresAt?.getTime() ?? null;
43
13
  database
44
- .prepare(`INSERT INTO credentials (agent, name, type, encrypted_data, salt, iv, auth_tag, created_at, updated_at, expires_at)
45
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
46
- ON CONFLICT(agent, name) DO UPDATE SET
47
- type = excluded.type, encrypted_data = excluded.encrypted_data, salt = excluded.salt, iv = excluded.iv, auth_tag = excluded.auth_tag,
48
- updated_at = excluded.updated_at, expires_at = excluded.expires_at`)
49
- .run(credential.agent, credential.name, credential.type, credential.encryptedData, credential.salt, credential.iv, credential.authTag, now, now, credential.expiresAt?.getTime() ?? null);
50
- /* eslint-enable unicorn/no-null */
14
+ .prepare(SQL.UPSERT_CREDENTIAL)
15
+ .run(credential.agent, credential.name, credential.type, credential.encryptedData, credential.salt, credential.iv, credential.authTag, now, now, expiresAt);
51
16
  }
52
17
  /** Get a credential by agent and name */
53
18
  function getCredential(database, agent, name) {
54
- const row = database
55
- .prepare(`SELECT agent, name, type, encrypted_data, salt, iv, auth_tag, created_at, updated_at, expires_at FROM credentials WHERE agent = ? AND name = ?`)
56
- .get(agent, name);
19
+ const row = database.prepare(SQL.SELECT_CREDENTIAL).get(agent, name);
57
20
  return row ? rowToRecord(row) : undefined;
58
21
  }
59
22
  /** List all credentials (metadata only) */
60
23
  function listCredentials(database) {
61
- const rows = database
62
- .prepare(`SELECT agent, name, type, created_at, updated_at, expires_at FROM credentials ORDER BY agent, name`)
63
- .all();
24
+ const rows = database.prepare(SQL.SELECT_ALL_METADATA).all();
64
25
  return rows.map((row) => rowToMetadata(row));
65
26
  }
66
27
  /** List credentials accessible by an API key's read access list */
@@ -71,15 +32,12 @@ function listCredentialsForApiKey(database, readAccess) {
71
32
  }
72
33
  /** Delete a credential */
73
34
  function deleteCredential(database, agent, name) {
74
- return (database
75
- .prepare(`DELETE FROM credentials WHERE agent = ? AND name = ?`)
76
- .run(agent, name).changes > 0);
35
+ return database.prepare(SQL.DELETE_CREDENTIAL).run(agent, name).changes > 0;
77
36
  }
78
37
  /** Update expiration time after refresh */
79
38
  function updateExpiresAt(database, agent, name, expiresAt) {
80
- database
81
- .prepare(`UPDATE credentials SET expires_at = ?, updated_at = ? WHERE agent = ? AND name = ?`)
82
- // eslint-disable-next-line unicorn/no-null -- SQLite requires null for NULL values
83
- .run(expiresAt?.getTime() ?? null, Date.now(), agent, name);
39
+ // eslint-disable-next-line unicorn/no-null -- SQLite requires null for NULL values
40
+ const expires = expiresAt?.getTime() ?? null;
41
+ database.prepare(SQL.UPDATE_EXPIRES_AT).run(expires, Date.now(), agent, name);
84
42
  }
85
43
  export { deleteCredential, getCredential, listCredentials, listCredentialsForApiKey, updateExpiresAt, upsertCredential, };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Pure functions for parsing credential database rows.
3
+ */
4
+ import { CredentialType } from "axshared";
5
+ import type { CredentialRow, MetadataRow } from "../types.js";
6
+ /** Credential record stored in database */
7
+ interface CredentialRecord {
8
+ agent: string;
9
+ name: string;
10
+ type: CredentialType;
11
+ encryptedData: Buffer;
12
+ salt: Buffer;
13
+ iv: Buffer;
14
+ authTag: Buffer;
15
+ createdAt: Date;
16
+ updatedAt: Date;
17
+ expiresAt: Date | undefined;
18
+ }
19
+ /** Credential metadata (without encrypted data) */
20
+ interface CredentialMetadata {
21
+ agent: string;
22
+ name: string;
23
+ type: CredentialType;
24
+ createdAt: Date;
25
+ updatedAt: Date;
26
+ expiresAt: Date | undefined;
27
+ }
28
+ /** Convert database row to record */
29
+ declare function rowToRecord(row: CredentialRow): CredentialRecord;
30
+ /** Convert metadata row to metadata */
31
+ declare function rowToMetadata(row: MetadataRow): CredentialMetadata;
32
+ export { rowToMetadata, rowToRecord };
33
+ export type { CredentialMetadata, CredentialRecord };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Pure functions for parsing credential database rows.
3
+ */
4
+ import { CredentialType } from "axshared";
5
+ /** Parse and validate credential type from database */
6
+ function parseCredentialType(type) {
7
+ const result = CredentialType.safeParse(type);
8
+ if (!result.success) {
9
+ throw new Error(`Invalid credential type: ${type}`);
10
+ }
11
+ return result.data;
12
+ }
13
+ /** Convert database row to record */
14
+ function rowToRecord(row) {
15
+ return {
16
+ agent: row.agent,
17
+ name: row.name,
18
+ type: parseCredentialType(row.type),
19
+ encryptedData: row.encrypted_data,
20
+ salt: row.salt,
21
+ iv: row.iv,
22
+ authTag: row.auth_tag,
23
+ createdAt: new Date(row.created_at),
24
+ updatedAt: new Date(row.updated_at),
25
+ expiresAt: row.expires_at ? new Date(row.expires_at) : undefined,
26
+ };
27
+ }
28
+ /** Convert metadata row to metadata */
29
+ function rowToMetadata(row) {
30
+ return {
31
+ agent: row.agent,
32
+ name: row.name,
33
+ type: parseCredentialType(row.type),
34
+ createdAt: new Date(row.created_at),
35
+ updatedAt: new Date(row.updated_at),
36
+ expiresAt: row.expires_at ? new Date(row.expires_at) : undefined,
37
+ };
38
+ }
39
+ export { rowToMetadata, rowToRecord };
@@ -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;
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * GET /api/v1/credentials/:agent/:name handler.
3
3
  */
4
+ import { isRefreshableCredentialType } from "axshared";
4
5
  import { hasReadAccess } from "../db/repositories/api-keys.js";
5
6
  import { getCredential } from "../db/repositories/credentials.js";
6
7
  import { logAccess } from "../db/repositories/audit-log.js";
@@ -69,7 +70,7 @@ function createGetCredentialHandler(database, config) {
69
70
  let wasRefreshed = false;
70
71
  let refreshFailed = false;
71
72
  if (config.refreshThresholdSeconds > 0 &&
72
- credential.type === "oauth" &&
73
+ isRefreshableCredentialType(credential.type) &&
73
74
  isRefreshable(data) &&
74
75
  needsRefresh(data, config.refreshThresholdSeconds)) {
75
76
  try {
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * PUT /api/v1/credentials/:agent/:name handler.
3
3
  */
4
+ import { CredentialType, } from "axshared";
4
5
  import { hasWriteAccess } from "../db/repositories/api-keys.js";
5
6
  import { upsertCredential } from "../db/repositories/credentials.js";
6
7
  import { logAccess } from "../db/repositories/audit-log.js";
@@ -43,7 +44,8 @@ function createPutCredentialHandler(database) {
43
44
  .json({ error: "Request body must include 'data' object" });
44
45
  return;
45
46
  }
46
- if (body.type !== "oauth" && body.type !== "api-key") {
47
+ const typeResult = CredentialType.safeParse(body.type);
48
+ if (!typeResult.success) {
47
49
  logAccess(database, {
48
50
  apiKeyId: apiKey.id,
49
51
  action: "write",
@@ -53,11 +55,11 @@ function createPutCredentialHandler(database) {
53
55
  errorMessage: "Invalid type",
54
56
  });
55
57
  response.status(400).json({
56
- error: 'Request body must include \'type\' ("oauth" or "api-key")',
58
+ error: `Request body must include 'type' (${CredentialType.options.map((t) => `"${t}"`).join(", ")})`,
57
59
  });
58
60
  return;
59
61
  }
60
- const credentialType = body.type;
62
+ const credentialType = typeResult.data;
61
63
  try {
62
64
  const encrypted = encryptCredential(body.data);
63
65
  let expiresAt;
@@ -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.
@@ -50,10 +50,10 @@ function toAxauthCredentials(agent, data) {
50
50
  if (!VALID_AGENTS.has(agent)) {
51
51
  return undefined;
52
52
  }
53
- // Only OAuth credentials can be refreshed
53
+ // Only OAuth credentials with refresh tokens can be refreshed
54
54
  return {
55
55
  agent: agent,
56
- type: "oauth",
56
+ type: "oauth-credentials",
57
57
  data,
58
58
  };
59
59
  }
@@ -5,7 +5,7 @@
5
5
  * with axauth's refresh functionality.
6
6
  */
7
7
  import type Database from "better-sqlite3";
8
- import type { CredentialType } from "../db/repositories/credentials.js";
8
+ import type { CredentialType } from "axshared";
9
9
  /** Key for credential-specific mutex */
10
10
  type CredentialKey = `${string}/${string}`;
11
11
  /** Result type for the full refresh operation */
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.2.0",
5
+ "version": "1.4.0",
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.7.2",
53
- "axshared": "^1.8.0",
54
- "better-sqlite3": "^12.5.0",
52
+ "axauth": "^1.9.0",
53
+ "axshared": "1.9.0",
54
+ "better-sqlite3": "^12.6.0",
55
55
  "commander": "^14.0.2",
56
56
  "express": "^5.2.1"
57
57
  },
@@ -78,13 +78,13 @@
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.3",
81
+ "@types/node": "^25.0.5",
82
82
  "@vitest/coverage-v8": "^4.0.16",
83
83
  "eslint": "^9.39.2",
84
84
  "eslint-config-axkit": "^1.0.0",
85
85
  "fta-check": "^1.5.1",
86
86
  "fta-cli": "^3.0.0",
87
- "knip": "^5.80.0",
87
+ "knip": "^5.80.2",
88
88
  "prettier": "3.7.4",
89
89
  "semantic-release": "^25.0.2",
90
90
  "typescript": "^5.9.3",