axvault 1.9.0 → 1.9.2

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.
@@ -7,14 +7,16 @@ import { runMigrations } from "../db/migrations.js";
7
7
  import { deleteCredential, listCredentials, } from "../db/repositories/credentials.js";
8
8
  import { containsControlChars, formatRelativeTime, getErrorMessage, sanitizeForTsv, } from "../lib/format.js";
9
9
  import { CREDENTIAL_NAME_FORMAT_DESCRIPTION, isValidCredentialName, } from "../lib/credential-name.js";
10
- /** Print credentials as TSV table (opaque schema - no type/expires columns) */
10
+ /** Print credentials as TSV table */
11
11
  function printCredentialTable(credentials) {
12
- console.log("NAME\tCREATED\tUPDATED");
12
+ console.log("NAME\tAGENT\tPROVIDER\tCREATED\tUPDATED");
13
13
  for (const cred of credentials) {
14
14
  const name = sanitizeForTsv(cred.name);
15
+ const agent = cred.agent || "";
16
+ const provider = cred.provider ?? "";
15
17
  const created = formatRelativeTime(cred.createdAt);
16
18
  const updated = formatRelativeTime(cred.updatedAt);
17
- console.log(`${name}\t${created}\t${updated}`);
19
+ console.log(`${name}\t${agent}\t${provider}\t${created}\t${updated}`);
18
20
  }
19
21
  }
20
22
  /**
@@ -53,9 +55,10 @@ export function handleCredentialList(options) {
53
55
  runMigrations(database);
54
56
  const credentials = listCredentials(database);
55
57
  if (options.json) {
56
- // Opaque schema: type/provider/expiresAt not available without decrypting
57
58
  const output = credentials.map((cred) => ({
58
59
  name: cred.name,
60
+ ...(cred.agent !== "" && { agent: cred.agent }),
61
+ ...(cred.provider !== undefined && { provider: cred.provider }),
59
62
  createdAt: cred.createdAt.toISOString(),
60
63
  updatedAt: cred.updatedAt.toISOString(),
61
64
  }));
@@ -5,7 +5,7 @@
5
5
  * Credentials are identified by name only - the blob contains all metadata.
6
6
  */
7
7
  import type Database from "better-sqlite3";
8
- declare const CURRENT_VERSION = 1;
8
+ declare const CURRENT_VERSION = 2;
9
9
  /** Run all pending migrations */
10
10
  declare function runMigrations(database: Database.Database): void;
11
11
  /** Get current schema version */
@@ -4,7 +4,7 @@
4
4
  * Simple schema with opaque credential storage.
5
5
  * Credentials are identified by name only - the blob contains all metadata.
6
6
  */
7
- const CURRENT_VERSION = 1;
7
+ const CURRENT_VERSION = 2;
8
8
  /** Run all pending migrations */
9
9
  function runMigrations(database) {
10
10
  let version = getSchemaVersion(database);
@@ -17,6 +17,11 @@ function runMigrations(database) {
17
17
  version = 1;
18
18
  continue;
19
19
  }
20
+ if (version === 1) {
21
+ migrateToV2(database);
22
+ version = 2;
23
+ continue;
24
+ }
20
25
  throw new Error(`Unsupported database schema version v${version} (expected v${CURRENT_VERSION}). Delete the database file to reinitialize.`);
21
26
  }
22
27
  }
@@ -88,4 +93,24 @@ function migrateToV1(database) {
88
93
  setSchemaVersion(database, 1);
89
94
  })();
90
95
  }
96
+ /**
97
+ * Migration to version 2: Add agent and provider columns
98
+ *
99
+ * Moves routing metadata (agent, provider) from the encrypted blob to
100
+ * unencrypted columns. This enables refresh-on-read to pass agent/provider
101
+ * to axauth without parsing the blob, and allows future filtering by agent.
102
+ *
103
+ * Existing rows get empty agent (new PUTs populate the column).
104
+ */
105
+ function migrateToV2(database) {
106
+ database.transaction(() => {
107
+ database.exec(`
108
+ ALTER TABLE credentials ADD COLUMN agent TEXT NOT NULL DEFAULT ''
109
+ `);
110
+ database.exec(`
111
+ ALTER TABLE credentials ADD COLUMN provider TEXT DEFAULT NULL
112
+ `);
113
+ setSchemaVersion(database, 2);
114
+ })();
115
+ }
91
116
  export { CURRENT_VERSION, getSchemaVersion, runMigrations };
@@ -1,12 +1,10 @@
1
1
  /**
2
2
  * SQL queries for credentials repository.
3
- *
4
- * Name-only primary key, opaque blob storage.
5
3
  */
6
- export declare const UPSERT_CREDENTIAL = "\n INSERT INTO credentials (name, encrypted_data, salt, iv, auth_tag, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(name) DO UPDATE SET\n encrypted_data = excluded.encrypted_data, salt = excluded.salt, iv = excluded.iv, auth_tag = excluded.auth_tag,\n updated_at = excluded.updated_at\n RETURNING created_at, updated_at";
4
+ export declare const UPSERT_CREDENTIAL = "\n INSERT INTO credentials (name, agent, provider, encrypted_data, salt, iv, auth_tag, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(name) DO UPDATE SET\n agent = excluded.agent, provider = excluded.provider,\n encrypted_data = excluded.encrypted_data, salt = excluded.salt, iv = excluded.iv, auth_tag = excluded.auth_tag,\n updated_at = excluded.updated_at\n RETURNING created_at, updated_at";
7
5
  export declare const UPDATE_CREDENTIAL_IF_UPDATED_AT_MATCHES = "\n UPDATE credentials\n SET encrypted_data = ?, salt = ?, iv = ?, auth_tag = ?, updated_at = ?\n WHERE name = ? AND updated_at = ?";
8
- export declare const SELECT_CREDENTIAL = "\n SELECT name, encrypted_data, salt, iv, auth_tag, created_at, updated_at\n FROM credentials WHERE name = ?";
9
- export declare const SELECT_ALL_METADATA = "\n SELECT name, created_at, updated_at\n FROM credentials ORDER BY name";
10
- export declare const SELECT_METADATA_PAGINATED = "\n SELECT name, created_at, updated_at\n FROM credentials\n WHERE name > ?\n ORDER BY name\n LIMIT ?";
11
- export declare const SELECT_METADATA_FIRST_PAGE = "\n SELECT name, created_at, updated_at\n FROM credentials\n ORDER BY name\n LIMIT ?";
6
+ export declare const SELECT_CREDENTIAL = "\n SELECT name, agent, provider, encrypted_data, salt, iv, auth_tag, created_at, updated_at\n FROM credentials WHERE name = ?";
7
+ export declare const SELECT_ALL_METADATA = "\n SELECT name, agent, provider, created_at, updated_at\n FROM credentials ORDER BY name";
8
+ export declare const SELECT_METADATA_PAGINATED = "\n SELECT name, agent, provider, created_at, updated_at\n FROM credentials\n WHERE name > ?\n ORDER BY name\n LIMIT ?";
9
+ export declare const SELECT_METADATA_FIRST_PAGE = "\n SELECT name, agent, provider, created_at, updated_at\n FROM credentials\n ORDER BY name\n LIMIT ?";
12
10
  export declare const DELETE_CREDENTIAL = "\n DELETE FROM credentials WHERE name = ?";
@@ -1,12 +1,11 @@
1
1
  /**
2
2
  * SQL queries for credentials repository.
3
- *
4
- * Name-only primary key, opaque blob storage.
5
3
  */
6
4
  export const UPSERT_CREDENTIAL = `
7
- INSERT INTO credentials (name, encrypted_data, salt, iv, auth_tag, created_at, updated_at)
8
- VALUES (?, ?, ?, ?, ?, ?, ?)
5
+ INSERT INTO credentials (name, agent, provider, encrypted_data, salt, iv, auth_tag, created_at, updated_at)
6
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
9
7
  ON CONFLICT(name) DO UPDATE SET
8
+ agent = excluded.agent, provider = excluded.provider,
10
9
  encrypted_data = excluded.encrypted_data, salt = excluded.salt, iv = excluded.iv, auth_tag = excluded.auth_tag,
11
10
  updated_at = excluded.updated_at
12
11
  RETURNING created_at, updated_at`;
@@ -15,19 +14,19 @@ export const UPDATE_CREDENTIAL_IF_UPDATED_AT_MATCHES = `
15
14
  SET encrypted_data = ?, salt = ?, iv = ?, auth_tag = ?, updated_at = ?
16
15
  WHERE name = ? AND updated_at = ?`;
17
16
  export const SELECT_CREDENTIAL = `
18
- SELECT name, encrypted_data, salt, iv, auth_tag, created_at, updated_at
17
+ SELECT name, agent, provider, encrypted_data, salt, iv, auth_tag, created_at, updated_at
19
18
  FROM credentials WHERE name = ?`;
20
19
  export const SELECT_ALL_METADATA = `
21
- SELECT name, created_at, updated_at
20
+ SELECT name, agent, provider, created_at, updated_at
22
21
  FROM credentials ORDER BY name`;
23
22
  export const SELECT_METADATA_PAGINATED = `
24
- SELECT name, created_at, updated_at
23
+ SELECT name, agent, provider, created_at, updated_at
25
24
  FROM credentials
26
25
  WHERE name > ?
27
26
  ORDER BY name
28
27
  LIMIT ?`;
29
28
  export const SELECT_METADATA_FIRST_PAGE = `
30
- SELECT name, created_at, updated_at
29
+ SELECT name, agent, provider, created_at, updated_at
31
30
  FROM credentials
32
31
  ORDER BY name
33
32
  LIMIT ?`;
@@ -14,6 +14,8 @@ interface UpsertTimestamps {
14
14
  /** Store or update a credential, returning the resulting timestamps */
15
15
  declare function upsertCredential(database: Database.Database, credential: {
16
16
  name: string;
17
+ agent: string;
18
+ provider: string | undefined;
17
19
  encryptedData: Buffer;
18
20
  salt: Buffer;
19
21
  iv: Buffer;
@@ -11,7 +11,7 @@ function upsertCredential(database, credential) {
11
11
  const now = Date.now();
12
12
  const row = database
13
13
  .prepare(SQL.UPSERT_CREDENTIAL)
14
- .get(credential.name, credential.encryptedData, credential.salt, credential.iv, credential.authTag, now, now);
14
+ .get(credential.name, credential.agent, credential.provider ?? undefined, credential.encryptedData, credential.salt, credential.iv, credential.authTag, now, now);
15
15
  return {
16
16
  createdAt: new Date(row.created_at),
17
17
  updatedAt: new Date(row.updated_at),
@@ -16,7 +16,7 @@ function buildFilteredQuery(names, cursor, limit) {
16
16
  const placeholders = names.map(() => "?").join(", ");
17
17
  const cursorClause = cursor ? " AND name > ?" : "";
18
18
  const sql = `
19
- SELECT name, created_at, updated_at
19
+ SELECT name, agent, provider, created_at, updated_at
20
20
  FROM credentials
21
21
  WHERE name IN (${placeholders})${cursorClause}
22
22
  ORDER BY name
@@ -7,6 +7,8 @@ import type { CredentialRow, MetadataRow } from "../types.js";
7
7
  /** Credential record stored in database */
8
8
  interface CredentialRecord {
9
9
  name: string;
10
+ agent: string;
11
+ provider: string | undefined;
10
12
  encryptedData: Buffer;
11
13
  salt: Buffer;
12
14
  iv: Buffer;
@@ -17,6 +19,8 @@ interface CredentialRecord {
17
19
  /** Credential metadata (without encrypted data) */
18
20
  interface CredentialMetadata {
19
21
  name: string;
22
+ agent: string;
23
+ provider: string | undefined;
20
24
  createdAt: Date;
21
25
  updatedAt: Date;
22
26
  }
@@ -7,6 +7,8 @@
7
7
  function rowToRecord(row) {
8
8
  return {
9
9
  name: row.name,
10
+ agent: row.agent,
11
+ provider: row.provider ?? undefined,
10
12
  encryptedData: row.encrypted_data,
11
13
  salt: row.salt,
12
14
  iv: row.iv,
@@ -19,6 +21,8 @@ function rowToRecord(row) {
19
21
  function rowToMetadata(row) {
20
22
  return {
21
23
  name: row.name,
24
+ agent: row.agent,
25
+ provider: row.provider ?? undefined,
22
26
  createdAt: new Date(row.created_at),
23
27
  updatedAt: new Date(row.updated_at),
24
28
  };
@@ -23,9 +23,11 @@ export interface AuditLogRow {
23
23
  success: number;
24
24
  error_message: string | null;
25
25
  }
26
- /** Raw credential row from database (opaque blob storage) */
26
+ /** Raw credential row from database */
27
27
  export interface CredentialRow {
28
28
  name: string;
29
+ agent: string;
30
+ provider: string | null;
29
31
  encrypted_data: Buffer;
30
32
  salt: Buffer;
31
33
  iv: Buffer;
@@ -36,6 +38,8 @@ export interface CredentialRow {
36
38
  /** Raw credential metadata row from database */
37
39
  export interface MetadataRow {
38
40
  name: string;
41
+ agent: string;
42
+ provider: string | null;
39
43
  created_at: number;
40
44
  updated_at: number;
41
45
  }
@@ -67,6 +67,8 @@ function createGetCredentialHandler(database, config) {
67
67
  apiKeyId: apiKey.id,
68
68
  name,
69
69
  blob,
70
+ agent: credential.agent,
71
+ provider: credential.provider,
70
72
  expectedUpdatedAt: credential.updatedAt,
71
73
  refreshThresholdSeconds: config.refreshThresholdSeconds,
72
74
  refreshTimeoutMs: config.refreshTimeoutMs,
@@ -94,9 +96,13 @@ function createGetCredentialHandler(database, config) {
94
96
  if (refreshFailed) {
95
97
  response.setHeader("X-Axvault-Refresh-Failed", "true");
96
98
  }
97
- // Return the blob as-is (opaque to axvault)
99
+ // Return the blob along with routing metadata from columns
98
100
  response.json({
99
101
  name,
102
+ ...(credential.agent !== "" && { agent: credential.agent }),
103
+ ...(credential.provider !== undefined && {
104
+ provider: credential.provider,
105
+ }),
100
106
  credential: finalBlob,
101
107
  updatedAt: finalUpdatedAt.toISOString(),
102
108
  });
@@ -10,6 +10,16 @@
10
10
  */
11
11
  import { listCredentialsForApiKey, listCredentialsPaginated, } from "../db/repositories/credentials.js";
12
12
  import { logAccess } from "../db/repositories/audit-log.js";
13
+ /** Format credential metadata for API response */
14
+ function formatCredentialMetadata(cred) {
15
+ return {
16
+ name: cred.name,
17
+ ...(cred.agent !== "" && { agent: cred.agent }),
18
+ ...(cred.provider !== undefined && { provider: cred.provider }),
19
+ createdAt: cred.createdAt.toISOString(),
20
+ updatedAt: cred.updatedAt.toISOString(),
21
+ };
22
+ }
13
23
  /** Maximum page size when limit is specified */
14
24
  const MAX_LIMIT = 1000;
15
25
  /**
@@ -61,11 +71,7 @@ function createListCredentialsHandler(database) {
61
71
  success: true,
62
72
  });
63
73
  response.json({
64
- credentials: credentials.map((cred) => ({
65
- name: cred.name,
66
- createdAt: cred.createdAt.toISOString(),
67
- updatedAt: cred.updatedAt.toISOString(),
68
- })),
74
+ credentials: credentials.map((cred) => formatCredentialMetadata(cred)),
69
75
  });
70
76
  return;
71
77
  }
@@ -92,11 +98,7 @@ function createListCredentialsHandler(database) {
92
98
  success: true,
93
99
  });
94
100
  response.json({
95
- credentials: result.credentials.map((cred) => ({
96
- name: cred.name,
97
- createdAt: cred.createdAt.toISOString(),
98
- updatedAt: cred.updatedAt.toISOString(),
99
- })),
101
+ credentials: result.credentials.map((cred) => formatCredentialMetadata(cred)),
100
102
  ...(result.nextCursor !== undefined && {
101
103
  nextCursor: result.nextCursor,
102
104
  }),
@@ -48,10 +48,19 @@ function createPutCredentialHandler(database) {
48
48
  return;
49
49
  }
50
50
  try {
51
- // Encrypt the blob as-is (opaque storage)
52
- const encrypted = encryptCredential(body);
51
+ // Extract routing metadata from body before encrypting
52
+ const bodyRecord = body;
53
+ const agent = typeof bodyRecord.agent === "string" ? bodyRecord.agent : "";
54
+ const provider = typeof bodyRecord.provider === "string"
55
+ ? bodyRecord.provider
56
+ : undefined;
57
+ // Encrypt the credential blob (agent/provider stored as columns, not in blob)
58
+ const credentialBlob = Object.fromEntries(Object.entries(bodyRecord).filter(([key]) => key !== "agent" && key !== "provider"));
59
+ const encrypted = encryptCredential(credentialBlob);
53
60
  const timestamps = upsertCredential(database, {
54
61
  name,
62
+ agent,
63
+ provider,
55
64
  ...encrypted,
56
65
  });
57
66
  logAccess(database, {
@@ -19,6 +19,8 @@ declare function refreshCredentialOnRead(options: {
19
19
  apiKeyId: string;
20
20
  name: string;
21
21
  blob: unknown;
22
+ agent: string;
23
+ provider: string | undefined;
22
24
  expectedUpdatedAt: Date;
23
25
  refreshThresholdSeconds: number;
24
26
  refreshTimeoutMs: number;
@@ -5,17 +5,21 @@
5
5
  * stays small enough for static complexity checks (FTA).
6
6
  */
7
7
  import { isCredentialExpired, isRefreshable, refreshBlob, } from "axauth";
8
- import { isRefreshableCredentialType, parseCredentials } from "axshared";
8
+ import { isValidAgentCli, isRefreshableCredentialType, parseCredentials, } from "axshared";
9
9
  import { getCredential, updateCredentialIfUnchanged, } from "../db/repositories/credentials.js";
10
10
  import { logAccess } from "../db/repositories/audit-log.js";
11
11
  import { decryptCredential, encryptCredential } from "../lib/encryption.js";
12
12
  /** Per-credential mutex to prevent concurrent refreshes */
13
13
  const pendingRefreshes = new Map();
14
- function getRefreshPromise(name, blob, refreshTimeoutMs) {
14
+ function getRefreshPromise(name, blob, agent, provider, refreshTimeoutMs) {
15
15
  const existing = pendingRefreshes.get(name);
16
16
  if (existing)
17
17
  return existing;
18
- const promise = refreshBlob(blob, { timeout: refreshTimeoutMs });
18
+ const promise = refreshBlob(blob, {
19
+ agent,
20
+ provider,
21
+ timeout: refreshTimeoutMs,
22
+ });
19
23
  pendingRefreshes.set(name, promise);
20
24
  void promise.finally(() => {
21
25
  if (pendingRefreshes.get(name) === promise) {
@@ -34,6 +38,17 @@ async function refreshCredentialOnRead(options) {
34
38
  refreshFailed: false,
35
39
  };
36
40
  }
41
+ // Refresh requires a valid agent ID (empty means pre-v2 credential)
42
+ if (!isValidAgentCli(options.agent)) {
43
+ return {
44
+ status: "ok",
45
+ blob: options.blob,
46
+ updatedAt: options.expectedUpdatedAt,
47
+ wasRefreshed: false,
48
+ refreshFailed: false,
49
+ };
50
+ }
51
+ const agent = options.agent;
37
52
  const parsedCredentials = parseCredentials(options.blob);
38
53
  if (parsedCredentials === undefined ||
39
54
  !isRefreshableCredentialType(parsedCredentials.type) ||
@@ -48,7 +63,7 @@ async function refreshCredentialOnRead(options) {
48
63
  };
49
64
  }
50
65
  try {
51
- const refreshResult = await getRefreshPromise(options.name, options.blob, options.refreshTimeoutMs);
66
+ const refreshResult = await getRefreshPromise(options.name, options.blob, agent, options.provider, options.refreshTimeoutMs);
52
67
  if (refreshResult.ok) {
53
68
  const encrypted = encryptCredential(refreshResult.blob);
54
69
  const updateResult = updateCredentialIfUnchanged(options.database, {
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.9.0",
5
+ "version": "1.9.2",
6
6
  "description": "Remote credential storage server for axkit",
7
7
  "repository": {
8
8
  "type": "git",
@@ -49,8 +49,8 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@commander-js/extra-typings": "^14.0.0",
52
- "axauth": "^3.1.1",
53
- "axshared": "4.0.0",
52
+ "axauth": "^3.1.4",
53
+ "axshared": "5.0.0",
54
54
  "better-sqlite3": "^12.6.2",
55
55
  "commander": "^14.0.2",
56
56
  "express": "^5.2.1"