axvault 1.8.0 → 1.8.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.
Files changed (48) hide show
  1. package/README.md +120 -36
  2. package/dist/cli.js +7 -7
  3. package/dist/commands/credential.d.ts +1 -1
  4. package/dist/commands/credential.js +26 -32
  5. package/dist/commands/init.js +12 -11
  6. package/dist/commands/serve.js +1 -0
  7. package/dist/db/migrations.d.ts +3 -2
  8. package/dist/db/migrations.js +25 -120
  9. package/dist/db/repositories/api-key-utilities.d.ts +3 -3
  10. package/dist/db/repositories/api-key-utilities.js +9 -10
  11. package/dist/db/repositories/audit-log.d.ts +3 -5
  12. package/dist/db/repositories/audit-log.js +7 -8
  13. package/dist/db/repositories/credentials-queries.d.ts +9 -5
  14. package/dist/db/repositories/credentials-queries.js +28 -12
  15. package/dist/db/repositories/credentials.d.ts +31 -14
  16. package/dist/db/repositories/credentials.js +39 -21
  17. package/dist/db/repositories/list-credentials-paginated.d.ts +26 -0
  18. package/dist/db/repositories/list-credentials-paginated.js +69 -0
  19. package/dist/db/repositories/parse-credential-row.d.ts +2 -9
  20. package/dist/db/repositories/parse-credential-row.js +2 -17
  21. package/dist/db/types.d.ts +3 -12
  22. package/dist/db/types.js +1 -1
  23. package/dist/handlers/delete-credential.d.ts +2 -3
  24. package/dist/handlers/delete-credential.js +8 -11
  25. package/dist/handlers/get-credential.d.ts +6 -3
  26. package/dist/handlers/get-credential.js +35 -78
  27. package/dist/handlers/list-credentials.d.ts +19 -3
  28. package/dist/handlers/list-credentials.js +83 -8
  29. package/dist/handlers/put-credential.d.ts +10 -3
  30. package/dist/handlers/put-credential.js +25 -78
  31. package/dist/handlers/refresh-credential-on-read.d.ts +26 -0
  32. package/dist/handlers/refresh-credential-on-read.js +145 -0
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.js +1 -1
  35. package/dist/lib/credential-name.d.ts +10 -0
  36. package/dist/lib/credential-name.js +12 -0
  37. package/dist/lib/format.d.ts +1 -7
  38. package/dist/lib/format.js +7 -55
  39. package/dist/middleware/validate-parameters.d.ts +3 -3
  40. package/dist/middleware/validate-parameters.js +7 -14
  41. package/dist/server/routes.js +3 -3
  42. package/package.json +9 -9
  43. package/dist/refresh/check-refresh.d.ts +0 -29
  44. package/dist/refresh/check-refresh.js +0 -51
  45. package/dist/refresh/log-refresh.d.ts +0 -17
  46. package/dist/refresh/log-refresh.js +0 -35
  47. package/dist/refresh/refresh-manager.d.ts +0 -54
  48. package/dist/refresh/refresh-manager.js +0 -137
@@ -1,32 +1,23 @@
1
1
  /**
2
2
  * Database schema migrations.
3
3
  *
4
- * Uses a simple version-based migration system.
4
+ * Simple schema with opaque credential storage.
5
+ * Credentials are identified by name only - the blob contains all metadata.
5
6
  */
6
- const CURRENT_VERSION = 7;
7
+ const CURRENT_VERSION = 1;
7
8
  /** Run all pending migrations */
8
9
  function runMigrations(database) {
9
- const version = getSchemaVersion(database);
10
- if (version < 1) {
11
- migrateToV1(database);
10
+ let version = getSchemaVersion(database);
11
+ if (version > CURRENT_VERSION) {
12
+ throw new Error(`Unsupported database schema version v${version} (expected v${CURRENT_VERSION}). Delete the database file to reinitialize.`);
12
13
  }
13
- if (version < 2) {
14
- migrateToV2(database);
15
- }
16
- if (version < 3) {
17
- migrateToV3(database);
18
- }
19
- if (version < 4) {
20
- migrateToV4(database);
21
- }
22
- if (version < 5) {
23
- migrateToV5(database);
24
- }
25
- if (version < 6) {
26
- migrateToV6(database);
27
- }
28
- if (version < 7) {
29
- migrateToV7(database);
14
+ while (version < CURRENT_VERSION) {
15
+ if (version === 0) {
16
+ migrateToV1(database);
17
+ version = 1;
18
+ continue;
19
+ }
20
+ throw new Error(`Unsupported database schema version v${version} (expected v${CURRENT_VERSION}). Delete the database file to reinitialize.`);
30
21
  }
31
22
  }
32
23
  /** Get current schema version */
@@ -41,7 +32,10 @@ function setSchemaVersion(database, version) {
41
32
  /**
42
33
  * Migration to version 1: Initial schema
43
34
  *
44
- * Creates tables for API keys, credentials, and audit log.
35
+ * Simple opaque credential storage:
36
+ * - Credentials identified by name only
37
+ * - All credential metadata (agent, type, provider, data) in encrypted blob
38
+ * - API keys with name-based access patterns
45
39
  */
46
40
  function migrateToV1(database) {
47
41
  database.transaction(() => {
@@ -51,8 +45,10 @@ function migrateToV1(database) {
51
45
  id TEXT PRIMARY KEY,
52
46
  name TEXT NOT NULL,
53
47
  key_hash TEXT NOT NULL UNIQUE,
48
+ key_prefix TEXT,
54
49
  read_access TEXT NOT NULL,
55
50
  write_access TEXT NOT NULL,
51
+ grant_access TEXT NOT NULL DEFAULT '[]',
56
52
  created_at INTEGER NOT NULL,
57
53
  last_used_at INTEGER
58
54
  )
@@ -61,29 +57,26 @@ function migrateToV1(database) {
61
57
  database.exec(`
62
58
  CREATE INDEX idx_api_keys_key_hash ON api_keys(key_hash)
63
59
  `);
64
- // Encrypted credentials
60
+ // Encrypted credentials (opaque blob storage)
65
61
  database.exec(`
66
62
  CREATE TABLE credentials (
67
- agent TEXT NOT NULL,
68
- name TEXT NOT NULL,
63
+ name TEXT PRIMARY KEY,
69
64
  encrypted_data BLOB NOT NULL,
65
+ salt BLOB NOT NULL,
70
66
  iv BLOB NOT NULL,
71
67
  auth_tag BLOB NOT NULL,
72
68
  created_at INTEGER NOT NULL,
73
- updated_at INTEGER NOT NULL,
74
- expires_at INTEGER,
75
- PRIMARY KEY (agent, name)
69
+ updated_at INTEGER NOT NULL
76
70
  )
77
71
  `);
78
- // Audit log - intentionally no FK on api_key_id to preserve logs after key deletion
72
+ // Audit log
79
73
  database.exec(`
80
74
  CREATE TABLE audit_log (
81
75
  id INTEGER PRIMARY KEY AUTOINCREMENT,
82
76
  timestamp INTEGER NOT NULL,
83
77
  api_key_id TEXT,
84
78
  action TEXT NOT NULL,
85
- agent TEXT,
86
- name TEXT,
79
+ credential_name TEXT,
87
80
  success INTEGER NOT NULL,
88
81
  error_message TEXT
89
82
  )
@@ -95,92 +88,4 @@ function migrateToV1(database) {
95
88
  setSchemaVersion(database, 1);
96
89
  })();
97
90
  }
98
- /**
99
- * Migration to version 2: Add salt column to credentials
100
- *
101
- * AES-256-GCM with PBKDF2 requires a salt for key derivation.
102
- */
103
- function migrateToV2(database) {
104
- database.transaction(() => {
105
- database.exec(`
106
- ALTER TABLE credentials ADD COLUMN salt BLOB
107
- `);
108
- setSchemaVersion(database, 2);
109
- })();
110
- }
111
- /**
112
- * Migration to version 3: Add type column to credentials
113
- *
114
- * Explicit credential type ("oauth" or "api-key") instead of inferring from data.
115
- * Existing credentials without type must be re-uploaded.
116
- */
117
- function migrateToV3(database) {
118
- database.transaction(() => {
119
- database.exec(`
120
- ALTER TABLE credentials ADD COLUMN type TEXT
121
- `);
122
- setSchemaVersion(database, 3);
123
- })();
124
- }
125
- /**
126
- * Migration to version 4: Add grant_access column to api_keys
127
- *
128
- * The grant permission controls which credentials a key can delegate access to.
129
- * Existing keys get an empty grant_access array (no delegation rights).
130
- */
131
- function migrateToV4(database) {
132
- database.transaction(() => {
133
- database.exec(`
134
- ALTER TABLE api_keys ADD COLUMN grant_access TEXT NOT NULL DEFAULT '[]'
135
- `);
136
- setSchemaVersion(database, 4);
137
- })();
138
- }
139
- /**
140
- * Migration to version 5: Rename credential type "oauth" to "oauth-credentials"
141
- *
142
- * Distinguishes refreshable OAuth credentials (oauth-credentials) from
143
- * long-lived OAuth tokens like CLAUDE_CODE_OAUTH_TOKEN (oauth-token).
144
- *
145
- * - `oauth-credentials`: Full OAuth flow with accessToken, refreshToken, expiresAt (refreshable)
146
- * - `oauth-token`: Long-lived OAuth token for CI/CD (static, no refresh)
147
- * - `api-key`: API key (static)
148
- */
149
- function migrateToV5(database) {
150
- database.transaction(() => {
151
- database.exec(`
152
- UPDATE credentials SET type = 'oauth-credentials' WHERE type = 'oauth'
153
- `);
154
- setSchemaVersion(database, 5);
155
- })();
156
- }
157
- /**
158
- * Migration to version 6: Add key_prefix column to api_keys
159
- *
160
- * Stores first 8 characters of the key secret for identification.
161
- * Existing keys will have NULL prefix (key value not recoverable).
162
- */
163
- function migrateToV6(database) {
164
- database.transaction(() => {
165
- database.exec(`
166
- ALTER TABLE api_keys ADD COLUMN key_prefix TEXT
167
- `);
168
- setSchemaVersion(database, 6);
169
- })();
170
- }
171
- /**
172
- * Migration to version 7: Add provider column to credentials
173
- *
174
- * Stores the provider name for multi-provider agents like OpenCode.
175
- * For example, OpenCode credentials can have provider = "anthropic", "openai",
176
- * "gemini", or "opencode" (the OpenCode Zen service).
177
- */
178
- function migrateToV7(database) {
179
- database.transaction(() => {
180
- database.exec(`
181
- ALTER TABLE credentials ADD COLUMN provider TEXT
182
- `);
183
- setSchemaVersion(database, 7);
184
- })();
185
- }
186
91
  export { CURRENT_VERSION, getSchemaVersion, runMigrations };
@@ -18,9 +18,9 @@ interface AccessLists {
18
18
  grantAccess: string[];
19
19
  }
20
20
  /** Check if API key has read access to a credential */
21
- declare function hasReadAccess(apiKey: AccessLists, agent: string, name: string): boolean;
21
+ declare function hasReadAccess(apiKey: AccessLists, name: string): boolean;
22
22
  /** Check if API key has write access to a credential */
23
- declare function hasWriteAccess(apiKey: AccessLists, agent: string, name: string): boolean;
23
+ declare function hasWriteAccess(apiKey: AccessLists, name: string): boolean;
24
24
  /** Check if API key has grant access to a credential */
25
- declare function hasGrantAccess(apiKey: AccessLists, agent: string, name: string): boolean;
25
+ declare function hasGrantAccess(apiKey: AccessLists, name: string): boolean;
26
26
  export { extractKeyPrefix, generateKeyId, generateKeySecret, hashApiKey, hasGrantAccess, hasReadAccess, hasWriteAccess, };
@@ -23,21 +23,20 @@ function extractKeyPrefix(key) {
23
23
  // Key format: axv_sk_ + 32 hex chars
24
24
  return key.slice(0, 7 + KEY_PREFIX_LENGTH); // "axv_sk_" (7) + 8 chars
25
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);
26
+ /** Check if access list includes the given credential name */
27
+ function hasAccess(accessList, name) {
28
+ return accessList.includes("*") || accessList.includes(name);
30
29
  }
31
30
  /** Check if API key has read access to a credential */
32
- function hasReadAccess(apiKey, agent, name) {
33
- return hasAccess(apiKey.readAccess, agent, name);
31
+ function hasReadAccess(apiKey, name) {
32
+ return hasAccess(apiKey.readAccess, name);
34
33
  }
35
34
  /** Check if API key has write access to a credential */
36
- function hasWriteAccess(apiKey, agent, name) {
37
- return hasAccess(apiKey.writeAccess, agent, name);
35
+ function hasWriteAccess(apiKey, name) {
36
+ return hasAccess(apiKey.writeAccess, name);
38
37
  }
39
38
  /** Check if API key has grant access to a credential */
40
- function hasGrantAccess(apiKey, agent, name) {
41
- return hasAccess(apiKey.grantAccess, agent, name);
39
+ function hasGrantAccess(apiKey, name) {
40
+ return hasAccess(apiKey.grantAccess, name);
42
41
  }
43
42
  export { extractKeyPrefix, generateKeyId, generateKeySecret, hashApiKey, hasGrantAccess, hasReadAccess, hasWriteAccess, };
@@ -10,8 +10,7 @@ interface AuditLogEntry {
10
10
  timestamp: Date;
11
11
  apiKeyId: string | undefined;
12
12
  action: "auth" | "read" | "write" | "delete" | "refresh" | "list" | "grant";
13
- agent: string | undefined;
14
- name: string | undefined;
13
+ credentialName: string | undefined;
15
14
  success: boolean;
16
15
  errorMessage: string | undefined;
17
16
  }
@@ -19,8 +18,7 @@ interface AuditLogEntry {
19
18
  declare function logAccess(database: Database.Database, entry: {
20
19
  apiKeyId?: string;
21
20
  action: AuditLogEntry["action"];
22
- agent?: string;
23
- name?: string;
21
+ credentialName?: string;
24
22
  success: boolean;
25
23
  errorMessage?: string;
26
24
  }): void;
@@ -30,7 +28,7 @@ declare function getRecentLogs(database: Database.Database, options?: {
30
28
  apiKeyId?: string;
31
29
  }): AuditLogEntry[];
32
30
  /** Get logs for a specific credential */
33
- declare function getLogsForCredential(database: Database.Database, agent: string, name: string, limit_?: number): AuditLogEntry[];
31
+ declare function getLogsForCredential(database: Database.Database, credentialName: string, limit_?: number): AuditLogEntry[];
34
32
  /** Prune old audit log entries */
35
33
  declare function pruneOldLogs(database: Database.Database, olderThanDays: number): number;
36
34
  export { getLogsForCredential, getRecentLogs, logAccess, pruneOldLogs };
@@ -10,19 +10,18 @@ function rowToEntry(row) {
10
10
  timestamp: new Date(row.timestamp),
11
11
  apiKeyId: row.api_key_id ?? undefined,
12
12
  action: row.action,
13
- agent: row.agent ?? undefined,
14
- name: row.name ?? undefined,
13
+ credentialName: row.credential_name ?? undefined,
15
14
  success: row.success === 1,
16
15
  errorMessage: row.error_message ?? undefined,
17
16
  };
18
17
  }
19
- const SELECT_COLUMNS = `id, timestamp, api_key_id, action, agent, name, success, error_message`;
18
+ const SELECT_COLUMNS = `id, timestamp, api_key_id, action, credential_name, success, error_message`;
20
19
  /** Log a credential access event */
21
20
  function logAccess(database, entry) {
22
21
  /* eslint-disable unicorn/no-null -- SQLite requires null for NULL values */
23
22
  database
24
- .prepare(`INSERT INTO audit_log (timestamp, api_key_id, action, agent, name, success, error_message) VALUES (?, ?, ?, ?, ?, ?, ?)`)
25
- .run(Date.now(), entry.apiKeyId ?? null, entry.action, entry.agent ?? null, entry.name ?? null, entry.success ? 1 : 0, entry.errorMessage ?? null);
23
+ .prepare(`INSERT INTO audit_log (timestamp, api_key_id, action, credential_name, success, error_message) VALUES (?, ?, ?, ?, ?, ?)`)
24
+ .run(Date.now(), entry.apiKeyId ?? null, entry.action, entry.credentialName ?? null, entry.success ? 1 : 0, entry.errorMessage ?? null);
26
25
  /* eslint-enable unicorn/no-null */
27
26
  }
28
27
  /** Get recent audit log entries */
@@ -39,11 +38,11 @@ function getRecentLogs(database, options) {
39
38
  return database.prepare(query).all(...parameters).map((row) => rowToEntry(row));
40
39
  }
41
40
  /** Get logs for a specific credential */
42
- function getLogsForCredential(database, agent, name, limit_ = 50) {
41
+ function getLogsForCredential(database, credentialName, limit_ = 50) {
43
42
  const limit = Math.max(1, Math.min(limit_, 1000));
44
43
  const rows = database
45
- .prepare(`SELECT ${SELECT_COLUMNS} FROM audit_log WHERE agent = ? AND name = ? ORDER BY timestamp DESC LIMIT ?`)
46
- .all(agent, name, limit);
44
+ .prepare(`SELECT ${SELECT_COLUMNS} FROM audit_log WHERE credential_name = ? ORDER BY timestamp DESC LIMIT ?`)
45
+ .all(credentialName, limit);
47
46
  return rows.map((row) => rowToEntry(row));
48
47
  }
49
48
  /** Prune old audit log entries */
@@ -1,8 +1,12 @@
1
1
  /**
2
2
  * SQL queries for credentials repository.
3
+ *
4
+ * Name-only primary key, opaque blob storage.
3
5
  */
4
- export declare const UPSERT_CREDENTIAL = "\n INSERT INTO credentials (agent, name, type, provider, 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, provider = excluded.provider, 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, provider, 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, provider, 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 = ?";
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";
7
+ 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 ?";
12
+ export declare const DELETE_CREDENTIAL = "\n DELETE FROM credentials WHERE name = ?";
@@ -1,19 +1,35 @@
1
1
  /**
2
2
  * SQL queries for credentials repository.
3
+ *
4
+ * Name-only primary key, opaque blob storage.
3
5
  */
4
6
  export const UPSERT_CREDENTIAL = `
5
- INSERT INTO credentials (agent, name, type, provider, 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, provider = excluded.provider, 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`;
7
+ INSERT INTO credentials (name, encrypted_data, salt, iv, auth_tag, created_at, updated_at)
8
+ VALUES (?, ?, ?, ?, ?, ?, ?)
9
+ ON CONFLICT(name) DO UPDATE SET
10
+ encrypted_data = excluded.encrypted_data, salt = excluded.salt, iv = excluded.iv, auth_tag = excluded.auth_tag,
11
+ updated_at = excluded.updated_at
12
+ RETURNING created_at, updated_at`;
13
+ export const UPDATE_CREDENTIAL_IF_UPDATED_AT_MATCHES = `
14
+ UPDATE credentials
15
+ SET encrypted_data = ?, salt = ?, iv = ?, auth_tag = ?, updated_at = ?
16
+ WHERE name = ? AND updated_at = ?`;
10
17
  export const SELECT_CREDENTIAL = `
11
- SELECT agent, name, type, provider, encrypted_data, salt, iv, auth_tag, created_at, updated_at, expires_at
12
- FROM credentials WHERE agent = ? AND name = ?`;
18
+ SELECT name, encrypted_data, salt, iv, auth_tag, created_at, updated_at
19
+ FROM credentials WHERE name = ?`;
13
20
  export const SELECT_ALL_METADATA = `
14
- SELECT agent, name, type, provider, created_at, updated_at, expires_at
15
- FROM credentials ORDER BY agent, name`;
21
+ SELECT name, created_at, updated_at
22
+ FROM credentials ORDER BY name`;
23
+ export const SELECT_METADATA_PAGINATED = `
24
+ SELECT name, created_at, updated_at
25
+ FROM credentials
26
+ WHERE name > ?
27
+ ORDER BY name
28
+ LIMIT ?`;
29
+ export const SELECT_METADATA_FIRST_PAGE = `
30
+ SELECT name, created_at, updated_at
31
+ FROM credentials
32
+ ORDER BY name
33
+ LIMIT ?`;
16
34
  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 = ?`;
35
+ DELETE FROM credentials WHERE name = ?`;
@@ -1,32 +1,49 @@
1
1
  /**
2
2
  * Credentials repository.
3
3
  *
4
- * Manages encrypted credential storage.
4
+ * Manages encrypted credential storage with name-only keys.
5
+ * Credentials are fully opaque blobs.
5
6
  */
6
7
  import type Database from "better-sqlite3";
7
- import type { CredentialType } from "axshared";
8
8
  import { type CredentialMetadata, type CredentialRecord } from "./parse-credential-row.js";
9
- /** Store or update a credential */
9
+ /** Timestamps returned after storing a credential */
10
+ interface UpsertTimestamps {
11
+ createdAt: Date;
12
+ updatedAt: Date;
13
+ }
14
+ /** Store or update a credential, returning the resulting timestamps */
10
15
  declare function upsertCredential(database: Database.Database, credential: {
11
- agent: string;
12
16
  name: string;
13
- type: CredentialType;
14
- provider?: string;
15
17
  encryptedData: Buffer;
16
18
  salt: Buffer;
17
19
  iv: Buffer;
18
20
  authTag: Buffer;
19
- expiresAt?: Date;
20
- }): void;
21
- /** Get a credential by agent and name */
22
- declare function getCredential(database: Database.Database, agent: string, name: string): CredentialRecord | undefined;
21
+ }): UpsertTimestamps;
22
+ /**
23
+ * Update a credential only if its updated_at matches the expected value.
24
+ *
25
+ * Used to avoid overwriting concurrent PUT/DELETE changes when performing
26
+ * background refreshes during GET.
27
+ */
28
+ declare function updateCredentialIfUnchanged(database: Database.Database, credential: {
29
+ name: string;
30
+ encryptedData: Buffer;
31
+ salt: Buffer;
32
+ iv: Buffer;
33
+ authTag: Buffer;
34
+ expectedUpdatedAt: Date;
35
+ }): {
36
+ updated: boolean;
37
+ updatedAt: Date;
38
+ };
39
+ /** Get a credential by name */
40
+ declare function getCredential(database: Database.Database, name: string): CredentialRecord | undefined;
23
41
  /** List all credentials (metadata only) */
24
42
  declare function listCredentials(database: Database.Database): CredentialMetadata[];
25
43
  /** List credentials accessible by an API key's read access list */
26
44
  declare function listCredentialsForApiKey(database: Database.Database, readAccess: string[]): CredentialMetadata[];
27
45
  /** Delete a credential */
28
- declare function deleteCredential(database: Database.Database, agent: string, name: string): boolean;
29
- /** Update expiration time after refresh */
30
- declare function updateExpiresAt(database: Database.Database, agent: string, name: string, expiresAt: Date | undefined): void;
31
- export { deleteCredential, getCredential, listCredentials, listCredentialsForApiKey, updateExpiresAt, upsertCredential, };
46
+ declare function deleteCredential(database: Database.Database, name: string): boolean;
47
+ export { deleteCredential, getCredential, listCredentials, listCredentialsForApiKey, updateCredentialIfUnchanged, upsertCredential, };
48
+ export { listCredentialsPaginated } from "./list-credentials-paginated.js";
32
49
  export type { CredentialMetadata, CredentialRecord, } from "./parse-credential-row.js";
@@ -1,24 +1,38 @@
1
1
  /**
2
2
  * Credentials repository.
3
3
  *
4
- * Manages encrypted credential storage.
4
+ * Manages encrypted credential storage with name-only keys.
5
+ * Credentials are fully opaque blobs.
5
6
  */
6
7
  import * as SQL from "./credentials-queries.js";
7
8
  import { rowToMetadata, rowToRecord, } from "./parse-credential-row.js";
8
- /** Store or update a credential */
9
+ /** Store or update a credential, returning the resulting timestamps */
9
10
  function upsertCredential(database, credential) {
10
11
  const now = Date.now();
11
- // eslint-disable-next-line unicorn/no-null -- SQLite requires null for NULL values
12
- const expiresAt = credential.expiresAt?.getTime() ?? null;
13
- // eslint-disable-next-line unicorn/no-null -- SQLite requires null for NULL values
14
- const provider = credential.provider ?? null;
15
- database
12
+ const row = database
16
13
  .prepare(SQL.UPSERT_CREDENTIAL)
17
- .run(credential.agent, credential.name, credential.type, provider, credential.encryptedData, credential.salt, credential.iv, credential.authTag, now, now, expiresAt);
14
+ .get(credential.name, credential.encryptedData, credential.salt, credential.iv, credential.authTag, now, now);
15
+ return {
16
+ createdAt: new Date(row.created_at),
17
+ updatedAt: new Date(row.updated_at),
18
+ };
18
19
  }
19
- /** Get a credential by agent and name */
20
- function getCredential(database, agent, name) {
21
- const row = database.prepare(SQL.SELECT_CREDENTIAL).get(agent, name);
20
+ /**
21
+ * Update a credential only if its updated_at matches the expected value.
22
+ *
23
+ * Used to avoid overwriting concurrent PUT/DELETE changes when performing
24
+ * background refreshes during GET.
25
+ */
26
+ function updateCredentialIfUnchanged(database, credential) {
27
+ const now = Date.now();
28
+ const changes = database
29
+ .prepare(SQL.UPDATE_CREDENTIAL_IF_UPDATED_AT_MATCHES)
30
+ .run(credential.encryptedData, credential.salt, credential.iv, credential.authTag, now, credential.name, credential.expectedUpdatedAt.getTime()).changes;
31
+ return { updated: changes > 0, updatedAt: new Date(now) };
32
+ }
33
+ /** Get a credential by name */
34
+ function getCredential(database, name) {
35
+ const row = database.prepare(SQL.SELECT_CREDENTIAL).get(name);
22
36
  return row ? rowToRecord(row) : undefined;
23
37
  }
24
38
  /** List all credentials (metadata only) */
@@ -30,16 +44,20 @@ function listCredentials(database) {
30
44
  function listCredentialsForApiKey(database, readAccess) {
31
45
  if (readAccess.includes("*"))
32
46
  return listCredentials(database);
33
- return listCredentials(database).filter((cred) => readAccess.includes(`${cred.agent}/${cred.name}`));
47
+ if (readAccess.length === 0)
48
+ return [];
49
+ const placeholders = readAccess.map(() => "?").join(", ");
50
+ const sql = `
51
+ SELECT name, created_at, updated_at
52
+ FROM credentials
53
+ WHERE name IN (${placeholders})
54
+ ORDER BY name`;
55
+ const rows = database.prepare(sql).all(...readAccess);
56
+ return rows.map((row) => rowToMetadata(row));
34
57
  }
35
58
  /** Delete a credential */
36
- function deleteCredential(database, agent, name) {
37
- return database.prepare(SQL.DELETE_CREDENTIAL).run(agent, name).changes > 0;
38
- }
39
- /** Update expiration time after refresh */
40
- function updateExpiresAt(database, agent, name, expiresAt) {
41
- // eslint-disable-next-line unicorn/no-null -- SQLite requires null for NULL values
42
- const expires = expiresAt?.getTime() ?? null;
43
- database.prepare(SQL.UPDATE_EXPIRES_AT).run(expires, Date.now(), agent, name);
59
+ function deleteCredential(database, name) {
60
+ return database.prepare(SQL.DELETE_CREDENTIAL).run(name).changes > 0;
44
61
  }
45
- export { deleteCredential, getCredential, listCredentials, listCredentialsForApiKey, updateExpiresAt, upsertCredential, };
62
+ export { deleteCredential, getCredential, listCredentials, listCredentialsForApiKey, updateCredentialIfUnchanged, upsertCredential, };
63
+ export { listCredentialsPaginated } from "./list-credentials-paginated.js";
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Paginated credential listing with SQL-level filtering.
3
+ *
4
+ * Both wildcard and specific access lists use SQL-level pagination,
5
+ * avoiding full table scans of the credentials table.
6
+ */
7
+ import type Database from "better-sqlite3";
8
+ import { type CredentialMetadata } from "./parse-credential-row.js";
9
+ /** Pagination options for listing credentials */
10
+ interface PaginationOptions {
11
+ cursor: string | undefined;
12
+ limit: number;
13
+ }
14
+ /** Paginated result with optional next cursor */
15
+ interface PaginatedResult {
16
+ credentials: CredentialMetadata[];
17
+ nextCursor: string | undefined;
18
+ }
19
+ /**
20
+ * List credentials with cursor-based pagination.
21
+ *
22
+ * Fetches limit+1 rows to detect whether more results exist.
23
+ * Both wildcard and specific access lists use SQL-level pagination.
24
+ */
25
+ declare function listCredentialsPaginated(database: Database.Database, readAccess: string[], options: PaginationOptions): PaginatedResult;
26
+ export { listCredentialsPaginated };
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Paginated credential listing with SQL-level filtering.
3
+ *
4
+ * Both wildcard and specific access lists use SQL-level pagination,
5
+ * avoiding full table scans of the credentials table.
6
+ */
7
+ import * as SQL from "./credentials-queries.js";
8
+ import { rowToMetadata, } from "./parse-credential-row.js";
9
+ /**
10
+ * Build a parameterized query for fetching metadata filtered by an IN clause.
11
+ *
12
+ * Generates `WHERE name IN (?, ?, ...)` with optional cursor and limit.
13
+ * All filtering and pagination happen in SQL — no full table scan needed.
14
+ */
15
+ function buildFilteredQuery(names, cursor, limit) {
16
+ const placeholders = names.map(() => "?").join(", ");
17
+ const cursorClause = cursor ? " AND name > ?" : "";
18
+ const sql = `
19
+ SELECT name, created_at, updated_at
20
+ FROM credentials
21
+ WHERE name IN (${placeholders})${cursorClause}
22
+ ORDER BY name
23
+ LIMIT ?`;
24
+ const parameters = [...names];
25
+ if (cursor)
26
+ parameters.push(cursor);
27
+ parameters.push(limit);
28
+ return { sql, parameters };
29
+ }
30
+ /** Extract pagination result from a limit+1 fetch */
31
+ function paginateResults(rows, limit) {
32
+ if (rows.length > limit) {
33
+ const credentials = rows.slice(0, limit);
34
+ const lastCredential = credentials.at(-1);
35
+ return {
36
+ credentials,
37
+ nextCursor: lastCredential?.name,
38
+ };
39
+ }
40
+ return { credentials: rows, nextCursor: undefined };
41
+ }
42
+ /**
43
+ * List credentials with cursor-based pagination.
44
+ *
45
+ * Fetches limit+1 rows to detect whether more results exist.
46
+ * Both wildcard and specific access lists use SQL-level pagination.
47
+ */
48
+ function listCredentialsPaginated(database, readAccess, options) {
49
+ const fetchLimit = options.limit + 1;
50
+ if (readAccess.includes("*")) {
51
+ const rows = options.cursor
52
+ ? database
53
+ .prepare(SQL.SELECT_METADATA_PAGINATED)
54
+ .all(options.cursor, fetchLimit)
55
+ : database
56
+ .prepare(SQL.SELECT_METADATA_FIRST_PAGE)
57
+ .all(fetchLimit);
58
+ const credentials = rows.map((row) => rowToMetadata(row));
59
+ return paginateResults(credentials, options.limit);
60
+ }
61
+ if (readAccess.length === 0) {
62
+ return { credentials: [], nextCursor: undefined };
63
+ }
64
+ const { sql, parameters } = buildFilteredQuery(readAccess, options.cursor, fetchLimit);
65
+ const rows = database.prepare(sql).all(...parameters);
66
+ const credentials = rows.map((row) => rowToMetadata(row));
67
+ return paginateResults(credentials, options.limit);
68
+ }
69
+ export { listCredentialsPaginated };
@@ -1,31 +1,24 @@
1
1
  /**
2
2
  * Pure functions for parsing credential database rows.
3
+ *
4
+ * Credentials are fully opaque blobs - we don't inspect their structure.
3
5
  */
4
- import { CredentialType } from "axshared";
5
6
  import type { CredentialRow, MetadataRow } from "../types.js";
6
7
  /** Credential record stored in database */
7
8
  interface CredentialRecord {
8
- agent: string;
9
9
  name: string;
10
- type: CredentialType;
11
- provider: string | undefined;
12
10
  encryptedData: Buffer;
13
11
  salt: Buffer;
14
12
  iv: Buffer;
15
13
  authTag: Buffer;
16
14
  createdAt: Date;
17
15
  updatedAt: Date;
18
- expiresAt: Date | undefined;
19
16
  }
20
17
  /** Credential metadata (without encrypted data) */
21
18
  interface CredentialMetadata {
22
- agent: string;
23
19
  name: string;
24
- type: CredentialType;
25
- provider: string | undefined;
26
20
  createdAt: Date;
27
21
  updatedAt: Date;
28
- expiresAt: Date | undefined;
29
22
  }
30
23
  /** Convert database row to record */
31
24
  declare function rowToRecord(row: CredentialRow): CredentialRecord;