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,58 @@
1
+ /**
2
+ * Audit log repository.
3
+ *
4
+ * Records all credential access for security auditing.
5
+ */
6
+ /** Convert database row to entry */
7
+ function rowToEntry(row) {
8
+ return {
9
+ id: row.id,
10
+ timestamp: new Date(row.timestamp),
11
+ apiKeyId: row.api_key_id ?? undefined,
12
+ action: row.action,
13
+ agent: row.agent ?? undefined,
14
+ name: row.name ?? undefined,
15
+ success: row.success === 1,
16
+ errorMessage: row.error_message ?? undefined,
17
+ };
18
+ }
19
+ const SELECT_COLUMNS = `id, timestamp, api_key_id, action, agent, name, success, error_message`;
20
+ /** Log a credential access event */
21
+ function logAccess(database, entry) {
22
+ /* eslint-disable unicorn/no-null -- SQLite requires null for NULL values */
23
+ 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);
26
+ /* eslint-enable unicorn/no-null */
27
+ }
28
+ /** Get recent audit log entries */
29
+ function getRecentLogs(database, options) {
30
+ const limit = Math.max(1, Math.min(options?.limit ?? 100, 1000));
31
+ const parameters = [];
32
+ let query = `SELECT ${SELECT_COLUMNS} FROM audit_log`;
33
+ if (options?.apiKeyId) {
34
+ query += ` WHERE api_key_id = ?`;
35
+ parameters.push(options.apiKeyId);
36
+ }
37
+ query += ` ORDER BY timestamp DESC LIMIT ?`;
38
+ parameters.push(limit);
39
+ return database.prepare(query).all(...parameters).map((row) => rowToEntry(row));
40
+ }
41
+ /** Get logs for a specific credential */
42
+ function getLogsForCredential(database, agent, name, limit_ = 50) {
43
+ const limit = Math.max(1, Math.min(limit_, 1000));
44
+ 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);
47
+ return rows.map((row) => rowToEntry(row));
48
+ }
49
+ /** Prune old audit log entries */
50
+ function pruneOldLogs(database, olderThanDays) {
51
+ if (!Number.isFinite(olderThanDays) || olderThanDays <= 0)
52
+ return 0;
53
+ const cutoff = Date.now() - olderThanDays * 24 * 60 * 60 * 1000;
54
+ return database
55
+ .prepare(`DELETE FROM audit_log WHERE timestamp < ?`)
56
+ .run(cutoff).changes;
57
+ }
58
+ export { getLogsForCredential, getRecentLogs, logAccess, pruneOldLogs };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Credentials repository.
3
+ *
4
+ * Manages encrypted credential storage.
5
+ */
6
+ import type Database from "better-sqlite3";
7
+ /** Credential record stored in database */
8
+ interface CredentialRecord {
9
+ agent: string;
10
+ name: string;
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
+ createdAt: Date;
24
+ updatedAt: Date;
25
+ expiresAt: Date | undefined;
26
+ }
27
+ /** Store or update a credential */
28
+ declare function upsertCredential(database: Database.Database, credential: {
29
+ agent: string;
30
+ name: string;
31
+ encryptedData: Buffer;
32
+ salt: Buffer;
33
+ iv: Buffer;
34
+ authTag: Buffer;
35
+ expiresAt?: Date;
36
+ }): void;
37
+ /** Get a credential by agent and name */
38
+ declare function getCredential(database: Database.Database, agent: string, name: string): CredentialRecord | undefined;
39
+ /** List all credentials (metadata only) */
40
+ declare function listCredentials(database: Database.Database): CredentialMetadata[];
41
+ /** List credentials accessible by an API key's read access list */
42
+ declare function listCredentialsForApiKey(database: Database.Database, readAccess: string[]): CredentialMetadata[];
43
+ /** Delete a credential */
44
+ declare function deleteCredential(database: Database.Database, agent: string, name: string): boolean;
45
+ /** Update expiration time after refresh */
46
+ declare function updateExpiresAt(database: Database.Database, agent: string, name: string, expiresAt: Date | undefined): void;
47
+ export { deleteCredential, getCredential, listCredentials, listCredentialsForApiKey, updateExpiresAt, upsertCredential, };
48
+ export type { CredentialMetadata, CredentialRecord };
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Credentials repository.
3
+ *
4
+ * Manages encrypted credential storage.
5
+ */
6
+ /** Convert database row to record */
7
+ function rowToRecord(row) {
8
+ if (!row.salt) {
9
+ throw new Error("Credential missing salt - database may need migration");
10
+ }
11
+ return {
12
+ agent: row.agent,
13
+ name: row.name,
14
+ encryptedData: row.encrypted_data,
15
+ salt: row.salt,
16
+ iv: row.iv,
17
+ authTag: row.auth_tag,
18
+ createdAt: new Date(row.created_at),
19
+ updatedAt: new Date(row.updated_at),
20
+ expiresAt: row.expires_at ? new Date(row.expires_at) : undefined,
21
+ };
22
+ }
23
+ /** Convert metadata row to metadata */
24
+ function rowToMetadata(row) {
25
+ return {
26
+ agent: row.agent,
27
+ name: row.name,
28
+ createdAt: new Date(row.created_at),
29
+ updatedAt: new Date(row.updated_at),
30
+ expiresAt: row.expires_at ? new Date(row.expires_at) : undefined,
31
+ };
32
+ }
33
+ /** Store or update a credential */
34
+ function upsertCredential(database, credential) {
35
+ const now = Date.now();
36
+ /* eslint-disable unicorn/no-null -- SQLite requires null for NULL values */
37
+ database
38
+ .prepare(`INSERT INTO credentials (agent, name, encrypted_data, salt, iv, auth_tag, created_at, updated_at, expires_at)
39
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
40
+ ON CONFLICT(agent, name) DO UPDATE SET
41
+ encrypted_data = excluded.encrypted_data, salt = excluded.salt, iv = excluded.iv, auth_tag = excluded.auth_tag,
42
+ updated_at = excluded.updated_at, expires_at = excluded.expires_at`)
43
+ .run(credential.agent, credential.name, credential.encryptedData, credential.salt, credential.iv, credential.authTag, now, now, credential.expiresAt?.getTime() ?? null);
44
+ /* eslint-enable unicorn/no-null */
45
+ }
46
+ /** Get a credential by agent and name */
47
+ function getCredential(database, agent, name) {
48
+ const row = database
49
+ .prepare(`SELECT agent, name, encrypted_data, salt, iv, auth_tag, created_at, updated_at, expires_at FROM credentials WHERE agent = ? AND name = ?`)
50
+ .get(agent, name);
51
+ return row ? rowToRecord(row) : undefined;
52
+ }
53
+ /** List all credentials (metadata only) */
54
+ function listCredentials(database) {
55
+ const rows = database
56
+ .prepare(`SELECT agent, name, created_at, updated_at, expires_at FROM credentials ORDER BY agent, name`)
57
+ .all();
58
+ return rows.map((row) => rowToMetadata(row));
59
+ }
60
+ /** List credentials accessible by an API key's read access list */
61
+ function listCredentialsForApiKey(database, readAccess) {
62
+ if (readAccess.includes("*"))
63
+ return listCredentials(database);
64
+ return listCredentials(database).filter((cred) => readAccess.includes(`${cred.agent}/${cred.name}`));
65
+ }
66
+ /** Delete a credential */
67
+ function deleteCredential(database, agent, name) {
68
+ return (database
69
+ .prepare(`DELETE FROM credentials WHERE agent = ? AND name = ?`)
70
+ .run(agent, name).changes > 0);
71
+ }
72
+ /** Update expiration time after refresh */
73
+ function updateExpiresAt(database, agent, name, expiresAt) {
74
+ database
75
+ .prepare(`UPDATE credentials SET expires_at = ?, updated_at = ? WHERE agent = ? AND name = ?`)
76
+ // eslint-disable-next-line unicorn/no-null -- SQLite requires null for NULL values
77
+ .run(expiresAt?.getTime() ?? null, Date.now(), agent, name);
78
+ }
79
+ export { deleteCredential, getCredential, listCredentials, listCredentialsForApiKey, updateExpiresAt, upsertCredential, };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Database row types and conversion utilities.
3
+ */
4
+ /** Raw API key row from database */
5
+ export interface ApiKeyRow {
6
+ id: string;
7
+ name: string;
8
+ key_hash: string;
9
+ read_access: string;
10
+ write_access: string;
11
+ created_at: number;
12
+ last_used_at: number | null;
13
+ }
14
+ /** Raw audit log row from database */
15
+ export interface AuditLogRow {
16
+ id: number;
17
+ timestamp: number;
18
+ api_key_id: string | null;
19
+ action: string;
20
+ agent: string | null;
21
+ name: string | null;
22
+ success: number;
23
+ error_message: string | null;
24
+ }
25
+ /** Raw credential row from database */
26
+ export interface CredentialRow {
27
+ agent: string;
28
+ name: string;
29
+ encrypted_data: Buffer;
30
+ salt: Buffer | null;
31
+ iv: Buffer;
32
+ auth_tag: Buffer;
33
+ created_at: number;
34
+ updated_at: number;
35
+ expires_at: number | null;
36
+ }
37
+ /** Raw credential metadata row from database */
38
+ export interface MetadataRow {
39
+ agent: string;
40
+ name: string;
41
+ created_at: number;
42
+ updated_at: number;
43
+ expires_at: number | null;
44
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Database row types and conversion utilities.
3
+ */
4
+ export {};
@@ -0,0 +1,15 @@
1
+ /**
2
+ * DELETE /api/v1/credentials/:agent/:name handler.
3
+ */
4
+ import type { RequestHandler } from "express";
5
+ import type Database from "better-sqlite3";
6
+ /** Handler type for credential routes with agent/name params */
7
+ type CredentialHandler = RequestHandler<{
8
+ agent: string;
9
+ name: string;
10
+ }, unknown, unknown, unknown>;
11
+ /**
12
+ * Remove a credential.
13
+ */
14
+ declare function createDeleteCredentialHandler(database: Database.Database): CredentialHandler;
15
+ export { createDeleteCredentialHandler };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * DELETE /api/v1/credentials/:agent/:name handler.
3
+ */
4
+ import { hasWriteAccess } from "../db/repositories/api-keys.js";
5
+ import { deleteCredential } from "../db/repositories/credentials.js";
6
+ import { logAccess } from "../db/repositories/audit-log.js";
7
+ /**
8
+ * Remove a credential.
9
+ */
10
+ function createDeleteCredentialHandler(database) {
11
+ return (request, response) => {
12
+ const authenticatedRequest = request;
13
+ const { agent, name } = request.params;
14
+ const { apiKey } = authenticatedRequest;
15
+ if (!hasWriteAccess(apiKey, agent, name)) {
16
+ logAccess(database, {
17
+ apiKeyId: apiKey.id,
18
+ action: "delete",
19
+ agent,
20
+ name,
21
+ success: false,
22
+ errorMessage: "Access denied",
23
+ });
24
+ response.status(403).json({ error: "Access denied" });
25
+ return;
26
+ }
27
+ const deleted = deleteCredential(database, agent, name);
28
+ if (!deleted) {
29
+ logAccess(database, {
30
+ apiKeyId: apiKey.id,
31
+ action: "delete",
32
+ agent,
33
+ name,
34
+ success: false,
35
+ errorMessage: "Not found",
36
+ });
37
+ response.status(404).json({ error: "Credential not found" });
38
+ return;
39
+ }
40
+ logAccess(database, {
41
+ apiKeyId: apiKey.id,
42
+ action: "delete",
43
+ agent,
44
+ name,
45
+ success: true,
46
+ });
47
+ response.json({ message: "Credential deleted", agent, name });
48
+ };
49
+ }
50
+ export { createDeleteCredentialHandler };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * GET /api/v1/credentials/:agent/:name handler.
3
+ */
4
+ import type { Request, Response } from "express";
5
+ import type Database from "better-sqlite3";
6
+ /** Handler configuration */
7
+ interface GetCredentialConfig {
8
+ refreshThresholdSeconds: number;
9
+ refreshTimeoutMs: number;
10
+ }
11
+ /**
12
+ * Retrieve a credential by agent and name.
13
+ *
14
+ * Automatically refreshes credentials that are near expiration.
15
+ */
16
+ declare function createGetCredentialHandler(database: Database.Database, config: GetCredentialConfig): (request: Request<{
17
+ agent: string;
18
+ name: string;
19
+ }>, response: Response) => Promise<void>;
20
+ export { createGetCredentialHandler };
21
+ export type { GetCredentialConfig };
@@ -0,0 +1,143 @@
1
+ /**
2
+ * GET /api/v1/credentials/:agent/:name handler.
3
+ */
4
+ import { hasReadAccess } from "../db/repositories/api-keys.js";
5
+ import { getCredential } from "../db/repositories/credentials.js";
6
+ import { logAccess } from "../db/repositories/audit-log.js";
7
+ import { decryptCredential } from "../lib/encryption.js";
8
+ import { isRefreshable, needsRefresh, refreshWithMutex, } from "../refresh/refresh-manager.js";
9
+ /**
10
+ * Retrieve a credential by agent and name.
11
+ *
12
+ * Automatically refreshes credentials that are near expiration.
13
+ */
14
+ function createGetCredentialHandler(database, config) {
15
+ return async (request, response) => {
16
+ const authenticatedRequest = request;
17
+ const { agent, name } = request.params;
18
+ const { apiKey } = authenticatedRequest;
19
+ // Access control check
20
+ if (!hasReadAccess(apiKey, agent, name)) {
21
+ logAccess(database, {
22
+ apiKeyId: apiKey.id,
23
+ action: "read",
24
+ agent,
25
+ name,
26
+ success: false,
27
+ errorMessage: "Access denied",
28
+ });
29
+ response.status(403).json({ error: "Access denied" });
30
+ return;
31
+ }
32
+ // Fetch credential from database
33
+ let credential;
34
+ try {
35
+ credential = getCredential(database, agent, name);
36
+ }
37
+ catch (error) {
38
+ // Handle legacy credentials stored without salt (pre-v2 migration)
39
+ if (error instanceof Error && error.message.includes("missing salt")) {
40
+ logAccess(database, {
41
+ apiKeyId: apiKey.id,
42
+ action: "read",
43
+ agent,
44
+ name,
45
+ success: false,
46
+ errorMessage: "Legacy credential requires re-upload",
47
+ });
48
+ response.status(410).json({
49
+ error: "This credential was stored with legacy encryption and must be deleted and re-uploaded",
50
+ });
51
+ return;
52
+ }
53
+ throw error;
54
+ }
55
+ if (!credential) {
56
+ logAccess(database, {
57
+ apiKeyId: apiKey.id,
58
+ action: "read",
59
+ agent,
60
+ name,
61
+ success: false,
62
+ errorMessage: "Not found",
63
+ });
64
+ response.status(404).json({ error: "Credential not found" });
65
+ return;
66
+ }
67
+ // Decrypt credential data
68
+ let data;
69
+ try {
70
+ data = decryptCredential(credential);
71
+ }
72
+ catch (error) {
73
+ const message = error instanceof Error ? error.message : String(error);
74
+ logAccess(database, {
75
+ apiKeyId: apiKey.id,
76
+ action: "read",
77
+ agent,
78
+ name,
79
+ success: false,
80
+ errorMessage: `Decryption failed: ${message}`,
81
+ });
82
+ response.status(500).json({ error: "Failed to decrypt credential" });
83
+ return;
84
+ }
85
+ // Check if refresh is needed (isRefreshable is a type guard)
86
+ // When refreshThresholdSeconds is 0, auto-refresh is disabled
87
+ let finalData = data;
88
+ let finalExpiresAt = credential.expiresAt;
89
+ let finalUpdatedAt = credential.updatedAt;
90
+ let wasRefreshed = false;
91
+ let refreshFailed = false;
92
+ if (config.refreshThresholdSeconds > 0 &&
93
+ isRefreshable(data) &&
94
+ needsRefresh(data, config.refreshThresholdSeconds)) {
95
+ try {
96
+ const refreshResult = await refreshWithMutex(database, agent, name, data, apiKey.id, credential.updatedAt, { timeoutMs: config.refreshTimeoutMs });
97
+ if (refreshResult.ok) {
98
+ finalData = refreshResult.data;
99
+ finalExpiresAt = refreshResult.expiresAt;
100
+ finalUpdatedAt = refreshResult.updatedAt;
101
+ wasRefreshed = true;
102
+ }
103
+ else {
104
+ // Refresh failed - will return stale credentials with warning header
105
+ // Error details logged by refreshWithMutex to audit log
106
+ refreshFailed = true;
107
+ }
108
+ }
109
+ catch (error) {
110
+ // Programming error safety net - refreshWithMutex handles all expected
111
+ // errors internally and returns { ok: false }. This catch is for bugs
112
+ // in our code, not provider errors. Only log message, not stack/object.
113
+ console.error(`Unexpected refresh error for ${agent}/${name}:`, error instanceof Error ? error.message : error);
114
+ refreshFailed = true;
115
+ }
116
+ }
117
+ // Log successful read
118
+ logAccess(database, {
119
+ apiKeyId: apiKey.id,
120
+ action: "read",
121
+ agent,
122
+ name,
123
+ success: true,
124
+ });
125
+ // Set refresh header if token was refreshed
126
+ if (wasRefreshed) {
127
+ response.setHeader("X-Axvault-Refreshed", "true");
128
+ }
129
+ // Set warning header if refresh failed (still return 200 with stale data)
130
+ // Error details are logged to audit log; header is boolean to avoid invalid chars
131
+ if (refreshFailed) {
132
+ response.setHeader("X-Axvault-Refresh-Failed", "true");
133
+ }
134
+ response.json({
135
+ agent,
136
+ name,
137
+ data: finalData,
138
+ expiresAt: finalExpiresAt?.toISOString(),
139
+ updatedAt: finalUpdatedAt.toISOString(),
140
+ });
141
+ };
142
+ }
143
+ export { createGetCredentialHandler };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * GET /api/v1/credentials handler.
3
+ */
4
+ import type { RequestHandler } from "express";
5
+ import type Database from "better-sqlite3";
6
+ /** Handler type for list route (no params) */
7
+ type ListHandler = RequestHandler<Record<string, never>, unknown, unknown, unknown>;
8
+ /**
9
+ * List all credentials accessible by the API key (metadata only).
10
+ */
11
+ declare function createListCredentialsHandler(database: Database.Database): ListHandler;
12
+ export { createListCredentialsHandler };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * GET /api/v1/credentials handler.
3
+ */
4
+ import { listCredentialsForApiKey } from "../db/repositories/credentials.js";
5
+ import { logAccess } from "../db/repositories/audit-log.js";
6
+ /**
7
+ * List all credentials accessible by the API key (metadata only).
8
+ */
9
+ function createListCredentialsHandler(database) {
10
+ return (request, response) => {
11
+ const authenticatedRequest = request;
12
+ const { apiKey } = authenticatedRequest;
13
+ const credentials = listCredentialsForApiKey(database, apiKey.readAccess);
14
+ logAccess(database, {
15
+ apiKeyId: apiKey.id,
16
+ action: "list",
17
+ success: true,
18
+ });
19
+ response.json({
20
+ credentials: credentials.map((cred) => ({
21
+ agent: cred.agent,
22
+ name: cred.name,
23
+ expiresAt: cred.expiresAt?.toISOString(),
24
+ updatedAt: cred.updatedAt.toISOString(),
25
+ })),
26
+ });
27
+ };
28
+ }
29
+ export { createListCredentialsHandler };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * PUT /api/v1/credentials/:agent/:name handler.
3
+ */
4
+ import type { RequestHandler } from "express";
5
+ import type Database from "better-sqlite3";
6
+ /** Handler type for credential routes with agent/name params */
7
+ type CredentialHandler = RequestHandler<{
8
+ agent: string;
9
+ name: string;
10
+ }, unknown, unknown, unknown>;
11
+ /**
12
+ * Store or update a credential.
13
+ */
14
+ declare function createPutCredentialHandler(database: Database.Database): CredentialHandler;
15
+ export { createPutCredentialHandler };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * PUT /api/v1/credentials/:agent/:name handler.
3
+ */
4
+ import { hasWriteAccess } from "../db/repositories/api-keys.js";
5
+ import { upsertCredential } from "../db/repositories/credentials.js";
6
+ import { logAccess } from "../db/repositories/audit-log.js";
7
+ import { encryptCredential } from "../lib/encryption.js";
8
+ /**
9
+ * Store or update a credential.
10
+ */
11
+ function createPutCredentialHandler(database) {
12
+ return (request, response) => {
13
+ const authenticatedRequest = request;
14
+ const { agent, name } = request.params;
15
+ const { apiKey } = authenticatedRequest;
16
+ if (!hasWriteAccess(apiKey, agent, name)) {
17
+ logAccess(database, {
18
+ apiKeyId: apiKey.id,
19
+ action: "write",
20
+ agent,
21
+ name,
22
+ success: false,
23
+ errorMessage: "Access denied",
24
+ });
25
+ response.status(403).json({ error: "Access denied" });
26
+ return;
27
+ }
28
+ const body = request.body;
29
+ if (!body || typeof body.data !== "object" || body.data === null) {
30
+ logAccess(database, {
31
+ apiKeyId: apiKey.id,
32
+ action: "write",
33
+ agent,
34
+ name,
35
+ success: false,
36
+ errorMessage: "Invalid request body",
37
+ });
38
+ response
39
+ .status(400)
40
+ .json({ error: "Request body must include 'data' object" });
41
+ return;
42
+ }
43
+ try {
44
+ const encrypted = encryptCredential(body.data);
45
+ let expiresAt;
46
+ if (body.expiresAt) {
47
+ expiresAt = new Date(body.expiresAt);
48
+ if (Number.isNaN(expiresAt.getTime())) {
49
+ logAccess(database, {
50
+ apiKeyId: apiKey.id,
51
+ action: "write",
52
+ agent,
53
+ name,
54
+ success: false,
55
+ errorMessage: "Invalid expiresAt date",
56
+ });
57
+ response.status(400).json({ error: "Invalid expiresAt date" });
58
+ return;
59
+ }
60
+ }
61
+ upsertCredential(database, {
62
+ agent,
63
+ name,
64
+ ...encrypted,
65
+ expiresAt,
66
+ });
67
+ logAccess(database, {
68
+ apiKeyId: apiKey.id,
69
+ action: "write",
70
+ agent,
71
+ name,
72
+ success: true,
73
+ });
74
+ response.status(201).json({
75
+ message: "Credential stored",
76
+ agent,
77
+ name,
78
+ });
79
+ }
80
+ catch (error) {
81
+ const message = error instanceof Error ? error.message : String(error);
82
+ logAccess(database, {
83
+ apiKeyId: apiKey.id,
84
+ action: "write",
85
+ agent,
86
+ name,
87
+ success: false,
88
+ errorMessage: `Encryption failed: ${message}`,
89
+ });
90
+ response.status(500).json({ error: "Failed to encrypt credential" });
91
+ }
92
+ };
93
+ }
94
+ export { createPutCredentialHandler };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * axvault - Remote credential storage server for axpoint.
3
+ *
4
+ * This module exports types and functions for programmatic use.
5
+ */
6
+ export type { ServerConfig } from "./config.js";
7
+ export { getServerConfig } from "./config.js";
8
+ export type { AxvaultServer } from "./server/server.js";
9
+ export { createServer } from "./server/server.js";
10
+ export { createApiRouter } from "./server/routes.js";
11
+ export { closeDatabase, getDatabase, isDatabaseConnected, } from "./db/client.js";
12
+ export { CURRENT_VERSION, getSchemaVersion, runMigrations, } from "./db/migrations.js";
13
+ export type { ApiKeyRecord, ApiKeyWithSecret, } from "./db/repositories/api-keys.js";
14
+ export { createApiKey, deleteApiKey, findApiKeyById, findApiKeyByKey, hasReadAccess, hasWriteAccess, listApiKeys, updateLastUsed, } from "./db/repositories/api-keys.js";
15
+ export type { AuditLogEntry } from "./db/repositories/audit-log.js";
16
+ export { getLogsForCredential, getRecentLogs, logAccess, pruneOldLogs, } from "./db/repositories/audit-log.js";
17
+ export type { CredentialMetadata, CredentialRecord, } from "./db/repositories/credentials.js";
18
+ export { deleteCredential, getCredential, listCredentials, listCredentialsForApiKey, updateExpiresAt, upsertCredential, } from "./db/repositories/credentials.js";
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * axvault - Remote credential storage server for axpoint.
3
+ *
4
+ * This module exports types and functions for programmatic use.
5
+ */
6
+ export { getServerConfig } from "./config.js";
7
+ export { createServer } from "./server/server.js";
8
+ export { createApiRouter } from "./server/routes.js";
9
+ // Database client
10
+ export { closeDatabase, getDatabase, isDatabaseConnected, } from "./db/client.js";
11
+ export { CURRENT_VERSION, getSchemaVersion, runMigrations, } from "./db/migrations.js";
12
+ export { createApiKey, deleteApiKey, findApiKeyById, findApiKeyByKey, hasReadAccess, hasWriteAccess, listApiKeys, updateLastUsed, } from "./db/repositories/api-keys.js";
13
+ export { getLogsForCredential, getRecentLogs, logAccess, pruneOldLogs, } from "./db/repositories/audit-log.js";
14
+ export { deleteCredential, getCredential, listCredentials, listCredentialsForApiKey, updateExpiresAt, upsertCredential, } from "./db/repositories/credentials.js";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Encryption utilities for credential storage.
3
+ *
4
+ * Wraps axshared encryption functions with vault-specific configuration.
5
+ */
6
+ /** Encrypted credential data stored in database */
7
+ interface EncryptedCredential {
8
+ encryptedData: Buffer;
9
+ salt: Buffer;
10
+ iv: Buffer;
11
+ authTag: Buffer;
12
+ }
13
+ /** Encrypt credential data for storage */
14
+ declare function encryptCredential(data: unknown): EncryptedCredential;
15
+ /** Decrypt credential data from storage */
16
+ declare function decryptCredential(credential: EncryptedCredential): unknown;
17
+ export { decryptCredential, encryptCredential };