@absolutejs/secrets 0.6.1 → 0.7.1

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.
package/dist/broker.js ADDED
@@ -0,0 +1,825 @@
1
+ // @bun
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+ var __require = import.meta.require;
17
+
18
+ // node_modules/@absolutejs/telemetry/dist/index.js
19
+ var NOOP_SPAN_CONTEXT = {
20
+ spanId: "0000000000000000",
21
+ traceFlags: 0,
22
+ traceId: "00000000000000000000000000000000"
23
+ };
24
+ var noopSpan = {
25
+ addEvent: () => noopSpan,
26
+ end: () => {},
27
+ isRecording: () => false,
28
+ recordException: () => {},
29
+ setAttribute: () => noopSpan,
30
+ setAttributes: () => noopSpan,
31
+ setStatus: () => noopSpan,
32
+ spanContext: () => NOOP_SPAN_CONTEXT,
33
+ updateName: () => noopSpan
34
+ };
35
+ var startActiveSpanNoop = (_name, optionsOrFn, maybeFn) => {
36
+ const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn;
37
+ return fn(noopSpan);
38
+ };
39
+ var noopTracer = {
40
+ startActiveSpan: startActiveSpanNoop,
41
+ startSpan: () => noopSpan
42
+ };
43
+ var tracerOrNoop = (provider, name, version) => provider !== undefined ? provider.getTracer(name, version) : noopTracer;
44
+ var ABS_ATTRS = {
45
+ tenant: "abs.tenant",
46
+ shardId: "abs.shard.id",
47
+ engineId: "abs.engine.id",
48
+ collection: "abs.collection",
49
+ mutation: "abs.mutation",
50
+ mutationAttempt: "abs.mutation.attempt",
51
+ subscriptionId: "abs.subscription.id",
52
+ batchSize: "abs.batch.size",
53
+ clusterMessageOrigin: "abs.cluster.origin",
54
+ jobId: "abs.job.id",
55
+ jobKind: "abs.job.kind",
56
+ jobAttempt: "abs.job.attempt",
57
+ jobMaxAttempts: "abs.job.max_attempts",
58
+ workerId: "abs.worker.id",
59
+ runtimeKey: "abs.runtime.key",
60
+ runtimePid: "abs.runtime.pid",
61
+ runtimePort: "abs.runtime.port",
62
+ runtimeExitReason: "abs.runtime.exit_reason",
63
+ runtimeReadinessMs: "abs.runtime.readiness_ms",
64
+ routeShard: "abs.route.shard",
65
+ routeDecision: "abs.route.decision",
66
+ secretName: "abs.secret.name",
67
+ secretFingerprint: "abs.secret.fingerprint",
68
+ auditKind: "abs.audit.kind"
69
+ };
70
+
71
+ // src/broker.ts
72
+ class BrokerDrainedError extends Error {
73
+ constructor() {
74
+ super("[secrets] Broker is draining \u2014 resolve/rotate refused. " + "Use the broker before the shutdown handler fires.");
75
+ this.name = "BrokerDrainedError";
76
+ }
77
+ }
78
+ var HEX = "0123456789abcdef";
79
+ var sha256Hex = (input) => {
80
+ return sha256(input);
81
+ };
82
+ var ROUND_CONSTANTS = new Uint32Array([
83
+ 1116352408,
84
+ 1899447441,
85
+ 3049323471,
86
+ 3921009573,
87
+ 961987163,
88
+ 1508970993,
89
+ 2453635748,
90
+ 2870763221,
91
+ 3624381080,
92
+ 310598401,
93
+ 607225278,
94
+ 1426881987,
95
+ 1925078388,
96
+ 2162078206,
97
+ 2614888103,
98
+ 3248222580,
99
+ 3835390401,
100
+ 4022224774,
101
+ 264347078,
102
+ 604807628,
103
+ 770255983,
104
+ 1249150122,
105
+ 1555081692,
106
+ 1996064986,
107
+ 2554220882,
108
+ 2821834349,
109
+ 2952996808,
110
+ 3210313671,
111
+ 3336571891,
112
+ 3584528711,
113
+ 113926993,
114
+ 338241895,
115
+ 666307205,
116
+ 773529912,
117
+ 1294757372,
118
+ 1396182291,
119
+ 1695183700,
120
+ 1986661051,
121
+ 2177026350,
122
+ 2456956037,
123
+ 2730485921,
124
+ 2820302411,
125
+ 3259730800,
126
+ 3345764771,
127
+ 3516065817,
128
+ 3600352804,
129
+ 4094571909,
130
+ 275423344,
131
+ 430227734,
132
+ 506948616,
133
+ 659060556,
134
+ 883997877,
135
+ 958139571,
136
+ 1322822218,
137
+ 1537002063,
138
+ 1747873779,
139
+ 1955562222,
140
+ 2024104815,
141
+ 2227730452,
142
+ 2361852424,
143
+ 2428436474,
144
+ 2756734187,
145
+ 3204031479,
146
+ 3329325298
147
+ ]);
148
+ var sha256 = (input) => {
149
+ const bytes = new TextEncoder().encode(input);
150
+ const bitLength = bytes.length * 8;
151
+ const padLength = (bytes.length + 9 + 63 & ~63) - bytes.length;
152
+ const padded = new Uint8Array(bytes.length + padLength);
153
+ padded.set(bytes);
154
+ padded[bytes.length] = 128;
155
+ const view = new DataView(padded.buffer);
156
+ view.setUint32(padded.length - 4, bitLength >>> 0);
157
+ view.setUint32(padded.length - 8, Math.floor(bitLength / 4294967296));
158
+ let h0 = 1779033703, h1 = 3144134277, h2 = 1013904242, h3 = 2773480762;
159
+ let h4 = 1359893119, h5 = 2600822924, h6 = 528734635, h7 = 1541459225;
160
+ const w = new Uint32Array(64);
161
+ for (let i = 0;i < padded.length; i += 64) {
162
+ for (let t = 0;t < 16; t++) {
163
+ w[t] = view.getUint32(i + t * 4);
164
+ }
165
+ for (let t = 16;t < 64; t++) {
166
+ const w15 = w[t - 15];
167
+ const w2 = w[t - 2];
168
+ const s0 = (w15 >>> 7 | w15 << 25) ^ (w15 >>> 18 | w15 << 14) ^ w15 >>> 3;
169
+ const s1 = (w2 >>> 17 | w2 << 15) ^ (w2 >>> 19 | w2 << 13) ^ w2 >>> 10;
170
+ w[t] = w[t - 16] + s0 + w[t - 7] + s1 >>> 0;
171
+ }
172
+ let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7;
173
+ for (let t = 0;t < 64; t++) {
174
+ const S1 = (e >>> 6 | e << 26) ^ (e >>> 11 | e << 21) ^ (e >>> 25 | e << 7);
175
+ const ch = e & f ^ ~e & g;
176
+ const T1 = h + S1 + ch + ROUND_CONSTANTS[t] + w[t] >>> 0;
177
+ const S0 = (a >>> 2 | a << 30) ^ (a >>> 13 | a << 19) ^ (a >>> 22 | a << 10);
178
+ const maj = a & b ^ a & c ^ b & c;
179
+ const T2 = S0 + maj >>> 0;
180
+ h = g;
181
+ g = f;
182
+ f = e;
183
+ e = d + T1 >>> 0;
184
+ d = c;
185
+ c = b;
186
+ b = a;
187
+ a = T1 + T2 >>> 0;
188
+ }
189
+ h0 = h0 + a >>> 0;
190
+ h1 = h1 + b >>> 0;
191
+ h2 = h2 + c >>> 0;
192
+ h3 = h3 + d >>> 0;
193
+ h4 = h4 + e >>> 0;
194
+ h5 = h5 + f >>> 0;
195
+ h6 = h6 + g >>> 0;
196
+ h7 = h7 + h >>> 0;
197
+ }
198
+ const toHex = (n) => {
199
+ let result = "";
200
+ for (let i = 7;i >= 0; i--) {
201
+ result += HEX[n >>> i * 4 & 15];
202
+ }
203
+ return result;
204
+ };
205
+ return toHex(h0) + toHex(h1) + toHex(h2) + toHex(h3) + toHex(h4) + toHex(h5) + toHex(h6) + toHex(h7);
206
+ };
207
+ var fingerprintOf = (value) => sha256Hex(value).slice(0, 8);
208
+ var randomBase36 = (length) => {
209
+ let out = "";
210
+ while (out.length < length) {
211
+ out += Math.random().toString(36).slice(2);
212
+ }
213
+ return out.slice(0, length);
214
+ };
215
+ var inMemoryAdapter = (options = {}) => {
216
+ const store = new Map;
217
+ for (const [k, v] of Object.entries(options.initial ?? {}))
218
+ store.set(k, v);
219
+ const rotate = options.rotate ?? (() => randomBase36(32));
220
+ return {
221
+ fetch: async (name) => store.get(name) ?? null,
222
+ list: async () => Array.from(store.keys()),
223
+ put: async (name, value) => {
224
+ store.set(name, value);
225
+ },
226
+ remove: async (name) => {
227
+ store.delete(name);
228
+ },
229
+ rotate: async (name) => {
230
+ const next = rotate(name, store.get(name) ?? null);
231
+ store.set(name, next);
232
+ return next;
233
+ }
234
+ };
235
+ };
236
+ var envAdapter = (options = {}) => {
237
+ const prefix = options.prefix ?? "";
238
+ const env = options.env ?? globalThis.process?.env ?? {};
239
+ return {
240
+ fetch: async (name) => {
241
+ const key = `${prefix}${name}`;
242
+ const value = env[key];
243
+ return value === undefined ? null : value;
244
+ },
245
+ list: async () => {
246
+ if (!prefix)
247
+ return Object.keys(env);
248
+ const matches = [];
249
+ for (const key of Object.keys(env)) {
250
+ if (key.startsWith(prefix))
251
+ matches.push(key.slice(prefix.length));
252
+ }
253
+ return matches;
254
+ }
255
+ };
256
+ };
257
+ var compositeAdapter = (adapters) => {
258
+ const firstWith = (method) => adapters.find((adapter) => adapter[method] !== undefined);
259
+ return {
260
+ fetch: async (name) => {
261
+ for (const adapter of adapters) {
262
+ const value = await adapter.fetch(name);
263
+ if (value !== null)
264
+ return value;
265
+ }
266
+ return null;
267
+ },
268
+ list: async () => {
269
+ const seen = new Set;
270
+ for (const adapter of adapters) {
271
+ if (!adapter.list)
272
+ continue;
273
+ for (const name of await adapter.list())
274
+ seen.add(name);
275
+ }
276
+ return Array.from(seen);
277
+ },
278
+ put: async (name, value) => {
279
+ const target = firstWith("put");
280
+ if (!target?.put)
281
+ throw new Error("No adapter in the composite supports put()");
282
+ await target.put(name, value);
283
+ },
284
+ remove: async (name) => {
285
+ const target = firstWith("remove");
286
+ if (!target?.remove)
287
+ throw new Error("No adapter in the composite supports remove()");
288
+ await target.remove(name);
289
+ },
290
+ rotate: async (name) => {
291
+ const target = firstWith("rotate");
292
+ if (!target?.rotate)
293
+ throw new Error("No adapter in the composite supports rotate()");
294
+ return target.rotate(name);
295
+ }
296
+ };
297
+ };
298
+ var DEFAULT_PBKDF2_ITERATIONS = 600000;
299
+ var ENC_FILE_VERSION = 1;
300
+ var SALT_BYTES = 16;
301
+ var IV_BYTES = 12;
302
+ var KEY_BYTES = 32;
303
+ var bytesToBase64 = (bytes) => {
304
+ let bin = "";
305
+ for (const byte of bytes)
306
+ bin += String.fromCharCode(byte);
307
+ return btoa(bin);
308
+ };
309
+ var base64ToBytes = (b64) => {
310
+ const bin = atob(b64);
311
+ const out = new Uint8Array(bin.length);
312
+ for (let i = 0;i < bin.length; i += 1)
313
+ out[i] = bin.charCodeAt(i);
314
+ return out;
315
+ };
316
+ var defaultIo = () => ({
317
+ readFile: async (path) => {
318
+ try {
319
+ const text = await (await import("fs/promises")).readFile(path, "utf8");
320
+ return text;
321
+ } catch (error) {
322
+ if (error.code === "ENOENT")
323
+ return;
324
+ throw error;
325
+ }
326
+ },
327
+ writeFileAtomic: async (path, contents) => {
328
+ const fs = await import("fs/promises");
329
+ const tempPath = `${path}.tmp.${process.pid}`;
330
+ await fs.writeFile(tempPath, contents, { mode: 384 });
331
+ await fs.rename(tempPath, path);
332
+ }
333
+ });
334
+ var deriveKeyFromPassphrase = async (passphrase, salt, iterations) => {
335
+ const base = await crypto.subtle.importKey("raw", new TextEncoder().encode(passphrase), "PBKDF2", false, ["deriveKey"]);
336
+ return crypto.subtle.deriveKey({
337
+ hash: "SHA-256",
338
+ iterations,
339
+ name: "PBKDF2",
340
+ salt
341
+ }, base, { length: 256, name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
342
+ };
343
+ var importRawKey = async (bytes) => {
344
+ if (bytes.length !== KEY_BYTES) {
345
+ throw new Error(`[secrets/encrypted-file] raw key must be ${KEY_BYTES} bytes (got ${bytes.length})`);
346
+ }
347
+ return crypto.subtle.importKey("raw", bytes, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
348
+ };
349
+ var encryptedFileAdapter = (options) => {
350
+ const io = options.io ?? defaultIo();
351
+ const iterations = options.pbkdf2Iterations ?? DEFAULT_PBKDF2_ITERATIONS;
352
+ const rotate = options.rotate ?? (() => randomBase36(32));
353
+ let cache;
354
+ let derivedKey;
355
+ let mutationQueue = Promise.resolve();
356
+ let salt;
357
+ const mutate = (operation) => {
358
+ const result = mutationQueue.then(operation, operation);
359
+ mutationQueue = result.then(() => {
360
+ return;
361
+ }, () => {
362
+ return;
363
+ });
364
+ return result;
365
+ };
366
+ const ensureKey = async () => {
367
+ if (derivedKey !== undefined)
368
+ return derivedKey;
369
+ if (options.key.type === "raw") {
370
+ derivedKey = await importRawKey(options.key.bytes);
371
+ return derivedKey;
372
+ }
373
+ if (salt === undefined) {
374
+ salt = crypto.getRandomValues(new Uint8Array(SALT_BYTES));
375
+ }
376
+ derivedKey = await deriveKeyFromPassphrase(options.key.passphrase, salt, iterations);
377
+ return derivedKey;
378
+ };
379
+ const load = async () => {
380
+ if (cache !== undefined)
381
+ return cache;
382
+ const fileText = await io.readFile(options.path);
383
+ if (fileText === undefined) {
384
+ cache = new Map;
385
+ return cache;
386
+ }
387
+ let parsed;
388
+ try {
389
+ parsed = JSON.parse(fileText);
390
+ } catch (error) {
391
+ throw new Error(`[secrets/encrypted-file] could not parse ${options.path}: ${error.message}`);
392
+ }
393
+ if (parsed.version !== ENC_FILE_VERSION) {
394
+ throw new Error(`[secrets/encrypted-file] unsupported file version ${parsed.version} in ${options.path}`);
395
+ }
396
+ if (parsed.kdf !== undefined) {
397
+ if (options.key.type !== "passphrase") {
398
+ throw new Error(`[secrets/encrypted-file] file was written with a passphrase but raw key was supplied`);
399
+ }
400
+ salt = base64ToBytes(parsed.kdf.salt);
401
+ } else if (options.key.type === "passphrase") {
402
+ throw new Error(`[secrets/encrypted-file] file was written with a raw key but passphrase was supplied`);
403
+ }
404
+ const key = await ensureKey();
405
+ const decoded = new Map;
406
+ for (const [name, entry] of Object.entries(parsed.values)) {
407
+ try {
408
+ const iv = base64ToBytes(entry.iv);
409
+ const ct = base64ToBytes(entry.ct);
410
+ const pt = await crypto.subtle.decrypt({ iv, name: "AES-GCM" }, key, ct);
411
+ decoded.set(name, new TextDecoder().decode(pt));
412
+ } catch {
413
+ throw new Error(`[secrets/encrypted-file] failed to decrypt "${name}" in ${options.path} \u2014 wrong master key or corrupted file`);
414
+ }
415
+ }
416
+ cache = decoded;
417
+ return cache;
418
+ };
419
+ const save = async () => {
420
+ const data = cache ?? new Map;
421
+ const key = await ensureKey();
422
+ const values = {};
423
+ for (const [name, value] of data) {
424
+ const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
425
+ const ct = await crypto.subtle.encrypt({ iv, name: "AES-GCM" }, key, new TextEncoder().encode(value));
426
+ values[name] = {
427
+ ct: bytesToBase64(new Uint8Array(ct)),
428
+ iv: bytesToBase64(iv)
429
+ };
430
+ }
431
+ const file = {
432
+ values,
433
+ version: ENC_FILE_VERSION,
434
+ ...options.key.type === "passphrase" && salt !== undefined ? {
435
+ kdf: {
436
+ iterations,
437
+ salt: bytesToBase64(salt),
438
+ type: "pbkdf2-sha256"
439
+ }
440
+ } : {}
441
+ };
442
+ await io.writeFileAtomic(options.path, JSON.stringify(file, null, 2));
443
+ };
444
+ return {
445
+ fetch: async (name) => {
446
+ const data = await load();
447
+ return data.get(name) ?? null;
448
+ },
449
+ list: async () => {
450
+ const data = await load();
451
+ return Array.from(data.keys());
452
+ },
453
+ put: (name, value) => mutate(async () => {
454
+ const data = await load();
455
+ data.set(name, value);
456
+ await save();
457
+ }),
458
+ remove: (name) => mutate(async () => {
459
+ const data = await load();
460
+ data.delete(name);
461
+ await save();
462
+ }),
463
+ rotate: (name) => mutate(async () => {
464
+ const data = await load();
465
+ const previous = data.get(name) ?? null;
466
+ const next = rotate(name, previous);
467
+ data.set(name, next);
468
+ await save();
469
+ return next;
470
+ })
471
+ };
472
+ };
473
+ var rotateMasterKey = async (options) => {
474
+ const io = options.io ?? defaultIo();
475
+ const newIterations = options.newPbkdf2Iterations ?? DEFAULT_PBKDF2_ITERATIONS;
476
+ const fileText = await io.readFile(options.path);
477
+ if (fileText === undefined) {
478
+ throw new Error(`[secrets/encrypted-file] file ${options.path} does not exist`);
479
+ }
480
+ let parsed;
481
+ try {
482
+ parsed = JSON.parse(fileText);
483
+ } catch (error) {
484
+ throw new Error(`[secrets/encrypted-file] could not parse ${options.path}: ${error.message}`);
485
+ }
486
+ if (parsed.version !== ENC_FILE_VERSION) {
487
+ throw new Error(`[secrets/encrypted-file] unsupported file version ${parsed.version} in ${options.path}`);
488
+ }
489
+ let oldDerivedKey;
490
+ if (options.oldKey.type === "raw") {
491
+ if (parsed.kdf !== undefined) {
492
+ throw new Error(`[secrets/encrypted-file] file was written with a passphrase but old key was supplied as raw`);
493
+ }
494
+ oldDerivedKey = await importRawKey(options.oldKey.bytes);
495
+ } else {
496
+ if (parsed.kdf === undefined) {
497
+ throw new Error(`[secrets/encrypted-file] file was written with a raw key but old key was supplied as passphrase`);
498
+ }
499
+ const oldSalt = base64ToBytes(parsed.kdf.salt);
500
+ oldDerivedKey = await deriveKeyFromPassphrase(options.oldKey.passphrase, oldSalt, parsed.kdf.iterations);
501
+ }
502
+ const decrypted = new Map;
503
+ for (const [name, entry] of Object.entries(parsed.values)) {
504
+ try {
505
+ const iv = base64ToBytes(entry.iv);
506
+ const ct = base64ToBytes(entry.ct);
507
+ const pt = await crypto.subtle.decrypt({ iv, name: "AES-GCM" }, oldDerivedKey, ct);
508
+ decrypted.set(name, new TextDecoder().decode(pt));
509
+ } catch {
510
+ throw new Error(`[secrets/encrypted-file] failed to decrypt "${name}" with old master key \u2014 wrong key or corrupted file`);
511
+ }
512
+ }
513
+ let newDerivedKey;
514
+ let newSalt;
515
+ if (options.newKey.type === "raw") {
516
+ newDerivedKey = await importRawKey(options.newKey.bytes);
517
+ } else {
518
+ newSalt = crypto.getRandomValues(new Uint8Array(SALT_BYTES));
519
+ newDerivedKey = await deriveKeyFromPassphrase(options.newKey.passphrase, newSalt, newIterations);
520
+ }
521
+ const newValues = {};
522
+ for (const [name, value] of decrypted) {
523
+ const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
524
+ const ct = await crypto.subtle.encrypt({ iv, name: "AES-GCM" }, newDerivedKey, new TextEncoder().encode(value));
525
+ newValues[name] = {
526
+ ct: bytesToBase64(new Uint8Array(ct)),
527
+ iv: bytesToBase64(iv)
528
+ };
529
+ }
530
+ const newFile = {
531
+ values: newValues,
532
+ version: ENC_FILE_VERSION,
533
+ ...options.newKey.type === "passphrase" && newSalt !== undefined ? {
534
+ kdf: {
535
+ iterations: newIterations,
536
+ salt: bytesToBase64(newSalt),
537
+ type: "pbkdf2-sha256"
538
+ }
539
+ } : {}
540
+ };
541
+ await io.writeFileAtomic(options.path, JSON.stringify(newFile, null, 2));
542
+ };
543
+ var createSecretBroker = (options) => {
544
+ const clock = options.clock ?? Date.now;
545
+ const defaultTtl = options.cacheTtlMs ?? 60000;
546
+ const ttlOverrides = options.cacheTtlOverrides ?? {};
547
+ const minLen = options.redactionMinLength ?? 8;
548
+ const encodings = options.redactionEncodings ?? ["plain"];
549
+ const audit = options.audit;
550
+ const cache = new Map;
551
+ const rotationListeners = new Map;
552
+ let disposed = false;
553
+ let draining = false;
554
+ const tracer = tracerOrNoop(options.tracerProvider, "@absolutejs/secrets");
555
+ const counters = {
556
+ invalidations: 0,
557
+ redactCalls: 0,
558
+ redactionsApplied: 0,
559
+ redactionsBase64: 0,
560
+ resolveErrors: 0,
561
+ resolveHits: 0,
562
+ resolveMisses: 0,
563
+ resolves: 0,
564
+ rotateErrors: 0,
565
+ rotates: 0
566
+ };
567
+ const ttlFor = (name) => ttlOverrides[name] ?? defaultTtl;
568
+ const fireRotation = (name, value, fingerprint, at) => {
569
+ const set = rotationListeners.get(name);
570
+ if (!set || set.size === 0)
571
+ return;
572
+ for (const listener of set) {
573
+ try {
574
+ const ret = listener({ at, fingerprint, name, value });
575
+ if (ret && typeof ret.then === "function") {
576
+ ret.catch((error) => {
577
+ console.error("[secrets] rotation listener rejected:", error);
578
+ });
579
+ }
580
+ } catch (error) {
581
+ console.error("[secrets] rotation listener threw:", error);
582
+ }
583
+ }
584
+ };
585
+ const fireAudit = (event) => {
586
+ if (!audit)
587
+ return;
588
+ try {
589
+ const result = audit(event);
590
+ if (result && typeof result.then === "function") {
591
+ result.catch((error) => {
592
+ console.error("[secrets] audit hook rejected:", error);
593
+ });
594
+ }
595
+ } catch (error) {
596
+ console.error("[secrets] audit hook threw:", error);
597
+ }
598
+ };
599
+ const cacheEntry = (name, value, now) => {
600
+ const entry = {
601
+ fingerprint: fingerprintOf(value),
602
+ storedAt: now,
603
+ value
604
+ };
605
+ cache.set(name, entry);
606
+ return entry;
607
+ };
608
+ const resolve = async (name) => {
609
+ if (disposed)
610
+ return null;
611
+ if (draining)
612
+ throw new BrokerDrainedError;
613
+ const span = tracer.startSpan("secrets.resolve", {
614
+ attributes: { [ABS_ATTRS.secretName]: name }
615
+ });
616
+ counters.resolves += 1;
617
+ const now = clock();
618
+ try {
619
+ const cached = cache.get(name);
620
+ if (cached && now - cached.storedAt < ttlFor(name)) {
621
+ counters.resolveHits += 1;
622
+ span.setAttribute(ABS_ATTRS.secretFingerprint, cached.fingerprint);
623
+ span.setAttribute("secrets.cache", "hit");
624
+ fireAudit({
625
+ at: now,
626
+ event: "resolve.hit",
627
+ fingerprint: cached.fingerprint,
628
+ name
629
+ });
630
+ span.setStatus({ code: 1 });
631
+ return { fingerprint: cached.fingerprint, value: cached.value };
632
+ }
633
+ counters.resolveMisses += 1;
634
+ span.setAttribute("secrets.cache", "miss");
635
+ const value = await options.adapter.fetch(name);
636
+ if (value === null) {
637
+ fireAudit({ at: now, event: "resolve.miss", name });
638
+ cache.delete(name);
639
+ span.setAttribute("secrets.found", false);
640
+ span.setStatus({ code: 1 });
641
+ return null;
642
+ }
643
+ const entry = cacheEntry(name, value, now);
644
+ span.setAttribute(ABS_ATTRS.secretFingerprint, entry.fingerprint);
645
+ fireAudit({
646
+ at: now,
647
+ event: "resolve.miss",
648
+ fingerprint: entry.fingerprint,
649
+ name
650
+ });
651
+ span.setStatus({ code: 1 });
652
+ return { fingerprint: entry.fingerprint, value: entry.value };
653
+ } catch (error) {
654
+ counters.resolveErrors += 1;
655
+ fireAudit({
656
+ at: now,
657
+ error: error instanceof Error ? error.message : String(error),
658
+ event: "resolve.error",
659
+ name
660
+ });
661
+ span.recordException(error);
662
+ span.setStatus({
663
+ code: 2,
664
+ message: error instanceof Error ? error.message : String(error)
665
+ });
666
+ throw error;
667
+ } finally {
668
+ span.end();
669
+ }
670
+ };
671
+ const rotate = async (name) => {
672
+ if (disposed)
673
+ throw new Error("Broker is disposed");
674
+ if (draining)
675
+ throw new BrokerDrainedError;
676
+ if (!options.adapter.rotate) {
677
+ throw new Error("Adapter does not support rotate()");
678
+ }
679
+ const span = tracer.startSpan("secrets.rotate", {
680
+ attributes: { [ABS_ATTRS.secretName]: name }
681
+ });
682
+ try {
683
+ const next = await options.adapter.rotate(name);
684
+ const now = clock();
685
+ const entry = cacheEntry(name, next, now);
686
+ counters.rotates += 1;
687
+ span.setAttribute(ABS_ATTRS.secretFingerprint, entry.fingerprint);
688
+ span.setStatus({ code: 1 });
689
+ fireAudit({
690
+ at: now,
691
+ event: "rotate",
692
+ fingerprint: entry.fingerprint,
693
+ name
694
+ });
695
+ fireRotation(name, entry.value, entry.fingerprint, now);
696
+ return { fingerprint: entry.fingerprint, value: entry.value };
697
+ } catch (error) {
698
+ counters.rotateErrors += 1;
699
+ span.recordException(error);
700
+ span.setStatus({
701
+ code: 2,
702
+ message: error instanceof Error ? error.message : String(error)
703
+ });
704
+ throw error;
705
+ } finally {
706
+ span.end();
707
+ }
708
+ };
709
+ const invalidate = (name) => {
710
+ if (name === undefined) {
711
+ cache.clear();
712
+ } else {
713
+ cache.delete(name);
714
+ }
715
+ counters.invalidations += 1;
716
+ fireAudit({ at: clock(), event: "invalidate", name: name ?? null });
717
+ };
718
+ const redactionPairs = () => {
719
+ const pairs = [];
720
+ for (const [name, entry] of cache) {
721
+ if (entry.value.length < minLen)
722
+ continue;
723
+ for (const enc of encodings) {
724
+ if (enc === "plain") {
725
+ pairs.push([entry.value, `[REDACTED:${name}]`]);
726
+ } else if (enc === "base64") {
727
+ try {
728
+ const encoded = btoa(entry.value);
729
+ if (encoded.length >= minLen) {
730
+ pairs.push([encoded, `[REDACTED:${name}:b64]`]);
731
+ }
732
+ } catch {}
733
+ }
734
+ }
735
+ }
736
+ pairs.sort((a, b) => b[0].length - a[0].length);
737
+ return pairs;
738
+ };
739
+ const redact = (text) => {
740
+ counters.redactCalls += 1;
741
+ if (text.length === 0 || cache.size === 0)
742
+ return text;
743
+ let out = text;
744
+ for (const [needle, replacement] of redactionPairs()) {
745
+ if (!out.includes(needle))
746
+ continue;
747
+ out = out.split(needle).join(replacement);
748
+ counters.redactionsApplied += 1;
749
+ if (replacement.endsWith(":b64]"))
750
+ counters.redactionsBase64 += 1;
751
+ }
752
+ return out;
753
+ };
754
+ const redactStream = () => {
755
+ let buffer = "";
756
+ const maxLen = () => redactionPairs().reduce((max, [needle]) => Math.max(max, needle.length), 0);
757
+ return new TransformStream({
758
+ transform: (chunk, controller) => {
759
+ buffer += chunk;
760
+ const lookback = maxLen();
761
+ const reduced = redact(buffer);
762
+ if (reduced.length <= lookback) {
763
+ buffer = reduced;
764
+ return;
765
+ }
766
+ const safe = reduced.slice(0, reduced.length - lookback);
767
+ buffer = reduced.slice(reduced.length - lookback);
768
+ if (safe.length > 0)
769
+ controller.enqueue(safe);
770
+ },
771
+ flush: (controller) => {
772
+ if (buffer.length === 0)
773
+ return;
774
+ controller.enqueue(redact(buffer));
775
+ buffer = "";
776
+ }
777
+ });
778
+ };
779
+ const onRotate = (name, listener) => {
780
+ let set = rotationListeners.get(name);
781
+ if (!set) {
782
+ set = new Set;
783
+ rotationListeners.set(name, set);
784
+ }
785
+ set.add(listener);
786
+ return () => {
787
+ const current = rotationListeners.get(name);
788
+ if (!current)
789
+ return;
790
+ current.delete(listener);
791
+ if (current.size === 0)
792
+ rotationListeners.delete(name);
793
+ };
794
+ };
795
+ return {
796
+ dispose: () => {
797
+ disposed = true;
798
+ cache.clear();
799
+ rotationListeners.clear();
800
+ },
801
+ drain: () => {
802
+ draining = true;
803
+ },
804
+ fingerprint: fingerprintOf,
805
+ invalidate,
806
+ metrics: () => ({ ...counters }),
807
+ onRotate,
808
+ redact,
809
+ redactStream,
810
+ resolve,
811
+ rotate
812
+ };
813
+ };
814
+ export {
815
+ rotateMasterKey,
816
+ inMemoryAdapter,
817
+ envAdapter,
818
+ encryptedFileAdapter,
819
+ createSecretBroker,
820
+ compositeAdapter,
821
+ BrokerDrainedError
822
+ };
823
+
824
+ //# debugId=C0F1FD10B1D73AB064756E2164756E21
825
+ //# sourceMappingURL=broker.js.map