@blokjs/runner 0.6.21 → 0.7.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 (167) hide show
  1. package/dist/Blok.d.ts +2 -0
  2. package/dist/Blok.js +42 -110
  3. package/dist/Blok.js.map +1 -1
  4. package/dist/DefaultLogger.d.ts +13 -0
  5. package/dist/DefaultLogger.js +25 -0
  6. package/dist/DefaultLogger.js.map +1 -1
  7. package/dist/RunnerSteps.d.ts +23 -0
  8. package/dist/RunnerSteps.js +128 -87
  9. package/dist/RunnerSteps.js.map +1 -1
  10. package/dist/SubworkflowNode.js +19 -0
  11. package/dist/SubworkflowNode.js.map +1 -1
  12. package/dist/TriggerBase.d.ts +12 -0
  13. package/dist/TriggerBase.js +216 -181
  14. package/dist/TriggerBase.js.map +1 -1
  15. package/dist/adapters/grpc/GrpcRuntimeAdapter.d.ts +9 -0
  16. package/dist/adapters/grpc/GrpcRuntimeAdapter.js +76 -6
  17. package/dist/adapters/grpc/GrpcRuntimeAdapter.js.map +1 -1
  18. package/dist/index.d.ts +4 -39
  19. package/dist/index.js +7 -32
  20. package/dist/index.js.map +1 -1
  21. package/dist/monitoring/JanitorMetrics.d.ts +3 -0
  22. package/dist/monitoring/JanitorMetrics.js +11 -0
  23. package/dist/monitoring/JanitorMetrics.js.map +1 -1
  24. package/dist/monitoring/ProcessErrorMetrics.d.ts +32 -0
  25. package/dist/monitoring/ProcessErrorMetrics.js +43 -0
  26. package/dist/monitoring/ProcessErrorMetrics.js.map +1 -0
  27. package/dist/monitoring/PrometheusMetricsBridge.d.ts +7 -0
  28. package/dist/monitoring/PrometheusMetricsBridge.js +8 -2
  29. package/dist/monitoring/PrometheusMetricsBridge.js.map +1 -1
  30. package/dist/monitoring/SubworkflowMetrics.d.ts +25 -0
  31. package/dist/monitoring/SubworkflowMetrics.js +38 -0
  32. package/dist/monitoring/SubworkflowMetrics.js.map +1 -0
  33. package/dist/observability/ErrorSink.d.ts +23 -0
  34. package/dist/observability/ErrorSink.js +32 -0
  35. package/dist/observability/ErrorSink.js.map +1 -0
  36. package/dist/observability/SentryIntegration.d.ts +9 -0
  37. package/dist/observability/SentryIntegration.js +31 -0
  38. package/dist/observability/SentryIntegration.js.map +1 -0
  39. package/dist/scheduling/DebounceCoordinator.d.ts +7 -53
  40. package/dist/scheduling/DebounceCoordinator.js +8 -207
  41. package/dist/scheduling/DebounceCoordinator.js.map +1 -1
  42. package/dist/tracing/InMemoryRunStore.d.ts +5 -1
  43. package/dist/tracing/InMemoryRunStore.js +14 -0
  44. package/dist/tracing/InMemoryRunStore.js.map +1 -1
  45. package/dist/tracing/Janitor.js +3 -0
  46. package/dist/tracing/Janitor.js.map +1 -1
  47. package/dist/tracing/PostgresRunStore.d.ts +4 -1
  48. package/dist/tracing/PostgresRunStore.js +73 -3
  49. package/dist/tracing/PostgresRunStore.js.map +1 -1
  50. package/dist/tracing/RunStore.d.ts +17 -1
  51. package/dist/tracing/RunTracker.d.ts +13 -34
  52. package/dist/tracing/RunTracker.js +62 -32
  53. package/dist/tracing/RunTracker.js.map +1 -1
  54. package/dist/tracing/SqliteRunStore.d.ts +4 -1
  55. package/dist/tracing/SqliteRunStore.js +60 -0
  56. package/dist/tracing/SqliteRunStore.js.map +1 -1
  57. package/dist/tracing/TraceRouter.d.ts +13 -0
  58. package/dist/tracing/TraceRouter.js +43 -11
  59. package/dist/tracing/TraceRouter.js.map +1 -1
  60. package/dist/tracing/TracingLogger.js +22 -0
  61. package/dist/tracing/TracingLogger.js.map +1 -1
  62. package/dist/tracing/createStore.js +51 -22
  63. package/dist/tracing/createStore.js.map +1 -1
  64. package/dist/tracing/types.d.ts +22 -0
  65. package/dist/types/GlobalOptions.d.ts +5 -7
  66. package/dist/workflow/WorkflowNormalizer.js +63 -0
  67. package/dist/workflow/WorkflowNormalizer.js.map +1 -1
  68. package/package.json +7 -4
  69. package/dist/cache/NodeResultCache.d.ts +0 -286
  70. package/dist/cache/NodeResultCache.js +0 -506
  71. package/dist/cache/NodeResultCache.js.map +0 -1
  72. package/dist/cache/index.d.ts +0 -1
  73. package/dist/cache/index.js +0 -2
  74. package/dist/cache/index.js.map +0 -1
  75. package/dist/concurrency/ConcurrencyBackend.d.ts +0 -61
  76. package/dist/concurrency/ConcurrencyBackend.js +0 -20
  77. package/dist/concurrency/ConcurrencyBackend.js.map +0 -1
  78. package/dist/concurrency/NatsKvConcurrencyBackend.d.ts +0 -64
  79. package/dist/concurrency/NatsKvConcurrencyBackend.js +0 -310
  80. package/dist/concurrency/NatsKvConcurrencyBackend.js.map +0 -1
  81. package/dist/concurrency/RedisConcurrencyBackend.d.ts +0 -64
  82. package/dist/concurrency/RedisConcurrencyBackend.js +0 -374
  83. package/dist/concurrency/RedisConcurrencyBackend.js.map +0 -1
  84. package/dist/concurrency/createConcurrencyBackend.d.ts +0 -24
  85. package/dist/concurrency/createConcurrencyBackend.js +0 -38
  86. package/dist/concurrency/createConcurrencyBackend.js.map +0 -1
  87. package/dist/graphql/GraphQLSchemaGenerator.d.ts +0 -129
  88. package/dist/graphql/GraphQLSchemaGenerator.js +0 -425
  89. package/dist/graphql/GraphQLSchemaGenerator.js.map +0 -1
  90. package/dist/integrations/APMIntegration.d.ts +0 -141
  91. package/dist/integrations/APMIntegration.js +0 -212
  92. package/dist/integrations/APMIntegration.js.map +0 -1
  93. package/dist/integrations/AzureMonitorIntegration.d.ts +0 -118
  94. package/dist/integrations/AzureMonitorIntegration.js +0 -254
  95. package/dist/integrations/AzureMonitorIntegration.js.map +0 -1
  96. package/dist/integrations/CloudWatchIntegration.d.ts +0 -135
  97. package/dist/integrations/CloudWatchIntegration.js +0 -293
  98. package/dist/integrations/CloudWatchIntegration.js.map +0 -1
  99. package/dist/integrations/SentryIntegration.d.ts +0 -153
  100. package/dist/integrations/SentryIntegration.js +0 -200
  101. package/dist/integrations/SentryIntegration.js.map +0 -1
  102. package/dist/integrations/index.d.ts +0 -19
  103. package/dist/integrations/index.js +0 -16
  104. package/dist/integrations/index.js.map +0 -1
  105. package/dist/marketplace/RuntimeAutoScaler.d.ts +0 -148
  106. package/dist/marketplace/RuntimeAutoScaler.js +0 -366
  107. package/dist/marketplace/RuntimeAutoScaler.js.map +0 -1
  108. package/dist/marketplace/RuntimeCatalog.d.ts +0 -180
  109. package/dist/marketplace/RuntimeCatalog.js +0 -339
  110. package/dist/marketplace/RuntimeCatalog.js.map +0 -1
  111. package/dist/marketplace/RuntimeDiscovery.d.ts +0 -86
  112. package/dist/marketplace/RuntimeDiscovery.js +0 -231
  113. package/dist/marketplace/RuntimeDiscovery.js.map +0 -1
  114. package/dist/marketplace/RuntimeHealthMonitor.d.ts +0 -100
  115. package/dist/marketplace/RuntimeHealthMonitor.js +0 -241
  116. package/dist/marketplace/RuntimeHealthMonitor.js.map +0 -1
  117. package/dist/marketplace/RuntimeMetricsDashboard.d.ts +0 -113
  118. package/dist/marketplace/RuntimeMetricsDashboard.js +0 -293
  119. package/dist/marketplace/RuntimeMetricsDashboard.js.map +0 -1
  120. package/dist/openapi/OpenAPIGenerator.d.ts +0 -192
  121. package/dist/openapi/OpenAPIGenerator.js +0 -378
  122. package/dist/openapi/OpenAPIGenerator.js.map +0 -1
  123. package/dist/openapi/index.d.ts +0 -20
  124. package/dist/openapi/index.js +0 -20
  125. package/dist/openapi/index.js.map +0 -1
  126. package/dist/scheduling/DebounceBackend.d.ts +0 -108
  127. package/dist/scheduling/DebounceBackend.js +0 -23
  128. package/dist/scheduling/DebounceBackend.js.map +0 -1
  129. package/dist/scheduling/NatsKvDebounceBackend.d.ts +0 -53
  130. package/dist/scheduling/NatsKvDebounceBackend.js +0 -334
  131. package/dist/scheduling/NatsKvDebounceBackend.js.map +0 -1
  132. package/dist/scheduling/RedisDebounceBackend.d.ts +0 -49
  133. package/dist/scheduling/RedisDebounceBackend.js +0 -356
  134. package/dist/scheduling/RedisDebounceBackend.js.map +0 -1
  135. package/dist/scheduling/createDebounceBackend.d.ts +0 -25
  136. package/dist/scheduling/createDebounceBackend.js +0 -39
  137. package/dist/scheduling/createDebounceBackend.js.map +0 -1
  138. package/dist/security/ABAC.d.ts +0 -224
  139. package/dist/security/ABAC.js +0 -380
  140. package/dist/security/ABAC.js.map +0 -1
  141. package/dist/security/AuditLogger.d.ts +0 -242
  142. package/dist/security/AuditLogger.js +0 -317
  143. package/dist/security/AuditLogger.js.map +0 -1
  144. package/dist/security/AuthMiddleware.d.ts +0 -162
  145. package/dist/security/AuthMiddleware.js +0 -289
  146. package/dist/security/AuthMiddleware.js.map +0 -1
  147. package/dist/security/EncryptionAtRest.d.ts +0 -206
  148. package/dist/security/EncryptionAtRest.js +0 -236
  149. package/dist/security/EncryptionAtRest.js.map +0 -1
  150. package/dist/security/OAuthProvider.d.ts +0 -334
  151. package/dist/security/OAuthProvider.js +0 -719
  152. package/dist/security/OAuthProvider.js.map +0 -1
  153. package/dist/security/PIIDetector.d.ts +0 -233
  154. package/dist/security/PIIDetector.js +0 -354
  155. package/dist/security/PIIDetector.js.map +0 -1
  156. package/dist/security/RBAC.d.ts +0 -143
  157. package/dist/security/RBAC.js +0 -285
  158. package/dist/security/RBAC.js.map +0 -1
  159. package/dist/security/SecretManager.d.ts +0 -652
  160. package/dist/security/SecretManager.js +0 -1147
  161. package/dist/security/SecretManager.js.map +0 -1
  162. package/dist/security/TLSConfig.d.ts +0 -305
  163. package/dist/security/TLSConfig.js +0 -550
  164. package/dist/security/TLSConfig.js.map +0 -1
  165. package/dist/security/index.d.ts +0 -81
  166. package/dist/security/index.js +0 -82
  167. package/dist/security/index.js.map +0 -1
@@ -1,1147 +0,0 @@
1
- /**
2
- * Secret Management for Blok Framework
3
- *
4
- * Provides a unified interface for secret management across multiple providers:
5
- * - HashiCorp Vault (KV v2 engine via REST API)
6
- * - AWS Secrets Manager (via @aws-sdk/client-secrets-manager)
7
- * - GCP Secret Manager (via @google-cloud/secret-manager)
8
- * - Environment Variables (process.env)
9
- * - In-Memory (for testing)
10
- *
11
- * Features:
12
- * - Provider chain: try providers in order, first match wins
13
- * - Caching layer with TTL and max size (LRU eviction)
14
- * - Audit event emission for secret access tracking
15
- * - Template resolution for `${secret:KEY}` patterns
16
- *
17
- * @example
18
- * ```typescript
19
- * import {
20
- * SecretManager,
21
- * EnvironmentSecretProvider,
22
- * InMemorySecretProvider,
23
- * } from "@blokjs/runner";
24
- *
25
- * // Simple setup with environment variables
26
- * const secrets = new SecretManager({
27
- * providers: [
28
- * { type: "environment", config: { prefix: "BLOK_SECRET_" } },
29
- * ],
30
- * cache: { enabled: true, ttlMs: 60_000, maxSize: 100 },
31
- * auditLog: true,
32
- * });
33
- *
34
- * const dbPassword = await secrets.getSecret("DB_PASSWORD");
35
- * const connStr = await secrets.resolveTemplate(
36
- * "postgres://user:${secret:DB_PASSWORD}@host/db"
37
- * );
38
- * ```
39
- */
40
- import { EventEmitter } from "node:events";
41
- // ---------------------------------------------------------------------------
42
- // EnvironmentSecretProvider
43
- // ---------------------------------------------------------------------------
44
- /**
45
- * Secret provider backed by process.env
46
- *
47
- * Reads environment variables, optionally with a prefix. Supports
48
- * case-insensitive lookups when configured.
49
- *
50
- * @example
51
- * ```typescript
52
- * const provider = new EnvironmentSecretProvider({ prefix: "APP_" });
53
- * // Reads process.env.APP_DATABASE_URL
54
- * const dbUrl = await provider.get("DATABASE_URL");
55
- * ```
56
- */
57
- export class EnvironmentSecretProvider {
58
- name = "environment";
59
- prefix;
60
- caseSensitive;
61
- constructor(config) {
62
- this.prefix = config?.prefix ?? "";
63
- this.caseSensitive = config?.caseSensitive ?? true;
64
- }
65
- /**
66
- * Retrieve an environment variable value
67
- * @param key - Variable name (without prefix)
68
- */
69
- async get(key) {
70
- const envKey = this.resolveKey(key);
71
- const value = process.env[envKey];
72
- return value !== undefined ? value : null;
73
- }
74
- /**
75
- * Set an environment variable (primarily useful for testing)
76
- * @param key - Variable name (without prefix)
77
- * @param value - Value to set
78
- */
79
- async set(key, value, _metadata) {
80
- const envKey = this.resolveKey(key);
81
- process.env[envKey] = value;
82
- }
83
- /**
84
- * Delete an environment variable
85
- * @param key - Variable name (without prefix)
86
- */
87
- async delete(key) {
88
- const envKey = this.resolveKey(key);
89
- delete process.env[envKey];
90
- }
91
- /**
92
- * List environment variable names matching the configured prefix
93
- * @param prefix - Additional prefix to filter by (applied after the provider prefix)
94
- */
95
- async list(prefix) {
96
- const fullPrefix = this.prefix + (prefix ?? "");
97
- const keys = Object.keys(process.env).filter((k) => {
98
- const candidate = this.caseSensitive ? k : k.toUpperCase();
99
- const match = this.caseSensitive ? fullPrefix : fullPrefix.toUpperCase();
100
- return candidate.startsWith(match);
101
- });
102
- // Strip the provider prefix from returned keys
103
- return keys.map((k) => k.slice(this.prefix.length));
104
- }
105
- /**
106
- * Check whether an environment variable exists
107
- * @param key - Variable name (without prefix)
108
- */
109
- async exists(key) {
110
- const envKey = this.resolveKey(key);
111
- return envKey in process.env;
112
- }
113
- /**
114
- * Build the full environment variable name from a logical key
115
- */
116
- resolveKey(key) {
117
- const fullKey = this.prefix + key;
118
- if (this.caseSensitive) {
119
- return fullKey;
120
- }
121
- // For case-insensitive mode, find the matching key in process.env
122
- const upper = fullKey.toUpperCase();
123
- const match = Object.keys(process.env).find((k) => k.toUpperCase() === upper);
124
- return match ?? fullKey;
125
- }
126
- }
127
- // ---------------------------------------------------------------------------
128
- // InMemorySecretProvider
129
- // ---------------------------------------------------------------------------
130
- /**
131
- * In-memory secret provider for testing and development
132
- *
133
- * Stores secrets in a Map with full CRUD support. Provides stats
134
- * for debugging and verification.
135
- *
136
- * @example
137
- * ```typescript
138
- * const provider = new InMemorySecretProvider();
139
- * await provider.set("API_KEY", "test-key-123");
140
- * const key = await provider.get("API_KEY"); // "test-key-123"
141
- * console.log(provider.getStats()); // { size: 1, keys: ["API_KEY"] }
142
- * ```
143
- */
144
- export class InMemorySecretProvider {
145
- name = "memory";
146
- store = new Map();
147
- /**
148
- * Retrieve a secret from the in-memory store
149
- * @param key - The secret key
150
- */
151
- async get(key) {
152
- const entry = this.store.get(key);
153
- if (!entry)
154
- return null;
155
- // Check expiration
156
- if (entry.metadata?.expiresAt && entry.metadata.expiresAt < Date.now()) {
157
- this.store.delete(key);
158
- return null;
159
- }
160
- return entry.value;
161
- }
162
- /**
163
- * Store a secret in the in-memory store
164
- * @param key - The secret key
165
- * @param value - The secret value
166
- * @param metadata - Optional metadata
167
- */
168
- async set(key, value, metadata) {
169
- this.store.set(key, { value, metadata });
170
- }
171
- /**
172
- * Delete a secret from the in-memory store
173
- * @param key - The secret key
174
- */
175
- async delete(key) {
176
- this.store.delete(key);
177
- }
178
- /**
179
- * List all secret keys, optionally filtered by prefix
180
- * @param prefix - Optional prefix filter
181
- */
182
- async list(prefix) {
183
- const keys = Array.from(this.store.keys());
184
- if (!prefix)
185
- return keys;
186
- return keys.filter((k) => k.startsWith(prefix));
187
- }
188
- /**
189
- * Check whether a secret exists in the store
190
- * @param key - The secret key
191
- */
192
- async exists(key) {
193
- // `has`-then-`get` is the pattern; the `get` cannot return undefined
194
- // here because `has` returned true. Defensive-default to avoid the
195
- // non-null assertion biome flags.
196
- const entry = this.store.get(key);
197
- if (!entry)
198
- return false;
199
- if (entry.metadata?.expiresAt && entry.metadata.expiresAt < Date.now()) {
200
- this.store.delete(key);
201
- return false;
202
- }
203
- return true;
204
- }
205
- /**
206
- * Get debug statistics about the in-memory store
207
- * @returns Object with size and list of keys
208
- */
209
- getStats() {
210
- return {
211
- size: this.store.size,
212
- keys: Array.from(this.store.keys()),
213
- };
214
- }
215
- /**
216
- * Clear all secrets from the store
217
- */
218
- clear() {
219
- this.store.clear();
220
- }
221
- }
222
- // ---------------------------------------------------------------------------
223
- // VaultSecretProvider
224
- // ---------------------------------------------------------------------------
225
- /**
226
- * HashiCorp Vault secret provider (KV v2 engine)
227
- *
228
- * Communicates with Vault via its HTTP REST API using the native `fetch` API.
229
- * Supports token-based authentication, namespaces, and configurable mount paths.
230
- *
231
- * @example
232
- * ```typescript
233
- * const vault = new VaultSecretProvider({
234
- * address: "https://vault.example.com:8200",
235
- * token: process.env.VAULT_TOKEN,
236
- * mountPath: "secret",
237
- * });
238
- *
239
- * const dbPassword = await vault.get("database/credentials");
240
- * ```
241
- */
242
- export class VaultSecretProvider {
243
- name = "vault";
244
- address;
245
- token;
246
- namespace;
247
- mountPath;
248
- apiVersion;
249
- constructor(config) {
250
- this.address = config.address.replace(/\/+$/, "");
251
- this.token = config.token ?? "";
252
- this.namespace = config.namespace;
253
- this.mountPath = config.mountPath ?? "secret";
254
- this.apiVersion = config.apiVersion ?? "v1";
255
- }
256
- /**
257
- * Read a secret from Vault KV v2
258
- * @param key - The secret path within the mount
259
- */
260
- async get(key) {
261
- const url = this.buildUrl("data", key);
262
- const response = await fetch(url, {
263
- method: "GET",
264
- headers: this.buildHeaders(),
265
- });
266
- if (response.status === 404)
267
- return null;
268
- if (!response.ok) {
269
- const body = await response.text();
270
- throw new Error(`Vault GET failed (${response.status}): ${body}`);
271
- }
272
- const json = (await response.json());
273
- // KV v2 nests the actual data under data.data
274
- const value = json.data?.data?.value;
275
- if (typeof value === "string")
276
- return value;
277
- // If the secret has multiple fields, return as JSON
278
- if (json.data?.data && typeof json.data.data === "object") {
279
- return JSON.stringify(json.data.data);
280
- }
281
- return null;
282
- }
283
- /**
284
- * Write a secret to Vault KV v2
285
- * @param key - The secret path within the mount
286
- * @param value - The secret value
287
- * @param metadata - Optional metadata (stored as custom_metadata)
288
- */
289
- async set(key, value, metadata) {
290
- const url = this.buildUrl("data", key);
291
- const body = {
292
- data: { value },
293
- };
294
- if (metadata) {
295
- body.options = {};
296
- if (metadata.version) {
297
- body.options.cas = Number.parseInt(metadata.version, 10);
298
- }
299
- }
300
- const response = await fetch(url, {
301
- method: "POST",
302
- headers: this.buildHeaders(),
303
- body: JSON.stringify(body),
304
- });
305
- if (!response.ok) {
306
- const responseBody = await response.text();
307
- throw new Error(`Vault POST failed (${response.status}): ${responseBody}`);
308
- }
309
- // Set custom metadata if provided
310
- if (metadata?.tags || metadata?.description) {
311
- await this.setMetadata(key, metadata);
312
- }
313
- }
314
- /**
315
- * Delete a secret from Vault KV v2
316
- * @param key - The secret path within the mount
317
- */
318
- async delete(key) {
319
- const url = this.buildUrl("metadata", key);
320
- const response = await fetch(url, {
321
- method: "DELETE",
322
- headers: this.buildHeaders(),
323
- });
324
- if (!response.ok && response.status !== 404) {
325
- const body = await response.text();
326
- throw new Error(`Vault DELETE failed (${response.status}): ${body}`);
327
- }
328
- }
329
- /**
330
- * List secret keys under a given path prefix
331
- * @param prefix - Optional path prefix
332
- */
333
- async list(prefix) {
334
- const path = prefix ?? "";
335
- const url = `${this.buildUrl("metadata", path)}?list=true`;
336
- const response = await fetch(url, {
337
- method: "LIST",
338
- headers: this.buildHeaders(),
339
- });
340
- if (response.status === 404)
341
- return [];
342
- if (!response.ok) {
343
- const body = await response.text();
344
- throw new Error(`Vault LIST failed (${response.status}): ${body}`);
345
- }
346
- const json = (await response.json());
347
- return json.data?.keys ?? [];
348
- }
349
- /**
350
- * Check whether a secret exists in Vault
351
- * @param key - The secret path within the mount
352
- */
353
- async exists(key) {
354
- const url = this.buildUrl("data", key);
355
- const response = await fetch(url, {
356
- method: "GET",
357
- headers: this.buildHeaders(),
358
- });
359
- return response.ok;
360
- }
361
- /**
362
- * Update the Vault token (e.g., after token renewal)
363
- * @param token - The new Vault token
364
- */
365
- setToken(token) {
366
- this.token = token;
367
- }
368
- /**
369
- * Build the full URL for a Vault KV v2 API call
370
- */
371
- buildUrl(operation, path) {
372
- const cleanPath = path.replace(/^\/+|\/+$/g, "");
373
- return `${this.address}/${this.apiVersion}/${this.mountPath}/${operation}/${cleanPath}`;
374
- }
375
- /**
376
- * Build common HTTP headers for Vault requests
377
- */
378
- buildHeaders() {
379
- const headers = {
380
- "Content-Type": "application/json",
381
- };
382
- if (this.token) {
383
- headers["X-Vault-Token"] = this.token;
384
- }
385
- if (this.namespace) {
386
- headers["X-Vault-Namespace"] = this.namespace;
387
- }
388
- return headers;
389
- }
390
- /**
391
- * Set custom metadata on a secret in Vault KV v2
392
- */
393
- async setMetadata(key, metadata) {
394
- const url = this.buildUrl("metadata", key);
395
- const body = {
396
- custom_metadata: {
397
- ...metadata.tags,
398
- ...(metadata.description ? { description: metadata.description } : {}),
399
- },
400
- };
401
- const response = await fetch(url, {
402
- method: "POST",
403
- headers: this.buildHeaders(),
404
- body: JSON.stringify(body),
405
- });
406
- if (!response.ok) {
407
- // Non-fatal: metadata update failure should not break the set operation
408
- const responseBody = await response.text();
409
- console.warn(`Vault metadata update failed (${response.status}): ${responseBody}`);
410
- }
411
- }
412
- }
413
- // ---------------------------------------------------------------------------
414
- // AWSSecretsProvider
415
- // ---------------------------------------------------------------------------
416
- /**
417
- * AWS Secrets Manager provider
418
- *
419
- * Uses the `@aws-sdk/client-secrets-manager` SDK, loaded dynamically at
420
- * first use to avoid hard dependencies.
421
- *
422
- * @example
423
- * ```typescript
424
- * const aws = new AWSSecretsProvider({
425
- * region: "us-east-1",
426
- * });
427
- *
428
- * const apiKey = await aws.get("prod/api-key");
429
- * ```
430
- */
431
- export class AWSSecretsProvider {
432
- name = "aws";
433
- region;
434
- accessKeyId;
435
- secretAccessKey;
436
- profile;
437
- client = null;
438
- constructor(config) {
439
- this.region = config.region;
440
- this.accessKeyId = config.accessKeyId;
441
- this.secretAccessKey = config.secretAccessKey;
442
- this.profile = config.profile;
443
- }
444
- /**
445
- * Retrieve a secret from AWS Secrets Manager
446
- * @param key - The secret name or ARN
447
- */
448
- async get(key) {
449
- const client = await this.getClient();
450
- const { GetSecretValueCommand } = await this.getSDK();
451
- try {
452
- const result = await client.send(new GetSecretValueCommand({ SecretId: key }));
453
- return result.SecretString ?? null;
454
- }
455
- catch (err) {
456
- if (this.isAWSError(err, "ResourceNotFoundException")) {
457
- return null;
458
- }
459
- throw err;
460
- }
461
- }
462
- /**
463
- * Create or update a secret in AWS Secrets Manager
464
- * @param key - The secret name
465
- * @param value - The secret value
466
- * @param metadata - Optional metadata (tags and description supported)
467
- */
468
- async set(key, value, metadata) {
469
- const client = await this.getClient();
470
- const sdk = await this.getSDK();
471
- // Try to update first, create if it does not exist
472
- try {
473
- await client.send(new sdk.UpdateSecretCommand({
474
- SecretId: key,
475
- SecretString: value,
476
- ...(metadata?.description ? { Description: metadata.description } : {}),
477
- }));
478
- }
479
- catch (err) {
480
- if (this.isAWSError(err, "ResourceNotFoundException")) {
481
- const createParams = {
482
- Name: key,
483
- SecretString: value,
484
- };
485
- if (metadata?.description) {
486
- createParams.Description = metadata.description;
487
- }
488
- if (metadata?.tags) {
489
- createParams.Tags = Object.entries(metadata.tags).map(([Key, Value]) => ({
490
- Key,
491
- Value,
492
- }));
493
- }
494
- await client.send(new sdk.CreateSecretCommand(createParams));
495
- }
496
- else {
497
- throw err;
498
- }
499
- }
500
- }
501
- /**
502
- * Delete a secret from AWS Secrets Manager
503
- * @param key - The secret name or ARN
504
- */
505
- async delete(key) {
506
- const client = await this.getClient();
507
- const { DeleteSecretCommand } = await this.getSDK();
508
- try {
509
- await client.send(new DeleteSecretCommand({
510
- SecretId: key,
511
- ForceDeleteWithoutRecovery: true,
512
- }));
513
- }
514
- catch (err) {
515
- if (!this.isAWSError(err, "ResourceNotFoundException")) {
516
- throw err;
517
- }
518
- }
519
- }
520
- /**
521
- * List secrets in AWS Secrets Manager, optionally filtered by name prefix
522
- * @param prefix - Optional name prefix filter
523
- */
524
- async list(prefix) {
525
- const client = await this.getClient();
526
- const { ListSecretsCommand } = await this.getSDK();
527
- const secrets = [];
528
- let nextToken;
529
- do {
530
- const params = {
531
- MaxResults: 100,
532
- ...(nextToken ? { NextToken: nextToken } : {}),
533
- };
534
- if (prefix) {
535
- params.Filters = [{ Key: "name", Values: [prefix] }];
536
- }
537
- const result = await client.send(new ListSecretsCommand(params));
538
- if (result.SecretList) {
539
- for (const secret of result.SecretList) {
540
- if (secret.Name) {
541
- secrets.push(secret.Name);
542
- }
543
- }
544
- }
545
- nextToken = result.NextToken;
546
- } while (nextToken);
547
- return secrets;
548
- }
549
- /**
550
- * Check whether a secret exists in AWS Secrets Manager
551
- * @param key - The secret name or ARN
552
- */
553
- async exists(key) {
554
- const client = await this.getClient();
555
- const { DescribeSecretCommand } = await this.getSDK();
556
- try {
557
- await client.send(new DescribeSecretCommand({ SecretId: key }));
558
- return true;
559
- }
560
- catch (err) {
561
- if (this.isAWSError(err, "ResourceNotFoundException")) {
562
- return false;
563
- }
564
- throw err;
565
- }
566
- }
567
- /**
568
- * Lazily initialize and cache the AWS SecretsManager client
569
- */
570
- async getClient() {
571
- if (this.client)
572
- return this.client;
573
- const { SecretsManagerClient } = await this.getSDK();
574
- const clientConfig = {
575
- region: this.region,
576
- };
577
- if (this.accessKeyId && this.secretAccessKey) {
578
- clientConfig.credentials = {
579
- accessKeyId: this.accessKeyId,
580
- secretAccessKey: this.secretAccessKey,
581
- };
582
- }
583
- if (this.profile) {
584
- // When a profile is specified, set the AWS_PROFILE env var so
585
- // the SDK default credential chain picks it up.
586
- process.env.AWS_PROFILE = this.profile;
587
- }
588
- this.client = new SecretsManagerClient(clientConfig);
589
- return this.client;
590
- }
591
- /**
592
- * Dynamically import the AWS Secrets Manager SDK
593
- */
594
- async getSDK() {
595
- try {
596
- // @ts-ignore -- optional peer dependency, loaded dynamically at runtime
597
- return (await import("@aws-sdk/client-secrets-manager"));
598
- }
599
- catch {
600
- throw new Error("AWS Secrets Manager SDK not found. Install it with: npm install @aws-sdk/client-secrets-manager");
601
- }
602
- }
603
- /**
604
- * Type-safe check for AWS SDK error names
605
- */
606
- isAWSError(err, code) {
607
- return typeof err === "object" && err !== null && "name" in err && err.name === code;
608
- }
609
- }
610
- // ---------------------------------------------------------------------------
611
- // GCPSecretProvider
612
- // ---------------------------------------------------------------------------
613
- /**
614
- * Google Cloud Secret Manager provider
615
- *
616
- * Uses the `@google-cloud/secret-manager` SDK, loaded dynamically at
617
- * first use to avoid hard dependencies.
618
- *
619
- * @example
620
- * ```typescript
621
- * const gcp = new GCPSecretProvider({
622
- * projectId: "my-project",
623
- * });
624
- *
625
- * const apiKey = await gcp.get("api-key");
626
- * ```
627
- */
628
- export class GCPSecretProvider {
629
- name = "gcp";
630
- projectId;
631
- keyFile;
632
- client = null;
633
- constructor(config) {
634
- this.projectId = config.projectId;
635
- this.keyFile = config.keyFile;
636
- }
637
- /**
638
- * Retrieve the latest version of a secret from GCP Secret Manager
639
- * @param key - The secret ID
640
- */
641
- async get(key) {
642
- const client = await this.getClient();
643
- try {
644
- const [version] = await client.accessSecretVersion({
645
- name: `projects/${this.projectId}/secrets/${key}/versions/latest`,
646
- });
647
- const payload = version.payload?.data;
648
- if (!payload)
649
- return null;
650
- if (typeof payload === "string")
651
- return payload;
652
- if (payload instanceof Uint8Array || Buffer.isBuffer(payload)) {
653
- return Buffer.from(payload).toString("utf-8");
654
- }
655
- return null;
656
- }
657
- catch (err) {
658
- if (this.isGCPNotFoundError(err)) {
659
- return null;
660
- }
661
- throw err;
662
- }
663
- }
664
- /**
665
- * Create a secret and add a version, or add a new version to an existing secret
666
- * @param key - The secret ID
667
- * @param value - The secret value
668
- * @param metadata - Optional metadata (tags mapped to GCP labels)
669
- */
670
- async set(key, value, metadata) {
671
- const client = await this.getClient();
672
- const parent = `projects/${this.projectId}`;
673
- const secretName = `${parent}/secrets/${key}`;
674
- // Try to create the secret resource first
675
- try {
676
- const createRequest = {
677
- parent,
678
- secretId: key,
679
- secret: {
680
- replication: { automatic: {} },
681
- ...(metadata?.tags ? { labels: metadata.tags } : {}),
682
- },
683
- };
684
- await client.createSecret(createRequest);
685
- }
686
- catch (err) {
687
- // 6 = ALREADY_EXISTS - that is fine, we will add a version
688
- if (!this.isGCPError(err, 6)) {
689
- throw err;
690
- }
691
- }
692
- // Add the secret version with the actual payload
693
- await client.addSecretVersion({
694
- parent: secretName,
695
- payload: {
696
- data: Buffer.from(value, "utf-8"),
697
- },
698
- });
699
- }
700
- /**
701
- * Delete a secret from GCP Secret Manager
702
- * @param key - The secret ID
703
- */
704
- async delete(key) {
705
- const client = await this.getClient();
706
- try {
707
- await client.deleteSecret({
708
- name: `projects/${this.projectId}/secrets/${key}`,
709
- });
710
- }
711
- catch (err) {
712
- if (!this.isGCPNotFoundError(err)) {
713
- throw err;
714
- }
715
- }
716
- }
717
- /**
718
- * List secrets in the GCP project, optionally filtered by prefix
719
- * @param prefix - Optional prefix filter applied to secret IDs
720
- */
721
- async list(prefix) {
722
- const client = await this.getClient();
723
- const parent = `projects/${this.projectId}`;
724
- const [secrets] = await client.listSecrets({ parent });
725
- const names = [];
726
- for (const secret of secrets) {
727
- // Extract secret ID from the full resource name
728
- const fullName = secret.name ?? "";
729
- const parts = fullName.split("/");
730
- const secretId = parts[parts.length - 1];
731
- if (secretId) {
732
- if (!prefix || secretId.startsWith(prefix)) {
733
- names.push(secretId);
734
- }
735
- }
736
- }
737
- return names;
738
- }
739
- /**
740
- * Check whether a secret exists in GCP Secret Manager
741
- * @param key - The secret ID
742
- */
743
- async exists(key) {
744
- const client = await this.getClient();
745
- try {
746
- await client.getSecret({
747
- name: `projects/${this.projectId}/secrets/${key}`,
748
- });
749
- return true;
750
- }
751
- catch (err) {
752
- if (this.isGCPNotFoundError(err)) {
753
- return false;
754
- }
755
- throw err;
756
- }
757
- }
758
- /**
759
- * Lazily initialize and cache the GCP Secret Manager client
760
- */
761
- async getClient() {
762
- if (this.client)
763
- return this.client;
764
- try {
765
- // @ts-ignore -- optional peer dependency, loaded dynamically at runtime
766
- const module = (await import("@google-cloud/secret-manager"));
767
- const { SecretManagerServiceClient } = module;
768
- const options = {};
769
- if (this.keyFile) {
770
- options.keyFilename = this.keyFile;
771
- }
772
- this.client = new SecretManagerServiceClient(options);
773
- return this.client;
774
- }
775
- catch {
776
- throw new Error("GCP Secret Manager SDK not found. Install it with: npm install @google-cloud/secret-manager");
777
- }
778
- }
779
- /**
780
- * Check for GCP "not found" errors (gRPC status code 5)
781
- */
782
- isGCPNotFoundError(err) {
783
- return this.isGCPError(err, 5);
784
- }
785
- /**
786
- * Check for specific gRPC error codes from the GCP SDK
787
- */
788
- isGCPError(err, code) {
789
- return typeof err === "object" && err !== null && "code" in err && err.code === code;
790
- }
791
- }
792
- // ---------------------------------------------------------------------------
793
- // SecretManager
794
- // ---------------------------------------------------------------------------
795
- /** Regex for matching `${secret:KEY}` patterns in template strings */
796
- const SECRET_TEMPLATE_REGEX = /\$\{secret:([^}]+)\}/g;
797
- /**
798
- * Unified Secret Manager for the Blok Framework
799
- *
800
- * Orchestrates multiple secret providers with a provider chain (first match
801
- * wins), optional caching, and audit event emission.
802
- *
803
- * @example
804
- * ```typescript
805
- * const manager = new SecretManager({
806
- * providers: [
807
- * { type: "vault", config: { address: "https://vault:8200", token: "s.xxx" } },
808
- * { type: "environment", config: { prefix: "BLOK_" } },
809
- * ],
810
- * cache: { enabled: true, ttlMs: 300_000, maxSize: 500 },
811
- * auditLog: true,
812
- * });
813
- *
814
- * manager.on("secretAccess", (event) => {
815
- * console.log(`[audit] ${event.operation} ${event.key} via ${event.provider}`);
816
- * });
817
- *
818
- * const password = await manager.getSecretOrThrow("DB_PASSWORD");
819
- * const connStr = await manager.resolveTemplate(
820
- * "postgres://admin:${secret:DB_PASSWORD}@db:5432/app"
821
- * );
822
- * ```
823
- */
824
- export class SecretManager extends EventEmitter {
825
- providers = [];
826
- cache = new Map();
827
- cacheConfig;
828
- auditLog;
829
- cacheAccessOrder = [];
830
- constructor(config) {
831
- super();
832
- this.cacheConfig = config.cache ?? { enabled: false, ttlMs: 0, maxSize: 0 };
833
- this.auditLog = config.auditLog ?? false;
834
- // Initialize providers in order
835
- for (const providerConfig of config.providers) {
836
- this.providers.push(this.createProvider(providerConfig));
837
- }
838
- }
839
- /**
840
- * Retrieve a secret value by key
841
- *
842
- * Checks the cache first (if enabled), then queries each provider
843
- * in order until a value is found.
844
- *
845
- * @param key - The secret key
846
- * @returns The secret value, or null if not found in any provider
847
- */
848
- async getSecret(key) {
849
- // Check cache
850
- if (this.cacheConfig.enabled) {
851
- const cached = this.getCached(key);
852
- if (cached !== undefined) {
853
- this.emitAccess("get", key, "cache", true, true);
854
- return cached;
855
- }
856
- }
857
- // Query providers in order
858
- for (const provider of this.providers) {
859
- try {
860
- const value = await provider.get(key);
861
- if (value !== null) {
862
- if (this.cacheConfig.enabled) {
863
- this.setCache(key, value);
864
- }
865
- this.emitAccess("get", key, provider.name, true, false);
866
- return value;
867
- }
868
- }
869
- catch (err) {
870
- this.emitAccess("get", key, provider.name, false, false, errorMessage(err));
871
- // Continue to next provider
872
- }
873
- }
874
- this.emitAccess("get", key, "none", true, false);
875
- return null;
876
- }
877
- /**
878
- * Retrieve a secret or throw if it does not exist
879
- *
880
- * @param key - The secret key
881
- * @returns The secret value
882
- * @throws Error if the secret is not found in any provider
883
- */
884
- async getSecretOrThrow(key) {
885
- const value = await this.getSecret(key);
886
- if (value === null) {
887
- throw new Error(`Secret '${key}' not found in any provider`);
888
- }
889
- return value;
890
- }
891
- /**
892
- * Store a secret value in the first writable provider
893
- *
894
- * @param key - The secret key
895
- * @param value - The secret value
896
- * @param metadata - Optional metadata to associate with the secret
897
- */
898
- async setSecret(key, value, metadata) {
899
- let written = false;
900
- for (const provider of this.providers) {
901
- try {
902
- await provider.set(key, value, metadata);
903
- written = true;
904
- // Update cache
905
- if (this.cacheConfig.enabled) {
906
- this.setCache(key, value);
907
- }
908
- this.emitAccess("set", key, provider.name, true, false);
909
- break;
910
- }
911
- catch (err) {
912
- this.emitAccess("set", key, provider.name, false, false, errorMessage(err));
913
- // Continue to next provider
914
- }
915
- }
916
- if (!written) {
917
- throw new Error(`Failed to set secret '${key}' in any provider`);
918
- }
919
- }
920
- /**
921
- * Delete a secret from all providers that contain it
922
- *
923
- * @param key - The secret key
924
- */
925
- async deleteSecret(key) {
926
- // Invalidate cache
927
- this.cache.delete(key);
928
- this.cacheAccessOrder = this.cacheAccessOrder.filter((k) => k !== key);
929
- for (const provider of this.providers) {
930
- try {
931
- await provider.delete(key);
932
- this.emitAccess("delete", key, provider.name, true, false);
933
- }
934
- catch (err) {
935
- this.emitAccess("delete", key, provider.name, false, false, errorMessage(err));
936
- }
937
- }
938
- }
939
- /**
940
- * List secret keys across all providers, optionally filtered by prefix
941
- *
942
- * Merges results from all providers and deduplicates.
943
- *
944
- * @param prefix - Optional prefix filter
945
- * @returns Deduplicated array of secret key names
946
- */
947
- async listSecrets(prefix) {
948
- const allKeys = new Set();
949
- for (const provider of this.providers) {
950
- try {
951
- const keys = await provider.list(prefix);
952
- for (const key of keys) {
953
- allKeys.add(key);
954
- }
955
- this.emitAccess("list", undefined, provider.name, true, false);
956
- }
957
- catch (err) {
958
- this.emitAccess("list", undefined, provider.name, false, false, errorMessage(err));
959
- }
960
- }
961
- return Array.from(allKeys);
962
- }
963
- /**
964
- * Check whether a secret exists in any provider
965
- *
966
- * @param key - The secret key
967
- * @returns True if the secret exists in at least one provider
968
- */
969
- async exists(key) {
970
- // Check cache first
971
- if (this.cacheConfig.enabled) {
972
- const cached = this.getCached(key);
973
- if (cached !== undefined) {
974
- return true;
975
- }
976
- }
977
- for (const provider of this.providers) {
978
- try {
979
- const found = await provider.exists(key);
980
- if (found) {
981
- this.emitAccess("exists", key, provider.name, true, false);
982
- return true;
983
- }
984
- }
985
- catch (err) {
986
- this.emitAccess("exists", key, provider.name, false, false, errorMessage(err));
987
- }
988
- }
989
- return false;
990
- }
991
- /**
992
- * Resolve `${secret:KEY}` patterns in a template string
993
- *
994
- * Replaces every occurrence of `${secret:SOME_KEY}` with the actual
995
- * secret value from the provider chain. Missing secrets are replaced
996
- * with an empty string.
997
- *
998
- * @param template - The template string with `${secret:...}` placeholders
999
- * @returns The resolved string with secret values substituted
1000
- *
1001
- * @example
1002
- * ```typescript
1003
- * const resolved = await manager.resolveTemplate(
1004
- * "mongodb://${secret:MONGO_USER}:${secret:MONGO_PASS}@host/db"
1005
- * );
1006
- * ```
1007
- */
1008
- async resolveTemplate(template) {
1009
- const matches = [];
1010
- // Reset regex state
1011
- SECRET_TEMPLATE_REGEX.lastIndex = 0;
1012
- // Sticky-regex iteration — pull each match in its own statement so
1013
- // the assignment isn't inside the loop condition (biome flags
1014
- // assign-in-expression).
1015
- let match = SECRET_TEMPLATE_REGEX.exec(template);
1016
- while (match !== null) {
1017
- matches.push({ placeholder: match[0], key: match[1] });
1018
- match = SECRET_TEMPLATE_REGEX.exec(template);
1019
- }
1020
- if (matches.length === 0)
1021
- return template;
1022
- // Resolve all secrets in parallel
1023
- const resolutions = await Promise.all(matches.map(async (m) => ({
1024
- placeholder: m.placeholder,
1025
- value: (await this.getSecret(m.key)) ?? "",
1026
- })));
1027
- let result = template;
1028
- for (const resolution of resolutions) {
1029
- result = result.split(resolution.placeholder).join(resolution.value);
1030
- }
1031
- return result;
1032
- }
1033
- /**
1034
- * Get the list of configured providers
1035
- * @returns Array of provider instances
1036
- */
1037
- getProviders() {
1038
- return [...this.providers];
1039
- }
1040
- /**
1041
- * Get current cache statistics
1042
- * @returns Object with cache size and hit information
1043
- */
1044
- getCacheStats() {
1045
- return {
1046
- size: this.cache.size,
1047
- maxSize: this.cacheConfig.maxSize,
1048
- enabled: this.cacheConfig.enabled,
1049
- };
1050
- }
1051
- /**
1052
- * Clear the secret cache
1053
- */
1054
- clearCache() {
1055
- this.cache.clear();
1056
- this.cacheAccessOrder = [];
1057
- }
1058
- // -----------------------------------------------------------------------
1059
- // Private helpers
1060
- // -----------------------------------------------------------------------
1061
- /**
1062
- * Create a provider instance from its configuration
1063
- */
1064
- createProvider(config) {
1065
- switch (config.type) {
1066
- case "environment":
1067
- return new EnvironmentSecretProvider(config.config);
1068
- case "memory":
1069
- return new InMemorySecretProvider();
1070
- case "vault":
1071
- return new VaultSecretProvider(config.config);
1072
- case "aws":
1073
- return new AWSSecretsProvider(config.config);
1074
- case "gcp":
1075
- return new GCPSecretProvider(config.config);
1076
- default: {
1077
- const exhaustive = config;
1078
- throw new Error(`Unknown secret provider type: ${exhaustive.type}`);
1079
- }
1080
- }
1081
- }
1082
- /**
1083
- * Retrieve a value from the cache, returning undefined if not found or expired
1084
- */
1085
- getCached(key) {
1086
- const entry = this.cache.get(key);
1087
- if (!entry)
1088
- return undefined;
1089
- if (Date.now() > entry.expiresAt) {
1090
- this.cache.delete(key);
1091
- this.cacheAccessOrder = this.cacheAccessOrder.filter((k) => k !== key);
1092
- return undefined;
1093
- }
1094
- // Move to end of access order (LRU)
1095
- this.cacheAccessOrder = this.cacheAccessOrder.filter((k) => k !== key);
1096
- this.cacheAccessOrder.push(key);
1097
- return entry.value;
1098
- }
1099
- /**
1100
- * Store a value in the cache with TTL, evicting LRU entries if at capacity
1101
- */
1102
- setCache(key, value) {
1103
- // Evict if at capacity
1104
- while (this.cache.size >= this.cacheConfig.maxSize && this.cacheAccessOrder.length > 0) {
1105
- const evict = this.cacheAccessOrder.shift();
1106
- if (evict) {
1107
- this.cache.delete(evict);
1108
- }
1109
- }
1110
- this.cache.set(key, {
1111
- value,
1112
- expiresAt: Date.now() + this.cacheConfig.ttlMs,
1113
- });
1114
- // Update access order
1115
- this.cacheAccessOrder = this.cacheAccessOrder.filter((k) => k !== key);
1116
- this.cacheAccessOrder.push(key);
1117
- }
1118
- /**
1119
- * Emit a secret access audit event
1120
- */
1121
- emitAccess(operation, key, provider, success, cached, error) {
1122
- if (!this.auditLog)
1123
- return;
1124
- const event = {
1125
- operation,
1126
- key,
1127
- provider,
1128
- success,
1129
- cached,
1130
- timestamp: new Date().toISOString(),
1131
- ...(error ? { error } : {}),
1132
- };
1133
- this.emit("secretAccess", event);
1134
- }
1135
- }
1136
- // ---------------------------------------------------------------------------
1137
- // Helpers
1138
- // ---------------------------------------------------------------------------
1139
- /**
1140
- * Safely extract an error message from an unknown thrown value
1141
- */
1142
- function errorMessage(err) {
1143
- if (err instanceof Error)
1144
- return err.message;
1145
- return String(err);
1146
- }
1147
- //# sourceMappingURL=SecretManager.js.map