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,41 +1,26 @@
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
- /** Parse and validate credential type from database */
6
- function parseCredentialType(type) {
7
- const result = CredentialType.safeParse(type);
8
- if (!result.success) {
9
- throw new Error(`Invalid credential type: ${type}`);
10
- }
11
- return result.data;
12
- }
13
6
  /** Convert database row to record */
14
7
  function rowToRecord(row) {
15
8
  return {
16
- agent: row.agent,
17
9
  name: row.name,
18
- type: parseCredentialType(row.type),
19
- provider: row.provider ?? undefined,
20
10
  encryptedData: row.encrypted_data,
21
11
  salt: row.salt,
22
12
  iv: row.iv,
23
13
  authTag: row.auth_tag,
24
14
  createdAt: new Date(row.created_at),
25
15
  updatedAt: new Date(row.updated_at),
26
- expiresAt: row.expires_at ? new Date(row.expires_at) : undefined,
27
16
  };
28
17
  }
29
18
  /** Convert metadata row to metadata */
30
19
  function rowToMetadata(row) {
31
20
  return {
32
- agent: row.agent,
33
21
  name: row.name,
34
- type: parseCredentialType(row.type),
35
- provider: row.provider ?? undefined,
36
22
  createdAt: new Date(row.created_at),
37
23
  updatedAt: new Date(row.updated_at),
38
- expiresAt: row.expires_at ? new Date(row.expires_at) : undefined,
39
24
  };
40
25
  }
41
26
  export { rowToMetadata, rowToRecord };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Database row types and conversion utilities.
2
+ * Database row types.
3
3
  */
4
4
  /** Raw API key row from database */
5
5
  export interface ApiKeyRow {
@@ -19,32 +19,23 @@ export interface AuditLogRow {
19
19
  timestamp: number;
20
20
  api_key_id: string | null;
21
21
  action: string;
22
- agent: string | null;
23
- name: string | null;
22
+ credential_name: string | null;
24
23
  success: number;
25
24
  error_message: string | null;
26
25
  }
27
- /** Raw credential row from database */
26
+ /** Raw credential row from database (opaque blob storage) */
28
27
  export interface CredentialRow {
29
- agent: string;
30
28
  name: string;
31
- type: string;
32
- provider: string | null;
33
29
  encrypted_data: Buffer;
34
30
  salt: Buffer;
35
31
  iv: Buffer;
36
32
  auth_tag: Buffer;
37
33
  created_at: number;
38
34
  updated_at: number;
39
- expires_at: number | null;
40
35
  }
41
36
  /** Raw credential metadata row from database */
42
37
  export interface MetadataRow {
43
- agent: string;
44
38
  name: string;
45
- type: string;
46
- provider: string | null;
47
39
  created_at: number;
48
40
  updated_at: number;
49
- expires_at: number | null;
50
41
  }
package/dist/db/types.js CHANGED
@@ -1,4 +1,4 @@
1
1
  /**
2
- * Database row types and conversion utilities.
2
+ * Database row types.
3
3
  */
4
4
  export {};
@@ -1,11 +1,10 @@
1
1
  /**
2
- * DELETE /api/v1/credentials/:agent/:name handler.
2
+ * DELETE /api/v1/credentials/:name handler.
3
3
  */
4
4
  import type { RequestHandler } from "express";
5
5
  import type Database from "better-sqlite3";
6
- /** Handler type for credential routes with agent/name params */
6
+ /** Handler type for credential routes with name param */
7
7
  type CredentialHandler = RequestHandler<{
8
- agent: string;
9
8
  name: string;
10
9
  }, unknown, unknown, unknown>;
11
10
  /**
@@ -1,5 +1,5 @@
1
1
  /**
2
- * DELETE /api/v1/credentials/:agent/:name handler.
2
+ * DELETE /api/v1/credentials/:name handler.
3
3
  */
4
4
  import { hasWriteAccess } from "../db/repositories/api-keys.js";
5
5
  import { deleteCredential } from "../db/repositories/credentials.js";
@@ -10,27 +10,25 @@ import { logAccess } from "../db/repositories/audit-log.js";
10
10
  function createDeleteCredentialHandler(database) {
11
11
  return (request, response) => {
12
12
  const authenticatedRequest = request;
13
- const { agent, name } = request.params;
13
+ const { name } = request.params;
14
14
  const { apiKey } = authenticatedRequest;
15
- if (!hasWriteAccess(apiKey, agent, name)) {
15
+ if (!hasWriteAccess(apiKey, name)) {
16
16
  logAccess(database, {
17
17
  apiKeyId: apiKey.id,
18
18
  action: "delete",
19
- agent,
20
- name,
19
+ credentialName: name,
21
20
  success: false,
22
21
  errorMessage: "Access denied",
23
22
  });
24
23
  response.status(403).json({ error: "Access denied" });
25
24
  return;
26
25
  }
27
- const deleted = deleteCredential(database, agent, name);
26
+ const deleted = deleteCredential(database, name);
28
27
  if (!deleted) {
29
28
  logAccess(database, {
30
29
  apiKeyId: apiKey.id,
31
30
  action: "delete",
32
- agent,
33
- name,
31
+ credentialName: name,
34
32
  success: false,
35
33
  errorMessage: "Not found",
36
34
  });
@@ -40,11 +38,10 @@ function createDeleteCredentialHandler(database) {
40
38
  logAccess(database, {
41
39
  apiKeyId: apiKey.id,
42
40
  action: "delete",
43
- agent,
44
- name,
41
+ credentialName: name,
45
42
  success: true,
46
43
  });
47
- response.json({ message: "Credential deleted", agent, name });
44
+ response.status(204).end();
48
45
  };
49
46
  }
50
47
  export { createDeleteCredentialHandler };
@@ -1,5 +1,9 @@
1
1
  /**
2
- * GET /api/v1/credentials/:agent/:name handler.
2
+ * GET /api/v1/credentials/:name handler.
3
+ *
4
+ * Fully opaque: The stored blob is decrypted and returned as-is. axvault does
5
+ * not inspect the blob's structure - all introspection (refresh checks, expiry
6
+ * detection) is delegated to axauth functions that accept opaque blobs.
3
7
  */
4
8
  import type { Request, Response } from "express";
5
9
  import type Database from "better-sqlite3";
@@ -9,12 +13,11 @@ interface GetCredentialConfig {
9
13
  refreshTimeoutMs: number;
10
14
  }
11
15
  /**
12
- * Retrieve a credential by agent and name.
16
+ * Retrieve a credential by name.
13
17
  *
14
18
  * Automatically refreshes credentials that are near expiration.
15
19
  */
16
20
  declare function createGetCredentialHandler(database: Database.Database, config: GetCredentialConfig): (request: Request<{
17
- agent: string;
18
21
  name: string;
19
22
  }>, response: Response) => Promise<void>;
20
23
  export { createGetCredentialHandler };
@@ -1,29 +1,31 @@
1
1
  /**
2
- * GET /api/v1/credentials/:agent/:name handler.
2
+ * GET /api/v1/credentials/:name handler.
3
+ *
4
+ * Fully opaque: The stored blob is decrypted and returned as-is. axvault does
5
+ * not inspect the blob's structure - all introspection (refresh checks, expiry
6
+ * detection) is delegated to axauth functions that accept opaque blobs.
3
7
  */
4
- import { isRefreshableCredentialType } from "axshared";
5
8
  import { hasReadAccess } from "../db/repositories/api-keys.js";
6
9
  import { getCredential } from "../db/repositories/credentials.js";
7
10
  import { logAccess } from "../db/repositories/audit-log.js";
8
11
  import { decryptCredential } from "../lib/encryption.js";
9
- import { isRefreshable, needsRefresh, refreshWithMutex, } from "../refresh/refresh-manager.js";
12
+ import { refreshCredentialOnRead } from "./refresh-credential-on-read.js";
10
13
  /**
11
- * Retrieve a credential by agent and name.
14
+ * Retrieve a credential by name.
12
15
  *
13
16
  * Automatically refreshes credentials that are near expiration.
14
17
  */
15
18
  function createGetCredentialHandler(database, config) {
16
19
  return async (request, response) => {
17
20
  const authenticatedRequest = request;
18
- const { agent, name } = request.params;
21
+ const { name } = request.params;
19
22
  const { apiKey } = authenticatedRequest;
20
23
  // Access control check
21
- if (!hasReadAccess(apiKey, agent, name)) {
24
+ if (!hasReadAccess(apiKey, name)) {
22
25
  logAccess(database, {
23
26
  apiKeyId: apiKey.id,
24
27
  action: "read",
25
- agent,
26
- name,
28
+ credentialName: name,
27
29
  success: false,
28
30
  errorMessage: "Access denied",
29
31
  });
@@ -31,98 +33,57 @@ function createGetCredentialHandler(database, config) {
31
33
  return;
32
34
  }
33
35
  // Fetch credential from database
34
- const credential = getCredential(database, agent, name);
36
+ const credential = getCredential(database, name);
35
37
  if (!credential) {
36
38
  logAccess(database, {
37
39
  apiKeyId: apiKey.id,
38
40
  action: "read",
39
- agent,
40
- name,
41
+ credentialName: name,
41
42
  success: false,
42
43
  errorMessage: "Not found",
43
44
  });
44
45
  response.status(404).json({ error: "Credential not found" });
45
46
  return;
46
47
  }
47
- // Decrypt credential data
48
- let data;
48
+ // Decrypt credential blob (opaque - we don't inspect its structure)
49
+ let blob;
49
50
  try {
50
- data = decryptCredential(credential);
51
+ blob = decryptCredential(credential);
51
52
  }
52
53
  catch (error) {
53
54
  const message = error instanceof Error ? error.message : String(error);
54
55
  logAccess(database, {
55
56
  apiKeyId: apiKey.id,
56
57
  action: "read",
57
- agent,
58
- name,
58
+ credentialName: name,
59
59
  success: false,
60
60
  errorMessage: `Decryption failed: ${message}`,
61
61
  });
62
62
  response.status(500).json({ error: "Failed to decrypt credential" });
63
63
  return;
64
64
  }
65
- // Check if refresh is needed (isRefreshable is a type guard)
66
- // When refreshThresholdSeconds is 0, auto-refresh is disabled
67
- let finalData = data;
68
- let finalExpiresAt = credential.expiresAt;
69
- let finalUpdatedAt = credential.updatedAt;
70
- let wasRefreshed = false;
71
- let refreshFailed = false;
72
- if (config.refreshThresholdSeconds > 0 &&
73
- isRefreshableCredentialType(credential.type) &&
74
- isRefreshable(data) &&
75
- needsRefresh(data, config.refreshThresholdSeconds)) {
76
- try {
77
- const refreshResult = await refreshWithMutex(database, agent, name, credential.type, credential.provider, data, apiKey.id, credential.updatedAt, { timeoutMs: config.refreshTimeoutMs });
78
- if (refreshResult.ok) {
79
- finalData = refreshResult.data;
80
- finalExpiresAt = refreshResult.expiresAt;
81
- finalUpdatedAt = refreshResult.updatedAt;
82
- wasRefreshed = true;
83
- }
84
- else {
85
- // Refresh failed - re-fetch to check if credential was deleted/modified
86
- const currentCredential = getCredential(database, agent, name);
87
- if (!currentCredential) {
88
- // Credential was deleted during refresh - don't leak old data
89
- logAccess(database, {
90
- apiKeyId: apiKey.id,
91
- action: "read",
92
- agent,
93
- name,
94
- success: false,
95
- errorMessage: "Credential deleted during refresh",
96
- });
97
- response.status(404).json({ error: "Credential not found" });
98
- return;
99
- }
100
- if (currentCredential.updatedAt.getTime() !==
101
- credential.updatedAt.getTime()) {
102
- // Credential was modified during refresh - return fresh data
103
- const freshData = decryptCredential(currentCredential);
104
- finalData = freshData;
105
- finalExpiresAt = currentCredential.expiresAt;
106
- finalUpdatedAt = currentCredential.updatedAt;
107
- }
108
- // Otherwise credential unchanged, return original with warning
109
- refreshFailed = true;
110
- }
111
- }
112
- catch (error) {
113
- // Programming error safety net - refreshWithMutex handles all expected
114
- // errors internally and returns { ok: false }. This catch is for bugs
115
- // in our code, not provider errors. Only log message, not stack/object.
116
- console.error(`Unexpected refresh error for ${agent}/${name}:`, error instanceof Error ? error.message : error);
117
- refreshFailed = true;
118
- }
65
+ const refreshResult = await refreshCredentialOnRead({
66
+ database,
67
+ apiKeyId: apiKey.id,
68
+ name,
69
+ blob,
70
+ expectedUpdatedAt: credential.updatedAt,
71
+ refreshThresholdSeconds: config.refreshThresholdSeconds,
72
+ refreshTimeoutMs: config.refreshTimeoutMs,
73
+ });
74
+ if (refreshResult.status === "not-found") {
75
+ response.status(404).json({ error: "Credential not found" });
76
+ return;
119
77
  }
78
+ const finalBlob = refreshResult.blob;
79
+ const finalUpdatedAt = refreshResult.updatedAt;
80
+ const wasRefreshed = refreshResult.wasRefreshed;
81
+ const refreshFailed = refreshResult.refreshFailed;
120
82
  // Log successful read
121
83
  logAccess(database, {
122
84
  apiKeyId: apiKey.id,
123
85
  action: "read",
124
- agent,
125
- name,
86
+ credentialName: name,
126
87
  success: true,
127
88
  });
128
89
  // Set refresh header if token was refreshed
@@ -130,17 +91,13 @@ function createGetCredentialHandler(database, config) {
130
91
  response.setHeader("X-Axvault-Refreshed", "true");
131
92
  }
132
93
  // Set warning header if refresh failed (still return 200 with stale data)
133
- // Error details are logged to audit log; header is boolean to avoid invalid chars
134
94
  if (refreshFailed) {
135
95
  response.setHeader("X-Axvault-Refresh-Failed", "true");
136
96
  }
97
+ // Return the blob as-is (opaque to axvault)
137
98
  response.json({
138
- agent,
139
99
  name,
140
- type: credential.type,
141
- ...(credential.provider && { provider: credential.provider }),
142
- data: finalData,
143
- expiresAt: finalExpiresAt?.toISOString(),
100
+ credential: finalBlob,
144
101
  updatedAt: finalUpdatedAt.toISOString(),
145
102
  });
146
103
  };
@@ -1,12 +1,28 @@
1
1
  /**
2
2
  * GET /api/v1/credentials handler.
3
+ *
4
+ * Lists all credentials accessible by the API key (metadata only).
5
+ * With opaque schema, only name and timestamps are available without decrypting.
6
+ *
7
+ * Supports opt-in cursor-based pagination via `limit` and `cursor` query
8
+ * parameters. Without `limit`, all accessible credentials are returned
9
+ * (backward-compatible with pre-pagination behavior).
3
10
  */
4
11
  import type { RequestHandler } from "express";
5
12
  import type Database from "better-sqlite3";
6
- /** Handler type for list route (no params) */
7
- type ListHandler = RequestHandler<Record<string, never>, unknown, unknown, unknown>;
13
+ /** Handler type for list route with pagination query params */
14
+ type ListHandler = RequestHandler<Record<string, never>, unknown, unknown, {
15
+ limit?: string;
16
+ cursor?: string;
17
+ }>;
8
18
  /**
9
- * List all credentials accessible by the API key (metadata only).
19
+ * List credentials accessible by the API key (metadata only).
20
+ *
21
+ * Query parameters:
22
+ * - `limit` — Results per page (1–1000). Omit for all results.
23
+ * - `cursor` — Name of the last credential from the previous page (requires `limit`)
24
+ *
25
+ * Returns `nextCursor` when more results are available.
10
26
  */
11
27
  declare function createListCredentialsHandler(database: Database.Database): ListHandler;
12
28
  export { createListCredentialsHandler };
@@ -1,30 +1,105 @@
1
1
  /**
2
2
  * GET /api/v1/credentials handler.
3
+ *
4
+ * Lists all credentials accessible by the API key (metadata only).
5
+ * With opaque schema, only name and timestamps are available without decrypting.
6
+ *
7
+ * Supports opt-in cursor-based pagination via `limit` and `cursor` query
8
+ * parameters. Without `limit`, all accessible credentials are returned
9
+ * (backward-compatible with pre-pagination behavior).
3
10
  */
4
- import { listCredentialsForApiKey } from "../db/repositories/credentials.js";
11
+ import { listCredentialsForApiKey, listCredentialsPaginated, } from "../db/repositories/credentials.js";
5
12
  import { logAccess } from "../db/repositories/audit-log.js";
13
+ /** Maximum page size when limit is specified */
14
+ const MAX_LIMIT = 1000;
6
15
  /**
7
- * List all credentials accessible by the API key (metadata only).
16
+ * List credentials accessible by the API key (metadata only).
17
+ *
18
+ * Query parameters:
19
+ * - `limit` — Results per page (1–1000). Omit for all results.
20
+ * - `cursor` — Name of the last credential from the previous page (requires `limit`)
21
+ *
22
+ * Returns `nextCursor` when more results are available.
8
23
  */
9
24
  function createListCredentialsHandler(database) {
10
25
  return (request, response) => {
11
26
  const authenticatedRequest = request;
12
27
  const { apiKey } = authenticatedRequest;
13
- const credentials = listCredentialsForApiKey(database, apiKey.readAccess);
28
+ const rawLimit = request.query.limit;
29
+ const rawCursor = request.query.cursor;
30
+ // Validate query param types (repeated params produce arrays)
31
+ if ((rawLimit !== undefined && typeof rawLimit !== "string") ||
32
+ (rawCursor !== undefined && typeof rawCursor !== "string")) {
33
+ logAccess(database, {
34
+ apiKeyId: apiKey.id,
35
+ action: "list",
36
+ success: false,
37
+ errorMessage: "Repeated query parameters",
38
+ });
39
+ response
40
+ .status(400)
41
+ .json({ error: "Repeated query parameters are not supported" });
42
+ return;
43
+ }
44
+ // cursor without limit is invalid — pagination requires an explicit page size
45
+ if (rawCursor && rawLimit === undefined) {
46
+ logAccess(database, {
47
+ apiKeyId: apiKey.id,
48
+ action: "list",
49
+ success: false,
50
+ errorMessage: "cursor requires limit",
51
+ });
52
+ response.status(400).json({ error: "cursor requires limit" });
53
+ return;
54
+ }
55
+ // No limit specified: return all accessible credentials (backward-compatible)
56
+ if (rawLimit === undefined) {
57
+ const credentials = listCredentialsForApiKey(database, apiKey.readAccess);
58
+ logAccess(database, {
59
+ apiKeyId: apiKey.id,
60
+ action: "list",
61
+ success: true,
62
+ });
63
+ response.json({
64
+ credentials: credentials.map((cred) => ({
65
+ name: cred.name,
66
+ createdAt: cred.createdAt.toISOString(),
67
+ updatedAt: cred.updatedAt.toISOString(),
68
+ })),
69
+ });
70
+ return;
71
+ }
72
+ // Paginated path (rawLimit is guaranteed to be defined here)
73
+ const parsedLimit = Number(rawLimit);
74
+ if (!Number.isInteger(parsedLimit) || parsedLimit < 1) {
75
+ logAccess(database, {
76
+ apiKeyId: apiKey.id,
77
+ action: "list",
78
+ success: false,
79
+ errorMessage: "Invalid limit parameter",
80
+ });
81
+ response.status(400).json({ error: "limit must be a positive integer" });
82
+ return;
83
+ }
84
+ const limit = Math.min(parsedLimit, MAX_LIMIT);
85
+ const result = listCredentialsPaginated(database, apiKey.readAccess, {
86
+ cursor: rawCursor || undefined,
87
+ limit,
88
+ });
14
89
  logAccess(database, {
15
90
  apiKeyId: apiKey.id,
16
91
  action: "list",
17
92
  success: true,
18
93
  });
19
94
  response.json({
20
- credentials: credentials.map((cred) => ({
21
- agent: cred.agent,
95
+ credentials: result.credentials.map((cred) => ({
22
96
  name: cred.name,
23
- type: cred.type,
24
- ...(cred.provider && { provider: cred.provider }),
25
- expiresAt: cred.expiresAt?.toISOString(),
97
+ createdAt: cred.createdAt.toISOString(),
26
98
  updatedAt: cred.updatedAt.toISOString(),
27
99
  })),
100
+ ...(result.nextCursor !== undefined && {
101
+ nextCursor: result.nextCursor,
102
+ }),
28
103
  });
29
104
  };
30
105
  }
@@ -1,15 +1,22 @@
1
1
  /**
2
- * PUT /api/v1/credentials/:agent/:name handler.
2
+ * PUT /api/v1/credentials/:name handler.
3
+ *
4
+ * Fully opaque: The request body is treated as an opaque blob. axvault does not
5
+ * validate or inspect its structure - that responsibility belongs to axauth.
6
+ * The blob is encrypted and stored as-is.
3
7
  */
4
8
  import type { RequestHandler } from "express";
5
9
  import type Database from "better-sqlite3";
6
- /** Handler type for credential routes with agent/name params */
10
+ /** Handler type for credential routes with name param */
7
11
  type CredentialHandler = RequestHandler<{
8
- agent: string;
9
12
  name: string;
10
13
  }, unknown, unknown, unknown>;
11
14
  /**
12
15
  * Store or update a credential.
16
+ *
17
+ * Request body is treated as an opaque blob. axvault does not validate its
18
+ * structure - the client (via axauth) is responsible for providing a valid
19
+ * Credentials object. The blob is encrypted and stored as-is.
13
20
  */
14
21
  declare function createPutCredentialHandler(database: Database.Database): CredentialHandler;
15
22
  export { createPutCredentialHandler };