axvault 1.0.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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +213 -0
  3. package/bin/axvault +2 -0
  4. package/dist/cli.d.ts +7 -0
  5. package/dist/cli.js +108 -0
  6. package/dist/commands/credential.d.ts +13 -0
  7. package/dist/commands/credential.js +113 -0
  8. package/dist/commands/init.d.ts +8 -0
  9. package/dist/commands/init.js +50 -0
  10. package/dist/commands/key.d.ts +21 -0
  11. package/dist/commands/key.js +157 -0
  12. package/dist/commands/serve.d.ts +12 -0
  13. package/dist/commands/serve.js +93 -0
  14. package/dist/config.d.ts +22 -0
  15. package/dist/config.js +44 -0
  16. package/dist/db/client.d.ts +13 -0
  17. package/dist/db/client.js +38 -0
  18. package/dist/db/migrations.d.ts +12 -0
  19. package/dist/db/migrations.js +96 -0
  20. package/dist/db/repositories/api-keys.d.ts +42 -0
  21. package/dist/db/repositories/api-keys.js +86 -0
  22. package/dist/db/repositories/audit-log.d.ts +37 -0
  23. package/dist/db/repositories/audit-log.js +58 -0
  24. package/dist/db/repositories/credentials.d.ts +48 -0
  25. package/dist/db/repositories/credentials.js +79 -0
  26. package/dist/db/types.d.ts +44 -0
  27. package/dist/db/types.js +4 -0
  28. package/dist/handlers/delete-credential.d.ts +15 -0
  29. package/dist/handlers/delete-credential.js +50 -0
  30. package/dist/handlers/get-credential.d.ts +21 -0
  31. package/dist/handlers/get-credential.js +143 -0
  32. package/dist/handlers/list-credentials.d.ts +12 -0
  33. package/dist/handlers/list-credentials.js +29 -0
  34. package/dist/handlers/put-credential.d.ts +15 -0
  35. package/dist/handlers/put-credential.js +94 -0
  36. package/dist/index.d.ts +18 -0
  37. package/dist/index.js +14 -0
  38. package/dist/lib/encryption.d.ts +17 -0
  39. package/dist/lib/encryption.js +38 -0
  40. package/dist/lib/format.d.ts +92 -0
  41. package/dist/lib/format.js +216 -0
  42. package/dist/middleware/auth.d.ts +21 -0
  43. package/dist/middleware/auth.js +50 -0
  44. package/dist/middleware/validate-parameters.d.ts +10 -0
  45. package/dist/middleware/validate-parameters.js +26 -0
  46. package/dist/refresh/check-refresh.d.ts +40 -0
  47. package/dist/refresh/check-refresh.js +83 -0
  48. package/dist/refresh/log-refresh.d.ts +17 -0
  49. package/dist/refresh/log-refresh.js +35 -0
  50. package/dist/refresh/refresh-manager.d.ts +51 -0
  51. package/dist/refresh/refresh-manager.js +132 -0
  52. package/dist/server/routes.d.ts +12 -0
  53. package/dist/server/routes.js +44 -0
  54. package/dist/server/server.d.ts +18 -0
  55. package/dist/server/server.js +106 -0
  56. package/package.json +93 -0
@@ -0,0 +1,157 @@
1
+ /**
2
+ * API key management command handlers.
3
+ */
4
+ import { getServerConfig } from "../config.js";
5
+ import { closeDatabase, getDatabase } from "../db/client.js";
6
+ import { runMigrations } from "../db/migrations.js";
7
+ import { createApiKey, deleteApiKey, listApiKeys, } from "../db/repositories/api-keys.js";
8
+ import { containsControlChars, formatAccessList, formatDateForJson, formatKeyRow, getAccessListErrorMessage, getErrorMessage, isValidKeyId, normalizeAccessList, parseAccessList, sanitizeForTsv, } from "../lib/format.js";
9
+ export function handleKeyCreate(options) {
10
+ // Validate key name (reject control characters)
11
+ if (containsControlChars(options.name)) {
12
+ console.error("Error: Key name contains control characters.");
13
+ process.exitCode = 2;
14
+ return;
15
+ }
16
+ // Parse and validate access lists
17
+ const readResult = parseAccessList(options.read);
18
+ const writeResult = parseAccessList(options.write);
19
+ if (readResult.error) {
20
+ console.error(`Error: ${getAccessListErrorMessage(readResult.error)}`);
21
+ process.exitCode = 2;
22
+ return;
23
+ }
24
+ if (writeResult.error) {
25
+ console.error(`Error: ${getAccessListErrorMessage(writeResult.error)}`);
26
+ process.exitCode = 2;
27
+ return;
28
+ }
29
+ // Normalize wildcards (warn if mixed with specific entries)
30
+ const readNorm = normalizeAccessList(readResult.entries, "read");
31
+ const writeNorm = normalizeAccessList(writeResult.entries, "write");
32
+ if (readNorm.warning)
33
+ console.warn(`Warning: ${readNorm.warning}`);
34
+ if (writeNorm.warning)
35
+ console.warn(`Warning: ${writeNorm.warning}`);
36
+ const readAccess = readNorm.normalized;
37
+ const writeAccess = writeNorm.normalized;
38
+ // Validate: at least one access must be specified
39
+ if (readAccess.length === 0 && writeAccess.length === 0) {
40
+ console.error("Error: At least one of --read or --write must be specified.");
41
+ console.error("Try 'axvault key create --help' for more information.");
42
+ process.exitCode = 2;
43
+ return;
44
+ }
45
+ try {
46
+ const config = getServerConfig(options);
47
+ const database = getDatabase(config.databasePath);
48
+ runMigrations(database);
49
+ const apiKey = createApiKey(database, {
50
+ name: options.name,
51
+ readAccess,
52
+ writeAccess,
53
+ });
54
+ if (options.json) {
55
+ console.warn("Warning: JSON output contains the secret key. Avoid logging in CI.");
56
+ console.log(JSON.stringify({
57
+ id: apiKey.id,
58
+ name: apiKey.name,
59
+ key: apiKey.key,
60
+ readAccess: apiKey.readAccess,
61
+ writeAccess: apiKey.writeAccess,
62
+ createdAt: apiKey.createdAt.toISOString(),
63
+ }, undefined, 2));
64
+ }
65
+ else {
66
+ console.error(`Created API key: ${sanitizeForTsv(apiKey.name)}`);
67
+ console.error(`ID: ${sanitizeForTsv(apiKey.id)}`);
68
+ console.error(`Read access: ${sanitizeForTsv(formatAccessList(apiKey.readAccess))}`);
69
+ console.error(`Write access: ${sanitizeForTsv(formatAccessList(apiKey.writeAccess))}`);
70
+ console.error("");
71
+ // Output the secret key to stdout for piping
72
+ console.log(apiKey.key);
73
+ console.error("");
74
+ console.error("Save this key securely - it cannot be retrieved later.");
75
+ }
76
+ }
77
+ catch (error) {
78
+ console.error(`Error: Failed to create API key: ${getErrorMessage(error)}`);
79
+ process.exitCode = 1;
80
+ }
81
+ finally {
82
+ closeDatabase();
83
+ }
84
+ }
85
+ export function handleKeyList(options) {
86
+ try {
87
+ const config = getServerConfig(options);
88
+ const database = getDatabase(config.databasePath);
89
+ runMigrations(database);
90
+ const keys = listApiKeys(database);
91
+ if (options.json) {
92
+ const output = keys.map((key) => ({
93
+ id: key.id,
94
+ name: key.name,
95
+ readAccess: key.readAccess,
96
+ writeAccess: key.writeAccess,
97
+ createdAt: key.createdAt.toISOString(),
98
+ lastUsedAt: formatDateForJson(key.lastUsedAt),
99
+ }));
100
+ console.log(JSON.stringify(output, undefined, 2));
101
+ }
102
+ else if (keys.length === 0) {
103
+ console.error("No API keys found.\nCreate one with: axvault key create --name <name> [--read <access>] [--write <access>]");
104
+ }
105
+ else {
106
+ console.log("ID\tNAME\tREAD ACCESS\tWRITE ACCESS\tLAST USED");
107
+ for (const key of keys)
108
+ console.log(formatKeyRow(key));
109
+ }
110
+ }
111
+ catch (error) {
112
+ console.error(`Error: Failed to list API keys: ${getErrorMessage(error)}`);
113
+ process.exitCode = 1;
114
+ }
115
+ finally {
116
+ closeDatabase();
117
+ }
118
+ }
119
+ export function handleKeyRevoke(id, options) {
120
+ // Reject inputs containing control characters BEFORE trimming
121
+ // (prevents silent bypass where \n or \t would be removed by trim)
122
+ if (containsControlChars(id)) {
123
+ console.error("Error: Invalid API key ID: contains control characters.");
124
+ console.error("API key IDs have format: k_<12 hex chars> (e.g., k_abc123def456)");
125
+ process.exitCode = 2;
126
+ return;
127
+ }
128
+ // Trim whitespace from ID (common copy-paste issue)
129
+ const trimmedId = id.trim();
130
+ // Validate ID format (k_ + 12 hex chars)
131
+ if (!isValidKeyId(trimmedId)) {
132
+ console.error(`Error: Invalid API key ID format: ${trimmedId}`);
133
+ console.error("API key IDs have format: k_<12 hex chars> (e.g., k_abc123def456)");
134
+ process.exitCode = 2;
135
+ return;
136
+ }
137
+ try {
138
+ const config = getServerConfig(options);
139
+ const database = getDatabase(config.databasePath);
140
+ runMigrations(database);
141
+ const deleted = deleteApiKey(database, trimmedId);
142
+ if (deleted) {
143
+ console.error(`Revoked API key: ${sanitizeForTsv(trimmedId)}`);
144
+ }
145
+ else {
146
+ console.error(`Error: API key not found: ${sanitizeForTsv(trimmedId)}`);
147
+ process.exitCode = 1;
148
+ }
149
+ }
150
+ catch (error) {
151
+ console.error(`Error: Failed to revoke API key: ${getErrorMessage(error)}`);
152
+ process.exitCode = 1;
153
+ }
154
+ finally {
155
+ closeDatabase();
156
+ }
157
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Start server command handler.
3
+ */
4
+ interface ServeOptions {
5
+ port?: string;
6
+ host?: string;
7
+ dbPath?: string;
8
+ refreshThreshold?: string;
9
+ refreshTimeout?: string;
10
+ }
11
+ export declare function handleServe(options: ServeOptions): Promise<void>;
12
+ export {};
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Start server command handler.
3
+ */
4
+ import { existsSync, mkdirSync } from "node:fs";
5
+ import path from "node:path";
6
+ import { getServerConfig } from "../config.js";
7
+ import { getDatabase, closeDatabase } from "../db/client.js";
8
+ import { runMigrations } from "../db/migrations.js";
9
+ import { createHealthRouter, createCredentialRouter, } from "../server/routes.js";
10
+ import { createServer } from "../server/server.js";
11
+ export async function handleServe(options) {
12
+ let config;
13
+ try {
14
+ config = getServerConfig({
15
+ ...options,
16
+ refreshThresholdSeconds: options.refreshThreshold,
17
+ refreshTimeoutMs: options.refreshTimeout,
18
+ });
19
+ }
20
+ catch (error) {
21
+ const message = error instanceof Error ? error.message : String(error);
22
+ console.error(`Error: ${message}`);
23
+ // eslint-disable-next-line unicorn/no-process-exit -- CLI config error
24
+ process.exit(1);
25
+ }
26
+ // Check encryption key is set
27
+ const encryptionKey = process.env["AXVAULT_ENCRYPTION_KEY"];
28
+ if (!encryptionKey || encryptionKey.length < 32) {
29
+ console.error("Error: AXVAULT_ENCRYPTION_KEY must be set to at least 32 characters");
30
+ // eslint-disable-next-line unicorn/no-process-exit -- CLI config error
31
+ process.exit(1);
32
+ }
33
+ // Ensure data directory exists (enables direct execution without init command)
34
+ const dataDirectory = path.dirname(config.databasePath);
35
+ if (!existsSync(dataDirectory)) {
36
+ try {
37
+ mkdirSync(dataDirectory, { recursive: true });
38
+ console.error(`Created data directory: ${dataDirectory}`);
39
+ }
40
+ catch (error) {
41
+ const message = error instanceof Error ? error.message : String(error);
42
+ console.error(`Failed to create data directory '${dataDirectory}': ${message}`);
43
+ // eslint-disable-next-line unicorn/no-process-exit -- CLI startup failure
44
+ process.exit(1);
45
+ }
46
+ }
47
+ // Initialize database
48
+ let database;
49
+ try {
50
+ database = getDatabase(config.databasePath);
51
+ runMigrations(database);
52
+ }
53
+ catch (error) {
54
+ const message = error instanceof Error ? error.message : String(error);
55
+ console.error(`Failed to initialize database: ${message}`);
56
+ // eslint-disable-next-line unicorn/no-process-exit -- CLI startup failure
57
+ process.exit(1);
58
+ }
59
+ // Create server with routers
60
+ const server = createServer(config, [
61
+ createHealthRouter(),
62
+ createCredentialRouter(database, {
63
+ refreshThresholdSeconds: config.refreshThresholdSeconds,
64
+ refreshTimeoutMs: config.refreshTimeoutMs,
65
+ }),
66
+ ]);
67
+ // Graceful shutdown handler
68
+ const shutdown = () => {
69
+ console.error("Shutting down...");
70
+ server.stop().then(() => {
71
+ closeDatabase();
72
+ // eslint-disable-next-line unicorn/no-process-exit -- CLI graceful shutdown
73
+ process.exit(0);
74
+ }, (error) => {
75
+ console.error("Error during shutdown:", error);
76
+ closeDatabase();
77
+ // eslint-disable-next-line unicorn/no-process-exit -- CLI graceful shutdown
78
+ process.exit(1);
79
+ });
80
+ };
81
+ process.once("SIGTERM", shutdown);
82
+ process.once("SIGINT", shutdown);
83
+ try {
84
+ await server.start();
85
+ }
86
+ catch (error) {
87
+ const message = error instanceof Error ? error.message : String(error);
88
+ console.error(`Failed to start server on http://${config.host}:${config.port}:`, message);
89
+ closeDatabase();
90
+ // eslint-disable-next-line unicorn/no-process-exit -- CLI startup failure
91
+ process.exit(1);
92
+ }
93
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Configuration for axvault server.
3
+ *
4
+ * Priority: CLI flags > environment variables > defaults.
5
+ */
6
+ export interface ServerConfig {
7
+ port: number;
8
+ host: string;
9
+ databasePath: string;
10
+ refreshThresholdSeconds: number;
11
+ refreshTimeoutMs: number;
12
+ }
13
+ interface ConfigOverrides {
14
+ port?: string;
15
+ host?: string;
16
+ dbPath?: string;
17
+ refreshThresholdSeconds?: string;
18
+ refreshTimeoutMs?: string;
19
+ }
20
+ /** Parse config from environment and CLI overrides */
21
+ export declare function getServerConfig(overrides?: ConfigOverrides): ServerConfig;
22
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Configuration for axvault server.
3
+ *
4
+ * Priority: CLI flags > environment variables > defaults.
5
+ */
6
+ const DEFAULT_PORT = 3847;
7
+ const DEFAULT_HOST = "127.0.0.1";
8
+ const DEFAULT_DATABASE_PATH = "./data/axvault.db";
9
+ const DEFAULT_REFRESH_THRESHOLD_SECONDS = 3600; // 1 hour
10
+ const DEFAULT_REFRESH_TIMEOUT_MS = 30_000; // 30 seconds
11
+ /** Parse config from environment and CLI overrides */
12
+ export function getServerConfig(overrides = {}) {
13
+ const port = parsePort(overrides.port ?? process.env.AXVAULT_PORT);
14
+ const host = overrides.host ?? process.env.AXVAULT_HOST ?? DEFAULT_HOST;
15
+ const databasePath = overrides.dbPath ?? process.env.AXVAULT_DB_PATH ?? DEFAULT_DATABASE_PATH;
16
+ const refreshThresholdSeconds = parseNonNegativeInt(overrides.refreshThresholdSeconds ?? process.env.AXVAULT_REFRESH_THRESHOLD, DEFAULT_REFRESH_THRESHOLD_SECONDS, "AXVAULT_REFRESH_THRESHOLD");
17
+ const refreshTimeoutMs = parseNonNegativeInt(overrides.refreshTimeoutMs ?? process.env.AXVAULT_REFRESH_TIMEOUT_MS, DEFAULT_REFRESH_TIMEOUT_MS, "AXVAULT_REFRESH_TIMEOUT_MS");
18
+ return {
19
+ port,
20
+ host,
21
+ databasePath,
22
+ refreshThresholdSeconds,
23
+ refreshTimeoutMs,
24
+ };
25
+ }
26
+ function parsePort(value) {
27
+ if (!value)
28
+ return DEFAULT_PORT;
29
+ const port = Number.parseInt(value);
30
+ if (Number.isNaN(port) || port < 1 || port > 65_535) {
31
+ throw new Error(`Invalid port: ${value}`);
32
+ }
33
+ return port;
34
+ }
35
+ function parseNonNegativeInt(value, defaultValue, name) {
36
+ if (!value)
37
+ return defaultValue;
38
+ const parsed = Number.parseInt(value);
39
+ if (Number.isNaN(parsed) || parsed < 0) {
40
+ console.warn(`Invalid ${name} value "${value}", using default ${defaultValue}`);
41
+ return defaultValue;
42
+ }
43
+ return parsed;
44
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * SQLite database client.
3
+ *
4
+ * Manages a single database connection with WAL mode for better concurrency.
5
+ */
6
+ import Database from "better-sqlite3";
7
+ /** Get the database connection, creating it if necessary */
8
+ declare function getDatabase(databasePath: string): Database.Database;
9
+ /** Close the database connection */
10
+ declare function closeDatabase(): void;
11
+ /** Check if database is connected */
12
+ declare function isDatabaseConnected(): boolean;
13
+ export { closeDatabase, getDatabase, isDatabaseConnected };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * SQLite database client.
3
+ *
4
+ * Manages a single database connection with WAL mode for better concurrency.
5
+ */
6
+ import Database from "better-sqlite3";
7
+ let database;
8
+ let currentPath;
9
+ /** Get the database connection, creating it if necessary */
10
+ function getDatabase(databasePath) {
11
+ if (database) {
12
+ if (currentPath !== databasePath) {
13
+ throw new Error(`Database already connected to '${currentPath}'. ` +
14
+ `Call closeDatabase() before connecting to '${databasePath}'.`);
15
+ }
16
+ return database;
17
+ }
18
+ database = new Database(databasePath);
19
+ currentPath = databasePath;
20
+ // Enable WAL mode for better concurrent read performance
21
+ database.pragma("journal_mode = WAL");
22
+ // Enable foreign keys
23
+ database.pragma("foreign_keys = ON");
24
+ return database;
25
+ }
26
+ /** Close the database connection */
27
+ function closeDatabase() {
28
+ if (database) {
29
+ database.close();
30
+ database = undefined;
31
+ currentPath = undefined;
32
+ }
33
+ }
34
+ /** Check if database is connected */
35
+ function isDatabaseConnected() {
36
+ return database !== undefined;
37
+ }
38
+ export { closeDatabase, getDatabase, isDatabaseConnected };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Database schema migrations.
3
+ *
4
+ * Uses a simple version-based migration system.
5
+ */
6
+ import type Database from "better-sqlite3";
7
+ declare const CURRENT_VERSION = 2;
8
+ /** Run all pending migrations */
9
+ declare function runMigrations(database: Database.Database): void;
10
+ /** Get current schema version */
11
+ declare function getSchemaVersion(database: Database.Database): number;
12
+ export { CURRENT_VERSION, getSchemaVersion, runMigrations };
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Database schema migrations.
3
+ *
4
+ * Uses a simple version-based migration system.
5
+ */
6
+ const CURRENT_VERSION = 2;
7
+ /** Run all pending migrations */
8
+ function runMigrations(database) {
9
+ const version = getSchemaVersion(database);
10
+ if (version < 1) {
11
+ migrateToV1(database);
12
+ }
13
+ if (version < 2) {
14
+ migrateToV2(database);
15
+ }
16
+ }
17
+ /** Get current schema version */
18
+ function getSchemaVersion(database) {
19
+ const result = database.pragma("user_version", { simple: true });
20
+ return typeof result === "number" ? result : 0;
21
+ }
22
+ /** Set schema version */
23
+ function setSchemaVersion(database, version) {
24
+ database.pragma(`user_version = ${version}`);
25
+ }
26
+ /**
27
+ * Migration to version 1: Initial schema
28
+ *
29
+ * Creates tables for API keys, credentials, and audit log.
30
+ */
31
+ function migrateToV1(database) {
32
+ database.transaction(() => {
33
+ // API keys with credential access lists
34
+ database.exec(`
35
+ CREATE TABLE api_keys (
36
+ id TEXT PRIMARY KEY,
37
+ name TEXT NOT NULL,
38
+ key_hash TEXT NOT NULL UNIQUE,
39
+ read_access TEXT NOT NULL,
40
+ write_access TEXT NOT NULL,
41
+ created_at INTEGER NOT NULL,
42
+ last_used_at INTEGER
43
+ )
44
+ `);
45
+ // Create index for key lookup
46
+ database.exec(`
47
+ CREATE INDEX idx_api_keys_key_hash ON api_keys(key_hash)
48
+ `);
49
+ // Encrypted credentials
50
+ database.exec(`
51
+ CREATE TABLE credentials (
52
+ agent TEXT NOT NULL,
53
+ name TEXT NOT NULL,
54
+ encrypted_data BLOB NOT NULL,
55
+ iv BLOB NOT NULL,
56
+ auth_tag BLOB NOT NULL,
57
+ created_at INTEGER NOT NULL,
58
+ updated_at INTEGER NOT NULL,
59
+ expires_at INTEGER,
60
+ PRIMARY KEY (agent, name)
61
+ )
62
+ `);
63
+ // Audit log - intentionally no FK on api_key_id to preserve logs after key deletion
64
+ database.exec(`
65
+ CREATE TABLE audit_log (
66
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
67
+ timestamp INTEGER NOT NULL,
68
+ api_key_id TEXT,
69
+ action TEXT NOT NULL,
70
+ agent TEXT,
71
+ name TEXT,
72
+ success INTEGER NOT NULL,
73
+ error_message TEXT
74
+ )
75
+ `);
76
+ // Create index for audit log queries by time
77
+ database.exec(`
78
+ CREATE INDEX idx_audit_log_timestamp ON audit_log(timestamp DESC)
79
+ `);
80
+ setSchemaVersion(database, 1);
81
+ })();
82
+ }
83
+ /**
84
+ * Migration to version 2: Add salt column to credentials
85
+ *
86
+ * AES-256-GCM with PBKDF2 requires a salt for key derivation.
87
+ */
88
+ function migrateToV2(database) {
89
+ database.transaction(() => {
90
+ database.exec(`
91
+ ALTER TABLE credentials ADD COLUMN salt BLOB
92
+ `);
93
+ setSchemaVersion(database, 2);
94
+ })();
95
+ }
96
+ export { CURRENT_VERSION, getSchemaVersion, runMigrations };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * API key repository.
3
+ *
4
+ * Manages API keys with read/write access lists for credentials.
5
+ */
6
+ import type Database from "better-sqlite3";
7
+ /** API key data stored in database */
8
+ interface ApiKeyRecord {
9
+ id: string;
10
+ name: string;
11
+ keyHash: string;
12
+ readAccess: string[];
13
+ writeAccess: string[];
14
+ createdAt: Date;
15
+ lastUsedAt: Date | undefined;
16
+ }
17
+ /** API key with the raw key (only returned on creation) */
18
+ interface ApiKeyWithSecret extends ApiKeyRecord {
19
+ key: string;
20
+ }
21
+ /** Create a new API key */
22
+ declare function createApiKey(database: Database.Database, options: {
23
+ name: string;
24
+ readAccess: string[];
25
+ writeAccess: string[];
26
+ }): ApiKeyWithSecret;
27
+ /** Find API key by raw key value */
28
+ declare function findApiKeyByKey(database: Database.Database, key: string): ApiKeyRecord | undefined;
29
+ /** Find API key by ID */
30
+ declare function findApiKeyById(database: Database.Database, id: string): ApiKeyRecord | undefined;
31
+ /** List all API keys (without revealing the raw key values) */
32
+ declare function listApiKeys(database: Database.Database): ApiKeyRecord[];
33
+ /** Update last used timestamp */
34
+ declare function updateLastUsed(database: Database.Database, id: string): void;
35
+ /** Delete an API key */
36
+ declare function deleteApiKey(database: Database.Database, id: string): boolean;
37
+ /** Check if API key has read access to a credential */
38
+ declare function hasReadAccess(apiKey: ApiKeyRecord, agent: string, name: string): boolean;
39
+ /** Check if API key has write access to a credential */
40
+ declare function hasWriteAccess(apiKey: ApiKeyRecord, agent: string, name: string): boolean;
41
+ export { createApiKey, deleteApiKey, findApiKeyById, findApiKeyByKey, hasReadAccess, hasWriteAccess, listApiKeys, updateLastUsed, };
42
+ export type { ApiKeyRecord, ApiKeyWithSecret };
@@ -0,0 +1,86 @@
1
+ /**
2
+ * API key repository.
3
+ *
4
+ * Manages API keys with read/write access lists for credentials.
5
+ */
6
+ import { createHash, randomBytes } from "node:crypto";
7
+ /** Convert database row to record */
8
+ function rowToRecord(row) {
9
+ return {
10
+ id: row.id,
11
+ name: row.name,
12
+ keyHash: row.key_hash,
13
+ readAccess: JSON.parse(row.read_access),
14
+ writeAccess: JSON.parse(row.write_access),
15
+ createdAt: new Date(row.created_at),
16
+ lastUsedAt: row.last_used_at ? new Date(row.last_used_at) : undefined,
17
+ };
18
+ }
19
+ /** Hash an API key for storage */
20
+ function hashApiKey(key) {
21
+ return createHash("sha256").update(key).digest("hex");
22
+ }
23
+ const SELECT_COLUMNS = `id, name, key_hash, read_access, write_access, created_at, last_used_at`;
24
+ /** Create a new API key */
25
+ function createApiKey(database, options) {
26
+ const id = `k_${randomBytes(6).toString("hex")}`;
27
+ const key = `axv_sk_${randomBytes(16).toString("hex")}`;
28
+ const keyHash = hashApiKey(key);
29
+ const now = Date.now();
30
+ database
31
+ .prepare(`INSERT INTO api_keys (id, name, key_hash, read_access, write_access, created_at)
32
+ VALUES (?, ?, ?, ?, ?, ?)`)
33
+ .run(id, options.name, keyHash, JSON.stringify(options.readAccess), JSON.stringify(options.writeAccess), now);
34
+ return {
35
+ id,
36
+ name: options.name,
37
+ key,
38
+ keyHash,
39
+ readAccess: options.readAccess,
40
+ writeAccess: options.writeAccess,
41
+ createdAt: new Date(now),
42
+ lastUsedAt: undefined,
43
+ };
44
+ }
45
+ /** Find API key by raw key value */
46
+ function findApiKeyByKey(database, key) {
47
+ const row = database
48
+ .prepare(`SELECT ${SELECT_COLUMNS} FROM api_keys WHERE key_hash = ?`)
49
+ .get(hashApiKey(key));
50
+ return row ? rowToRecord(row) : undefined;
51
+ }
52
+ /** Find API key by ID */
53
+ function findApiKeyById(database, id) {
54
+ const row = database
55
+ .prepare(`SELECT ${SELECT_COLUMNS} FROM api_keys WHERE id = ?`)
56
+ .get(id);
57
+ return row ? rowToRecord(row) : undefined;
58
+ }
59
+ /** List all API keys (without revealing the raw key values) */
60
+ function listApiKeys(database) {
61
+ const rows = database
62
+ .prepare(`SELECT ${SELECT_COLUMNS} FROM api_keys ORDER BY created_at DESC`)
63
+ .all();
64
+ return rows.map((row) => rowToRecord(row));
65
+ }
66
+ /** Update last used timestamp */
67
+ function updateLastUsed(database, id) {
68
+ database
69
+ .prepare(`UPDATE api_keys SET last_used_at = ? WHERE id = ?`)
70
+ .run(Date.now(), id);
71
+ }
72
+ /** Delete an API key */
73
+ function deleteApiKey(database, id) {
74
+ return (database.prepare(`DELETE FROM api_keys WHERE id = ?`).run(id).changes > 0);
75
+ }
76
+ /** Check if API key has read access to a credential */
77
+ function hasReadAccess(apiKey, agent, name) {
78
+ const path = `${agent}/${name}`;
79
+ return apiKey.readAccess.includes("*") || apiKey.readAccess.includes(path);
80
+ }
81
+ /** Check if API key has write access to a credential */
82
+ function hasWriteAccess(apiKey, agent, name) {
83
+ const path = `${agent}/${name}`;
84
+ return apiKey.writeAccess.includes("*") || apiKey.writeAccess.includes(path);
85
+ }
86
+ export { createApiKey, deleteApiKey, findApiKeyById, findApiKeyByKey, hasReadAccess, hasWriteAccess, listApiKeys, updateLastUsed, };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Audit log repository.
3
+ *
4
+ * Records all credential access for security auditing.
5
+ */
6
+ import type Database from "better-sqlite3";
7
+ /** Audit log entry */
8
+ interface AuditLogEntry {
9
+ id: number;
10
+ timestamp: Date;
11
+ apiKeyId: string | undefined;
12
+ action: "auth" | "read" | "write" | "delete" | "refresh" | "list";
13
+ agent: string | undefined;
14
+ name: string | undefined;
15
+ success: boolean;
16
+ errorMessage: string | undefined;
17
+ }
18
+ /** Log a credential access event */
19
+ declare function logAccess(database: Database.Database, entry: {
20
+ apiKeyId?: string;
21
+ action: AuditLogEntry["action"];
22
+ agent?: string;
23
+ name?: string;
24
+ success: boolean;
25
+ errorMessage?: string;
26
+ }): void;
27
+ /** Get recent audit log entries */
28
+ declare function getRecentLogs(database: Database.Database, options?: {
29
+ limit?: number;
30
+ apiKeyId?: string;
31
+ }): AuditLogEntry[];
32
+ /** Get logs for a specific credential */
33
+ declare function getLogsForCredential(database: Database.Database, agent: string, name: string, limit_?: number): AuditLogEntry[];
34
+ /** Prune old audit log entries */
35
+ declare function pruneOldLogs(database: Database.Database, olderThanDays: number): number;
36
+ export { getLogsForCredential, getRecentLogs, logAccess, pruneOldLogs };
37
+ export type { AuditLogEntry };