@absolutejs/secrets 0.0.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/LICENSE ADDED
@@ -0,0 +1,94 @@
1
+ # Business Source License 1.1
2
+
3
+ **Licensor:** Alex Kahn
4
+
5
+ **Licensed Work:** @absolutejs/secrets (https://github.com/absolutejs/secrets)
6
+
7
+ **Change Date:** May 29, 2030
8
+
9
+ **Change License:** Apache License, Version 2.0
10
+
11
+ ---
12
+
13
+ ## Terms
14
+
15
+ The Licensor hereby grants you the right to copy, modify, create derivative
16
+ works, redistribute, and make non-production use of the Licensed Work. The
17
+ Licensor may make an Additional Use Grant, permitting limited production use.
18
+
19
+ ### Additional Use Grant
20
+
21
+ You may use the Licensed Work in production, provided your use does not include
22
+ any of the following:
23
+
24
+ 1. **Offering a Competing Service.** You may not offer the Licensed Work, or
25
+ any derivative or substantial portion of it, to third parties as a hosted or
26
+ managed service that competes with a hosted secrets-management,
27
+ credential-broker, or runtime-secrets-injection platform (including, but not
28
+ limited to, services like AWS Secrets Manager, HashiCorp Vault Cloud,
29
+ Doppler, 1Password Secrets, Infisical, GCP Secret Manager, Azure Key Vault,
30
+ Akeyless, Cloudflare Workers Secrets, Vercel Environment Variables as a
31
+ product, or any similar hosted offering whose primary value to its users is
32
+ secrets storage, secrets injection at request time, secrets rotation, or
33
+ secrets redaction). This includes any product whose primary value to its
34
+ users is the functionality the Licensed Work provides.
35
+
36
+ 2. **Resale or Redistribution as a Standalone Product.** You may not sell,
37
+ license, or distribute the Licensed Work, or any derivative or fork of it,
38
+ as a standalone commercial product.
39
+
40
+ 3. **Removal of Attribution.** Any derivative work, fork, or redistribution of
41
+ the Licensed Work must prominently credit AbsoluteJS and include a link to
42
+ the original project repository (https://github.com/absolutejs/secrets).
43
+
44
+ For clarity, the following uses are expressly permitted:
45
+
46
+ - Using the Licensed Work to build and operate your own applications, websites,
47
+ internal tools, or SaaS products (whether commercial or non-commercial), so
48
+ long as the Licensed Work itself is not the primary product you are selling.
49
+ - Using the Licensed Work as a dependency in commercial software you build and
50
+ sell, as long as the software is not itself a competing managed service of
51
+ the kind described in clause 1.
52
+ - Providing consulting, development, or professional services to clients using
53
+ the Licensed Work.
54
+ - Forking and modifying the Licensed Work for your own internal use, provided
55
+ attribution is maintained.
56
+
57
+ ### Change Date and Change License
58
+
59
+ On the Change Date specified above, or on such other date as the Licensor may
60
+ specify by written notice, the Licensed Work will be made available under the
61
+ Change License (Apache License, Version 2.0). Until the Change Date, the terms
62
+ of this Business Source License 1.1 apply.
63
+
64
+ ### Trademark
65
+
66
+ This license does not grant you any rights to use the "AbsoluteJS" or
67
+ "@absolutejs" name, logo, or any related trademarks. Forks and derivative works
68
+ must not be named or branded in a manner that suggests endorsement by or
69
+ affiliation with AbsoluteJS or the Licensor.
70
+
71
+ ### Notices
72
+
73
+ You must not remove or obscure any licensing, copyright, or other notices
74
+ included in the Licensed Work.
75
+
76
+ ### No Warranty
77
+
78
+ THE LICENSED WORK IS PROVIDED "AS IS". THE LICENSOR HEREBY DISCLAIMS ALL
79
+ WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF
80
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO
81
+ EVENT SHALL THE LICENSOR BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY,
82
+ WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR
83
+ IN CONNECTION WITH THE LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE
84
+ LICENSED WORK.
85
+
86
+ ---
87
+
88
+ ## Contact
89
+
90
+ For commercial licensing inquiries or additional permissions, contact:
91
+
92
+ - **Alex Kahn**
93
+ - alexkahndev@gmail.com
94
+ - alexkahndev.github.io
package/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # @absolutejs/secrets
2
+
3
+ Host-side secret broker for multi-tenant Bun runtimes. Three jobs, kept narrow:
4
+
5
+ 1. **Resolve** secrets through a pluggable adapter. The broker caches the
6
+ value, returns a `{ value, fingerprint }` pair where the fingerprint is
7
+ a sha256 prefix safe to log, and fires an audit event per call.
8
+ 2. **Redact** known cached secrets out of arbitrary strings (an error
9
+ message, a stdout line, a stack trace) before the text lands in any
10
+ log sink. Replacement: `[REDACTED:NAME]`.
11
+ 3. **Rotate** through the adapter, invalidate the cache, return the new
12
+ value.
13
+
14
+ Pure logic, zero Bun / Elysia surface. The intended consumers inside the
15
+ SB-6 substrate are `@absolutejs/sync`'s `bridgeFetch.authorization()` hook
16
+ (host-side credential injection for sandboxed mutations) and `unsafeHost`
17
+ declarations (per-customer host functions like Stripe charge, Slack ping,
18
+ queue push).
19
+
20
+ ```ts
21
+ import { createSecretBroker, envAdapter, inMemoryAdapter, compositeAdapter } from '@absolutejs/secrets';
22
+
23
+ const broker = createSecretBroker({
24
+ adapter: compositeAdapter([
25
+ inMemoryAdapter({ initial: { TEST_KEY: 'sk_test_local_value' } }),
26
+ envAdapter({ prefix: 'ABS_SECRET_' }),
27
+ ]),
28
+ audit: (event) => observabilitySink.write(event),
29
+ cacheTtlMs: 60_000,
30
+ });
31
+
32
+ // In bridgeFetch.authorization():
33
+ const { value, fingerprint } = (await broker.resolve('STRIPE_KEY'))!;
34
+ logger.info('charging', { tenant, fingerprint }); // safe — no plaintext
35
+ return { 'Authorization': `Bearer ${value}` };
36
+
37
+ // In a log sink, before text leaves the host:
38
+ const sanitized = broker.redact(line); // [REDACTED:STRIPE_KEY] replaces plaintext
39
+ sinkToCustomerVisibleLog(sanitized);
40
+
41
+ // Rotate:
42
+ const next = await broker.rotate('STRIPE_KEY');
43
+ notifyDependents(next.fingerprint); // tell consumers a new key is in cache
44
+ ```
45
+
46
+ ## v0.0.1 surface
47
+
48
+ | API | Purpose |
49
+ |---|---|
50
+ | `createSecretBroker(options)` | Factory. Returns a `SecretBroker`. |
51
+ | `broker.resolve(name)` | Returns `{ value, fingerprint } | null`. Uses cache within `cacheTtlMs`. |
52
+ | `broker.fingerprint(value)` | Pure helper — sha256 prefix of any string. No adapter call. |
53
+ | `broker.redact(text)` | Rewrite arbitrary text, replacing every cached value (longer-first) with `[REDACTED:NAME]`. Skips values shorter than `redactionMinLength`. |
54
+ | `broker.rotate(name)` | Calls `adapter.rotate?`, caches the result, returns it. Throws if the adapter doesn't support rotation. |
55
+ | `broker.invalidate(name?)` | Clear one entry or the whole cache. |
56
+ | `broker.dispose()` | Tear down — clears cache; subsequent resolves return `null`. |
57
+
58
+ ### Bundled adapters
59
+
60
+ | Adapter | Use |
61
+ |---|---|
62
+ | `inMemoryAdapter({ initial?, rotate? })` | Tests, dev, and starter templates. Supports every operation. Default rotate = random base36. |
63
+ | `envAdapter({ prefix?, env? })` | Reads `process.env` (or any injected `env` map). Prefix-scoped to avoid leaking unrelated env vars via `list`. Read-only. |
64
+ | `compositeAdapter([...])` | Fan-out / fallback. `fetch` falls through; writes go to the first writeable adapter. |
65
+
66
+ AWS Secrets Manager / HashiCorp Vault / Doppler / Infisical / GCP Secret
67
+ Manager / Azure Key Vault adapters ship later as siblings — they're the
68
+ ones with real auth surface, so they don't belong in v0.0.1.
69
+
70
+ ### Audit
71
+
72
+ Every `resolve`, `rotate`, and `invalidate` fires the `audit` hook with
73
+ one of:
74
+
75
+ - `{ event: 'resolve.hit', name, fingerprint, at }`
76
+ - `{ event: 'resolve.miss', name, fingerprint?, at }` — fingerprint present when the miss turned into a cache write
77
+ - `{ event: 'resolve.error', name, error, at }` — adapter threw
78
+ - `{ event: 'rotate', name, fingerprint, at }`
79
+ - `{ event: 'invalidate', name, at }` — `name` is `null` for a full clear
80
+
81
+ A throwing or rejecting hook is logged + discarded. The broker is on the
82
+ hot path for every credential lookup — one broken audit sink must not
83
+ take everything down.
84
+
85
+ ### Why fingerprints
86
+
87
+ `broker.fingerprint(value)` is a deterministic sha256 prefix. Two purposes:
88
+
89
+ - **Logs.** Trace records can say "served credentials/abc12345" without
90
+ leaking the secret; if the same fingerprint shows up across two requests
91
+ you know they used the same key.
92
+ - **Webhook verification.** Compare an inbound HMAC hex against
93
+ `fingerprint(expectedSecret)` to confirm rotation propagation without
94
+ ever putting the secret next to the comparison.
95
+
96
+ It is NOT a security construct — 8 hex chars has only 32 bits of entropy.
97
+ Treat it as a tag, not a token.
98
+
99
+ ## What v0.0.1 does NOT include
100
+
101
+ - Vendored AWS / Vault / Doppler / Infisical adapters (siblings, later).
102
+ - Encrypted on-disk cache.
103
+ - Cross-process secret sharing.
104
+ - Streaming key-rotation propagation to downstream sandboxes (caller wires that on top of `rotate`'s return).
105
+
106
+ ## Architectural role
107
+
108
+ - **`@absolutejs/sync`** — `bridgeFetch.authorization()` and `unsafeHost` host functions ask the broker for credentials per call. The plaintext never crosses the sandbox boundary.
109
+ - **`@absolutejs/runtime`** — passes the broker into the per-tenant process via the `env` it injects (or via a side-channel; the broker doesn't care).
110
+ - **`@absolutejs/router`** — no direct relationship; runs upstream of the broker.
111
+ - **`@absolutejs/metering`** — `audit` can sink directly into the metering pipeline so credential lookups are billable observability events.
112
+
113
+ ## License
114
+
115
+ BSL 1.1 with a named carveout for the hosted secrets-management / credential-broker / runtime-secrets-injection category (AWS Secrets Manager, HashiCorp Vault Cloud, Doppler, 1Password Secrets, Infisical, GCP Secret Manager, Azure Key Vault, Akeyless, Cloudflare Workers Secrets, Vercel Environment Variables as a product). See [LICENSE](./LICENSE). Change Date: 4 years from first release; Change License: Apache 2.0.
@@ -0,0 +1,148 @@
1
+ /**
2
+ * @absolutejs/secrets — host-side secret broker for multi-tenant Bun
3
+ * runtimes.
4
+ *
5
+ * Three responsibilities, kept narrow on purpose:
6
+ *
7
+ * 1. **Resolve.** A pluggable adapter fetches a secret by name. The broker
8
+ * caches the answer, hands the caller back a `{ value, fingerprint }`
9
+ * pair (fingerprint is a sha256 prefix safe to put in logs), and fires
10
+ * an audit event for every lookup.
11
+ * 2. **Redact.** Walks every known cached secret out of an arbitrary string
12
+ * before it leaves the host (e.g. an error message that contains the
13
+ * leaked API key the host call just made). The replacement is
14
+ * `[REDACTED:name]`; matches shorter than `redactionMinLength` are
15
+ * skipped to avoid blanking coincidental short tokens.
16
+ * 3. **Rotate.** Delegates to `adapter.rotate?(name)` if the adapter
17
+ * supports it, invalidates the cache entry. Caller is expected to
18
+ * re-distribute to dependent surfaces.
19
+ *
20
+ * v0.0.1 ships three adapters: `inMemoryAdapter`, `envAdapter`,
21
+ * `compositeAdapter`. AWS Secrets Manager / Vault / Doppler / Infisical
22
+ * adapters ship later as siblings.
23
+ *
24
+ * The broker is bun/elysia-agnostic — same posture as router + meter.
25
+ */
26
+ export type SecretValue = {
27
+ /** The plaintext secret. Treat as poison: never log, never serialize. */
28
+ value: string;
29
+ /**
30
+ * Short sha256-derived id (first 8 hex chars). Stable across calls for
31
+ * the same value; safe to print in logs and traces. Useful for "which
32
+ * version of the key did this request use?" diagnostics without leaking.
33
+ */
34
+ fingerprint: string;
35
+ };
36
+ export type SecretAdapter = {
37
+ /** Return the plaintext value for a name, or `null` if not stored here. */
38
+ fetch: (name: string) => Promise<string | null>;
39
+ /** Write a value. Optional — adapters may be read-only. */
40
+ put?: (name: string, value: string) => Promise<void>;
41
+ /** Delete a value. Optional. */
42
+ remove?: (name: string) => Promise<void>;
43
+ /** Rotate a value; return the NEW plaintext. Optional. */
44
+ rotate?: (name: string) => Promise<string>;
45
+ /** Enumerate names. Optional; default omitted to avoid leaking the index. */
46
+ list?: () => Promise<string[]>;
47
+ };
48
+ export type AuditEvent = {
49
+ event: 'resolve.hit';
50
+ name: string;
51
+ fingerprint: string;
52
+ at: number;
53
+ } | {
54
+ event: 'resolve.miss';
55
+ name: string;
56
+ fingerprint?: string;
57
+ at: number;
58
+ } | {
59
+ event: 'resolve.error';
60
+ name: string;
61
+ error: string;
62
+ at: number;
63
+ } | {
64
+ event: 'rotate';
65
+ name: string;
66
+ fingerprint: string;
67
+ at: number;
68
+ } | {
69
+ event: 'invalidate';
70
+ name: string | null;
71
+ at: number;
72
+ };
73
+ export type AuditHook = (event: AuditEvent) => void | Promise<void>;
74
+ export type SecretBrokerOptions = {
75
+ /** The adapter the broker delegates fetch / rotate / put to. */
76
+ adapter: SecretAdapter;
77
+ /** Audit hook fired on every resolve / rotate / invalidate. */
78
+ audit?: AuditHook;
79
+ /**
80
+ * How long a cached secret stays fresh, in ms. After this, the next
81
+ * resolve re-hits the adapter. Default 60_000 (1 minute). Set to
82
+ * `Infinity` to disable TTL — only `rotate` / `invalidate` evict.
83
+ */
84
+ cacheTtlMs?: number;
85
+ /**
86
+ * Minimum length a cached value must have before `redact` will rewrite
87
+ * occurrences of it in arbitrary text. Default 8 — short values risk
88
+ * blanking out coincidental matches (e.g. a short password "abc1"
89
+ * appearing as a substring of unrelated text).
90
+ */
91
+ redactionMinLength?: number;
92
+ /** Override `Date.now` for tests. */
93
+ clock?: () => number;
94
+ };
95
+ export type SecretBroker = {
96
+ /**
97
+ * Resolve a secret by name. Returns `null` if the adapter reports
98
+ * no value. Caches the answer for `cacheTtlMs`.
99
+ */
100
+ resolve: (name: string) => Promise<SecretValue | null>;
101
+ /**
102
+ * Returns the fingerprint of a value WITHOUT touching the adapter.
103
+ * Useful for hashing a value the caller already has — e.g. a webhook
104
+ * payload — to compare against an audit log.
105
+ */
106
+ fingerprint: (value: string) => string;
107
+ /**
108
+ * Replace every cached secret value found in `text` with
109
+ * `[REDACTED:name]`. Returns the rewritten text. Subjects shorter than
110
+ * `redactionMinLength` are skipped.
111
+ */
112
+ redact: (text: string) => string;
113
+ /**
114
+ * Rotate a secret. Calls `adapter.rotate(name)`, invalidates the cache,
115
+ * returns the new `{ value, fingerprint }`. Throws if the adapter does
116
+ * not support rotation.
117
+ */
118
+ rotate: (name: string) => Promise<SecretValue>;
119
+ /**
120
+ * Invalidate one cache entry, or the whole cache when `name` is omitted.
121
+ */
122
+ invalidate: (name?: string) => void;
123
+ /** Tear down the broker — clears the cache; further resolves still hit the adapter. */
124
+ dispose: () => void;
125
+ };
126
+ export type InMemoryAdapterOptions = {
127
+ initial?: Record<string, string>;
128
+ /** Override the rotation strategy. Default = random 32-char base36 string. */
129
+ rotate?: (name: string, previous: string | null) => string;
130
+ };
131
+ export declare const inMemoryAdapter: (options?: InMemoryAdapterOptions) => SecretAdapter;
132
+ export type EnvAdapterOptions = {
133
+ /**
134
+ * If set, lookups are prefixed before reading from env. e.g.
135
+ * `prefix: 'ABS_SECRET_'` and `resolve('STRIPE_KEY')` reads `ABS_SECRET_STRIPE_KEY`.
136
+ * Default `''` (no prefix).
137
+ */
138
+ prefix?: string;
139
+ /** The env object to read from. Default `process.env`. */
140
+ env?: Record<string, string | undefined>;
141
+ };
142
+ export declare const envAdapter: (options?: EnvAdapterOptions) => SecretAdapter;
143
+ /**
144
+ * Compose adapters: `fetch` falls through to the first non-null result;
145
+ * `put` / `rotate` / `remove` go to the first adapter that implements them.
146
+ */
147
+ export declare const compositeAdapter: (adapters: SecretAdapter[]) => SecretAdapter;
148
+ export declare const createSecretBroker: (options: SecretBrokerOptions) => SecretBroker;
package/dist/index.js ADDED
@@ -0,0 +1,334 @@
1
+ // @bun
2
+ // src/index.ts
3
+ var HEX = "0123456789abcdef";
4
+ var sha256Hex = (input) => {
5
+ return sha256(input);
6
+ };
7
+ var ROUND_CONSTANTS = new Uint32Array([
8
+ 1116352408,
9
+ 1899447441,
10
+ 3049323471,
11
+ 3921009573,
12
+ 961987163,
13
+ 1508970993,
14
+ 2453635748,
15
+ 2870763221,
16
+ 3624381080,
17
+ 310598401,
18
+ 607225278,
19
+ 1426881987,
20
+ 1925078388,
21
+ 2162078206,
22
+ 2614888103,
23
+ 3248222580,
24
+ 3835390401,
25
+ 4022224774,
26
+ 264347078,
27
+ 604807628,
28
+ 770255983,
29
+ 1249150122,
30
+ 1555081692,
31
+ 1996064986,
32
+ 2554220882,
33
+ 2821834349,
34
+ 2952996808,
35
+ 3210313671,
36
+ 3336571891,
37
+ 3584528711,
38
+ 113926993,
39
+ 338241895,
40
+ 666307205,
41
+ 773529912,
42
+ 1294757372,
43
+ 1396182291,
44
+ 1695183700,
45
+ 1986661051,
46
+ 2177026350,
47
+ 2456956037,
48
+ 2730485921,
49
+ 2820302411,
50
+ 3259730800,
51
+ 3345764771,
52
+ 3516065817,
53
+ 3600352804,
54
+ 4094571909,
55
+ 275423344,
56
+ 430227734,
57
+ 506948616,
58
+ 659060556,
59
+ 883997877,
60
+ 958139571,
61
+ 1322822218,
62
+ 1537002063,
63
+ 1747873779,
64
+ 1955562222,
65
+ 2024104815,
66
+ 2227730452,
67
+ 2361852424,
68
+ 2428436474,
69
+ 2756734187,
70
+ 3204031479,
71
+ 3329325298
72
+ ]);
73
+ var sha256 = (input) => {
74
+ const bytes = new TextEncoder().encode(input);
75
+ const bitLength = bytes.length * 8;
76
+ const padLength = (bytes.length + 9 + 63 & ~63) - bytes.length;
77
+ const padded = new Uint8Array(bytes.length + padLength);
78
+ padded.set(bytes);
79
+ padded[bytes.length] = 128;
80
+ const view = new DataView(padded.buffer);
81
+ view.setUint32(padded.length - 4, bitLength >>> 0);
82
+ view.setUint32(padded.length - 8, Math.floor(bitLength / 4294967296));
83
+ let h0 = 1779033703, h1 = 3144134277, h2 = 1013904242, h3 = 2773480762;
84
+ let h4 = 1359893119, h5 = 2600822924, h6 = 528734635, h7 = 1541459225;
85
+ const w = new Uint32Array(64);
86
+ for (let i = 0;i < padded.length; i += 64) {
87
+ for (let t = 0;t < 16; t++) {
88
+ w[t] = view.getUint32(i + t * 4);
89
+ }
90
+ for (let t = 16;t < 64; t++) {
91
+ const w15 = w[t - 15];
92
+ const w2 = w[t - 2];
93
+ const s0 = (w15 >>> 7 | w15 << 25) ^ (w15 >>> 18 | w15 << 14) ^ w15 >>> 3;
94
+ const s1 = (w2 >>> 17 | w2 << 15) ^ (w2 >>> 19 | w2 << 13) ^ w2 >>> 10;
95
+ w[t] = w[t - 16] + s0 + w[t - 7] + s1 >>> 0;
96
+ }
97
+ let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7;
98
+ for (let t = 0;t < 64; t++) {
99
+ const S1 = (e >>> 6 | e << 26) ^ (e >>> 11 | e << 21) ^ (e >>> 25 | e << 7);
100
+ const ch = e & f ^ ~e & g;
101
+ const T1 = h + S1 + ch + ROUND_CONSTANTS[t] + w[t] >>> 0;
102
+ const S0 = (a >>> 2 | a << 30) ^ (a >>> 13 | a << 19) ^ (a >>> 22 | a << 10);
103
+ const maj = a & b ^ a & c ^ b & c;
104
+ const T2 = S0 + maj >>> 0;
105
+ h = g;
106
+ g = f;
107
+ f = e;
108
+ e = d + T1 >>> 0;
109
+ d = c;
110
+ c = b;
111
+ b = a;
112
+ a = T1 + T2 >>> 0;
113
+ }
114
+ h0 = h0 + a >>> 0;
115
+ h1 = h1 + b >>> 0;
116
+ h2 = h2 + c >>> 0;
117
+ h3 = h3 + d >>> 0;
118
+ h4 = h4 + e >>> 0;
119
+ h5 = h5 + f >>> 0;
120
+ h6 = h6 + g >>> 0;
121
+ h7 = h7 + h >>> 0;
122
+ }
123
+ const toHex = (n) => {
124
+ let result = "";
125
+ for (let i = 7;i >= 0; i--) {
126
+ result += HEX[n >>> i * 4 & 15];
127
+ }
128
+ return result;
129
+ };
130
+ return toHex(h0) + toHex(h1) + toHex(h2) + toHex(h3) + toHex(h4) + toHex(h5) + toHex(h6) + toHex(h7);
131
+ };
132
+ var fingerprintOf = (value) => sha256Hex(value).slice(0, 8);
133
+ var randomBase36 = (length) => {
134
+ let out = "";
135
+ while (out.length < length) {
136
+ out += Math.random().toString(36).slice(2);
137
+ }
138
+ return out.slice(0, length);
139
+ };
140
+ var inMemoryAdapter = (options = {}) => {
141
+ const store = new Map;
142
+ for (const [k, v] of Object.entries(options.initial ?? {}))
143
+ store.set(k, v);
144
+ const rotate = options.rotate ?? (() => randomBase36(32));
145
+ return {
146
+ fetch: async (name) => store.get(name) ?? null,
147
+ list: async () => Array.from(store.keys()),
148
+ put: async (name, value) => {
149
+ store.set(name, value);
150
+ },
151
+ remove: async (name) => {
152
+ store.delete(name);
153
+ },
154
+ rotate: async (name) => {
155
+ const next = rotate(name, store.get(name) ?? null);
156
+ store.set(name, next);
157
+ return next;
158
+ }
159
+ };
160
+ };
161
+ var envAdapter = (options = {}) => {
162
+ const prefix = options.prefix ?? "";
163
+ const env = options.env ?? globalThis.process?.env ?? {};
164
+ return {
165
+ fetch: async (name) => {
166
+ const key = `${prefix}${name}`;
167
+ const value = env[key];
168
+ return value === undefined ? null : value;
169
+ },
170
+ list: async () => {
171
+ if (!prefix)
172
+ return Object.keys(env);
173
+ const matches = [];
174
+ for (const key of Object.keys(env)) {
175
+ if (key.startsWith(prefix))
176
+ matches.push(key.slice(prefix.length));
177
+ }
178
+ return matches;
179
+ }
180
+ };
181
+ };
182
+ var compositeAdapter = (adapters) => {
183
+ const firstWith = (method) => adapters.find((adapter) => adapter[method] !== undefined);
184
+ return {
185
+ fetch: async (name) => {
186
+ for (const adapter of adapters) {
187
+ const value = await adapter.fetch(name);
188
+ if (value !== null)
189
+ return value;
190
+ }
191
+ return null;
192
+ },
193
+ list: async () => {
194
+ const seen = new Set;
195
+ for (const adapter of adapters) {
196
+ if (!adapter.list)
197
+ continue;
198
+ for (const name of await adapter.list())
199
+ seen.add(name);
200
+ }
201
+ return Array.from(seen);
202
+ },
203
+ put: async (name, value) => {
204
+ const target = firstWith("put");
205
+ if (!target?.put)
206
+ throw new Error("No adapter in the composite supports put()");
207
+ await target.put(name, value);
208
+ },
209
+ remove: async (name) => {
210
+ const target = firstWith("remove");
211
+ if (!target?.remove)
212
+ throw new Error("No adapter in the composite supports remove()");
213
+ await target.remove(name);
214
+ },
215
+ rotate: async (name) => {
216
+ const target = firstWith("rotate");
217
+ if (!target?.rotate)
218
+ throw new Error("No adapter in the composite supports rotate()");
219
+ return target.rotate(name);
220
+ }
221
+ };
222
+ };
223
+ var createSecretBroker = (options) => {
224
+ const clock = options.clock ?? Date.now;
225
+ const ttl = options.cacheTtlMs ?? 60000;
226
+ const minLen = options.redactionMinLength ?? 8;
227
+ const audit = options.audit;
228
+ const cache = new Map;
229
+ let disposed = false;
230
+ const fireAudit = (event) => {
231
+ if (!audit)
232
+ return;
233
+ try {
234
+ const result = audit(event);
235
+ if (result && typeof result.then === "function") {
236
+ result.catch((error) => {
237
+ console.error("[secrets] audit hook rejected:", error);
238
+ });
239
+ }
240
+ } catch (error) {
241
+ console.error("[secrets] audit hook threw:", error);
242
+ }
243
+ };
244
+ const cacheEntry = (name, value, now) => {
245
+ const entry = {
246
+ fingerprint: fingerprintOf(value),
247
+ storedAt: now,
248
+ value
249
+ };
250
+ cache.set(name, entry);
251
+ return entry;
252
+ };
253
+ const resolve = async (name) => {
254
+ if (disposed)
255
+ return null;
256
+ const now = clock();
257
+ const cached = cache.get(name);
258
+ if (cached && now - cached.storedAt < ttl) {
259
+ fireAudit({ at: now, event: "resolve.hit", fingerprint: cached.fingerprint, name });
260
+ return { fingerprint: cached.fingerprint, value: cached.value };
261
+ }
262
+ try {
263
+ const value = await options.adapter.fetch(name);
264
+ if (value === null) {
265
+ fireAudit({ at: now, event: "resolve.miss", name });
266
+ cache.delete(name);
267
+ return null;
268
+ }
269
+ const entry = cacheEntry(name, value, now);
270
+ fireAudit({ at: now, event: "resolve.miss", fingerprint: entry.fingerprint, name });
271
+ return { fingerprint: entry.fingerprint, value: entry.value };
272
+ } catch (error) {
273
+ fireAudit({
274
+ at: now,
275
+ error: error instanceof Error ? error.message : String(error),
276
+ event: "resolve.error",
277
+ name
278
+ });
279
+ throw error;
280
+ }
281
+ };
282
+ const rotate = async (name) => {
283
+ if (disposed)
284
+ throw new Error("Broker is disposed");
285
+ if (!options.adapter.rotate) {
286
+ throw new Error("Adapter does not support rotate()");
287
+ }
288
+ const next = await options.adapter.rotate(name);
289
+ const now = clock();
290
+ const entry = cacheEntry(name, next, now);
291
+ fireAudit({ at: now, event: "rotate", fingerprint: entry.fingerprint, name });
292
+ return { fingerprint: entry.fingerprint, value: entry.value };
293
+ };
294
+ const invalidate = (name) => {
295
+ if (name === undefined) {
296
+ cache.clear();
297
+ } else {
298
+ cache.delete(name);
299
+ }
300
+ fireAudit({ at: clock(), event: "invalidate", name: name ?? null });
301
+ };
302
+ const redact = (text) => {
303
+ if (text.length === 0 || cache.size === 0)
304
+ return text;
305
+ const ordered = Array.from(cache.entries()).filter(([, entry]) => entry.value.length >= minLen).sort(([, a], [, b]) => b.value.length - a.value.length);
306
+ let out = text;
307
+ for (const [name, entry] of ordered) {
308
+ if (!out.includes(entry.value))
309
+ continue;
310
+ out = out.split(entry.value).join(`[REDACTED:${name}]`);
311
+ }
312
+ return out;
313
+ };
314
+ return {
315
+ dispose: () => {
316
+ disposed = true;
317
+ cache.clear();
318
+ },
319
+ fingerprint: fingerprintOf,
320
+ invalidate,
321
+ redact,
322
+ resolve,
323
+ rotate
324
+ };
325
+ };
326
+ export {
327
+ inMemoryAdapter,
328
+ envAdapter,
329
+ createSecretBroker,
330
+ compositeAdapter
331
+ };
332
+
333
+ //# debugId=EE866E47E57910C764756E2164756E21
334
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * @absolutejs/secrets — host-side secret broker for multi-tenant Bun\n * runtimes.\n *\n * Three responsibilities, kept narrow on purpose:\n *\n * 1. **Resolve.** A pluggable adapter fetches a secret by name. The broker\n * caches the answer, hands the caller back a `{ value, fingerprint }`\n * pair (fingerprint is a sha256 prefix safe to put in logs), and fires\n * an audit event for every lookup.\n * 2. **Redact.** Walks every known cached secret out of an arbitrary string\n * before it leaves the host (e.g. an error message that contains the\n * leaked API key the host call just made). The replacement is\n * `[REDACTED:name]`; matches shorter than `redactionMinLength` are\n * skipped to avoid blanking coincidental short tokens.\n * 3. **Rotate.** Delegates to `adapter.rotate?(name)` if the adapter\n * supports it, invalidates the cache entry. Caller is expected to\n * re-distribute to dependent surfaces.\n *\n * v0.0.1 ships three adapters: `inMemoryAdapter`, `envAdapter`,\n * `compositeAdapter`. AWS Secrets Manager / Vault / Doppler / Infisical\n * adapters ship later as siblings.\n *\n * The broker is bun/elysia-agnostic — same posture as router + meter.\n */\n\nexport type SecretValue = {\n\t/** The plaintext secret. Treat as poison: never log, never serialize. */\n\tvalue: string;\n\t/**\n\t * Short sha256-derived id (first 8 hex chars). Stable across calls for\n\t * the same value; safe to print in logs and traces. Useful for \"which\n\t * version of the key did this request use?\" diagnostics without leaking.\n\t */\n\tfingerprint: string;\n};\n\nexport type SecretAdapter = {\n\t/** Return the plaintext value for a name, or `null` if not stored here. */\n\tfetch: (name: string) => Promise<string | null>;\n\t/** Write a value. Optional — adapters may be read-only. */\n\tput?: (name: string, value: string) => Promise<void>;\n\t/** Delete a value. Optional. */\n\tremove?: (name: string) => Promise<void>;\n\t/** Rotate a value; return the NEW plaintext. Optional. */\n\trotate?: (name: string) => Promise<string>;\n\t/** Enumerate names. Optional; default omitted to avoid leaking the index. */\n\tlist?: () => Promise<string[]>;\n};\n\nexport type AuditEvent =\n\t| { event: 'resolve.hit'; name: string; fingerprint: string; at: number }\n\t| { event: 'resolve.miss'; name: string; fingerprint?: string; at: number }\n\t| { event: 'resolve.error'; name: string; error: string; at: number }\n\t| { event: 'rotate'; name: string; fingerprint: string; at: number }\n\t| { event: 'invalidate'; name: string | null; at: number };\n\nexport type AuditHook = (event: AuditEvent) => void | Promise<void>;\n\nexport type SecretBrokerOptions = {\n\t/** The adapter the broker delegates fetch / rotate / put to. */\n\tadapter: SecretAdapter;\n\t/** Audit hook fired on every resolve / rotate / invalidate. */\n\taudit?: AuditHook;\n\t/**\n\t * How long a cached secret stays fresh, in ms. After this, the next\n\t * resolve re-hits the adapter. Default 60_000 (1 minute). Set to\n\t * `Infinity` to disable TTL — only `rotate` / `invalidate` evict.\n\t */\n\tcacheTtlMs?: number;\n\t/**\n\t * Minimum length a cached value must have before `redact` will rewrite\n\t * occurrences of it in arbitrary text. Default 8 — short values risk\n\t * blanking out coincidental matches (e.g. a short password \"abc1\"\n\t * appearing as a substring of unrelated text).\n\t */\n\tredactionMinLength?: number;\n\t/** Override `Date.now` for tests. */\n\tclock?: () => number;\n};\n\nexport type SecretBroker = {\n\t/**\n\t * Resolve a secret by name. Returns `null` if the adapter reports\n\t * no value. Caches the answer for `cacheTtlMs`.\n\t */\n\tresolve: (name: string) => Promise<SecretValue | null>;\n\t/**\n\t * Returns the fingerprint of a value WITHOUT touching the adapter.\n\t * Useful for hashing a value the caller already has — e.g. a webhook\n\t * payload — to compare against an audit log.\n\t */\n\tfingerprint: (value: string) => string;\n\t/**\n\t * Replace every cached secret value found in `text` with\n\t * `[REDACTED:name]`. Returns the rewritten text. Subjects shorter than\n\t * `redactionMinLength` are skipped.\n\t */\n\tredact: (text: string) => string;\n\t/**\n\t * Rotate a secret. Calls `adapter.rotate(name)`, invalidates the cache,\n\t * returns the new `{ value, fingerprint }`. Throws if the adapter does\n\t * not support rotation.\n\t */\n\trotate: (name: string) => Promise<SecretValue>;\n\t/**\n\t * Invalidate one cache entry, or the whole cache when `name` is omitted.\n\t */\n\tinvalidate: (name?: string) => void;\n\t/** Tear down the broker — clears the cache; further resolves still hit the adapter. */\n\tdispose: () => void;\n};\n\n// -----------------------------------------------------------------------------\n// Fingerprint\n// -----------------------------------------------------------------------------\n\nconst HEX = '0123456789abcdef';\n\nconst sha256Hex = (input: string): string => {\n\t// 2026-era Bun: `crypto.subtle.digest` is the portable path. Synchronous\n\t// path via `Bun.CryptoHasher` is faster but requires Bun globals; we use\n\t// subtle to keep the broker runtime-agnostic at the cost of being async.\n\t// For the `fingerprint(value)` *public* method we want a synchronous\n\t// answer — so we use a tiny in-house sha256. It's slower than the native\n\t// digest but only runs on the secret value (small, ~ < 1KB), once per\n\t// distinct value over the broker's lifetime (memoized in the cache entry).\n\treturn sha256(input);\n};\n\n// Pure JS sha256, NIST FIPS 180-4. Small + dependency-free. Returns lowercase hex.\nconst ROUND_CONSTANTS = new Uint32Array([\n\t0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,\n\t0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,\n\t0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,\n\t0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,\n\t0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,\n\t0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,\n\t0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,\n\t0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,\n\t0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,\n\t0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,\n\t0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,\n]);\n\nconst sha256 = (input: string): string => {\n\tconst bytes = new TextEncoder().encode(input);\n\tconst bitLength = bytes.length * 8;\n\tconst padLength = ((bytes.length + 9 + 63) & ~63) - bytes.length;\n\tconst padded = new Uint8Array(bytes.length + padLength);\n\tpadded.set(bytes);\n\tpadded[bytes.length] = 0x80;\n\tconst view = new DataView(padded.buffer);\n\tview.setUint32(padded.length - 4, bitLength >>> 0);\n\tview.setUint32(padded.length - 8, Math.floor(bitLength / 0x1_0000_0000));\n\n\tlet h0 = 0x6a09e667, h1 = 0xbb67ae85, h2 = 0x3c6ef372, h3 = 0xa54ff53a;\n\tlet h4 = 0x510e527f, h5 = 0x9b05688c, h6 = 0x1f83d9ab, h7 = 0x5be0cd19;\n\tconst w = new Uint32Array(64);\n\tfor (let i = 0; i < padded.length; i += 64) {\n\t\tfor (let t = 0; t < 16; t++) {\n\t\t\tw[t] = view.getUint32(i + t * 4);\n\t\t}\n\t\tfor (let t = 16; t < 64; t++) {\n\t\t\tconst w15 = w[t - 15]!;\n\t\t\tconst w2 = w[t - 2]!;\n\t\t\tconst s0 = ((w15 >>> 7) | (w15 << 25)) ^ ((w15 >>> 18) | (w15 << 14)) ^ (w15 >>> 3);\n\t\t\tconst s1 = ((w2 >>> 17) | (w2 << 15)) ^ ((w2 >>> 19) | (w2 << 13)) ^ (w2 >>> 10);\n\t\t\tw[t] = (w[t - 16]! + s0 + w[t - 7]! + s1) >>> 0;\n\t\t}\n\t\tlet a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7;\n\t\tfor (let t = 0; t < 64; t++) {\n\t\t\tconst S1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));\n\t\t\tconst ch = (e & f) ^ (~e & g);\n\t\t\tconst T1 = (h + S1 + ch + ROUND_CONSTANTS[t]! + w[t]!) >>> 0;\n\t\t\tconst S0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));\n\t\t\tconst maj = (a & b) ^ (a & c) ^ (b & c);\n\t\t\tconst T2 = (S0 + maj) >>> 0;\n\t\t\th = g;\n\t\t\tg = f;\n\t\t\tf = e;\n\t\t\te = (d + T1) >>> 0;\n\t\t\td = c;\n\t\t\tc = b;\n\t\t\tb = a;\n\t\t\ta = (T1 + T2) >>> 0;\n\t\t}\n\t\th0 = (h0 + a) >>> 0;\n\t\th1 = (h1 + b) >>> 0;\n\t\th2 = (h2 + c) >>> 0;\n\t\th3 = (h3 + d) >>> 0;\n\t\th4 = (h4 + e) >>> 0;\n\t\th5 = (h5 + f) >>> 0;\n\t\th6 = (h6 + g) >>> 0;\n\t\th7 = (h7 + h) >>> 0;\n\t}\n\tconst toHex = (n: number): string => {\n\t\tlet result = '';\n\t\tfor (let i = 7; i >= 0; i--) {\n\t\t\tresult += HEX[(n >>> (i * 4)) & 0xf];\n\t\t}\n\t\treturn result;\n\t};\n\treturn toHex(h0) + toHex(h1) + toHex(h2) + toHex(h3) + toHex(h4) + toHex(h5) + toHex(h6) + toHex(h7);\n};\n\nconst fingerprintOf = (value: string): string => sha256Hex(value).slice(0, 8);\n\n// -----------------------------------------------------------------------------\n// Bundled adapters\n// -----------------------------------------------------------------------------\n\nexport type InMemoryAdapterOptions = {\n\tinitial?: Record<string, string>;\n\t/** Override the rotation strategy. Default = random 32-char base36 string. */\n\trotate?: (name: string, previous: string | null) => string;\n};\n\nconst randomBase36 = (length: number): string => {\n\tlet out = '';\n\twhile (out.length < length) {\n\t\tout += Math.random().toString(36).slice(2);\n\t}\n\treturn out.slice(0, length);\n};\n\nexport const inMemoryAdapter = (\n\toptions: InMemoryAdapterOptions = {},\n): SecretAdapter => {\n\tconst store = new Map<string, string>();\n\tfor (const [k, v] of Object.entries(options.initial ?? {})) store.set(k, v);\n\tconst rotate = options.rotate ?? (() => randomBase36(32));\n\treturn {\n\t\tfetch: async (name) => store.get(name) ?? null,\n\t\tlist: async () => Array.from(store.keys()),\n\t\tput: async (name, value) => { store.set(name, value); },\n\t\tremove: async (name) => { store.delete(name); },\n\t\trotate: async (name) => {\n\t\t\tconst next = rotate(name, store.get(name) ?? null);\n\t\t\tstore.set(name, next);\n\t\t\treturn next;\n\t\t},\n\t};\n};\n\nexport type EnvAdapterOptions = {\n\t/**\n\t * If set, lookups are prefixed before reading from env. e.g.\n\t * `prefix: 'ABS_SECRET_'` and `resolve('STRIPE_KEY')` reads `ABS_SECRET_STRIPE_KEY`.\n\t * Default `''` (no prefix).\n\t */\n\tprefix?: string;\n\t/** The env object to read from. Default `process.env`. */\n\tenv?: Record<string, string | undefined>;\n};\n\nexport const envAdapter = (options: EnvAdapterOptions = {}): SecretAdapter => {\n\tconst prefix = options.prefix ?? '';\n\tconst env = options.env ?? (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env ?? {};\n\treturn {\n\t\tfetch: async (name) => {\n\t\t\tconst key = `${prefix}${name}`;\n\t\t\tconst value = env[key];\n\t\t\treturn value === undefined ? null : value;\n\t\t},\n\t\tlist: async () => {\n\t\t\tif (!prefix) return Object.keys(env);\n\t\t\tconst matches: string[] = [];\n\t\t\tfor (const key of Object.keys(env)) {\n\t\t\t\tif (key.startsWith(prefix)) matches.push(key.slice(prefix.length));\n\t\t\t}\n\t\t\treturn matches;\n\t\t},\n\t};\n};\n\n/**\n * Compose adapters: `fetch` falls through to the first non-null result;\n * `put` / `rotate` / `remove` go to the first adapter that implements them.\n */\nexport const compositeAdapter = (adapters: SecretAdapter[]): SecretAdapter => {\n\tconst firstWith = <K extends keyof SecretAdapter>(method: K) =>\n\t\tadapters.find((adapter) => adapter[method] !== undefined);\n\treturn {\n\t\tfetch: async (name) => {\n\t\t\tfor (const adapter of adapters) {\n\t\t\t\tconst value = await adapter.fetch(name);\n\t\t\t\tif (value !== null) return value;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t\tlist: async () => {\n\t\t\tconst seen = new Set<string>();\n\t\t\tfor (const adapter of adapters) {\n\t\t\t\tif (!adapter.list) continue;\n\t\t\t\tfor (const name of await adapter.list()) seen.add(name);\n\t\t\t}\n\t\t\treturn Array.from(seen);\n\t\t},\n\t\tput: async (name, value) => {\n\t\t\tconst target = firstWith('put');\n\t\t\tif (!target?.put) throw new Error('No adapter in the composite supports put()');\n\t\t\tawait target.put(name, value);\n\t\t},\n\t\tremove: async (name) => {\n\t\t\tconst target = firstWith('remove');\n\t\t\tif (!target?.remove) throw new Error('No adapter in the composite supports remove()');\n\t\t\tawait target.remove(name);\n\t\t},\n\t\trotate: async (name) => {\n\t\t\tconst target = firstWith('rotate');\n\t\t\tif (!target?.rotate) throw new Error('No adapter in the composite supports rotate()');\n\t\t\treturn target.rotate(name);\n\t\t},\n\t};\n};\n\n// -----------------------------------------------------------------------------\n// Broker\n// -----------------------------------------------------------------------------\n\ntype CacheEntry = {\n\tvalue: string;\n\tfingerprint: string;\n\tstoredAt: number;\n};\n\nexport const createSecretBroker = (options: SecretBrokerOptions): SecretBroker => {\n\tconst clock = options.clock ?? Date.now;\n\tconst ttl = options.cacheTtlMs ?? 60_000;\n\tconst minLen = options.redactionMinLength ?? 8;\n\tconst audit = options.audit;\n\tconst cache = new Map<string, CacheEntry>();\n\tlet disposed = false;\n\n\tconst fireAudit = (event: AuditEvent) => {\n\t\tif (!audit) return;\n\t\ttry {\n\t\t\tconst result = audit(event);\n\t\t\tif (result && typeof (result as Promise<void>).then === 'function') {\n\t\t\t\t(result as Promise<void>).catch((error) => {\n\t\t\t\t\tconsole.error('[secrets] audit hook rejected:', error);\n\t\t\t\t});\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error('[secrets] audit hook threw:', error);\n\t\t}\n\t};\n\n\tconst cacheEntry = (name: string, value: string, now: number): CacheEntry => {\n\t\tconst entry: CacheEntry = {\n\t\t\tfingerprint: fingerprintOf(value),\n\t\t\tstoredAt: now,\n\t\t\tvalue,\n\t\t};\n\t\tcache.set(name, entry);\n\t\treturn entry;\n\t};\n\n\tconst resolve: SecretBroker['resolve'] = async (name) => {\n\t\tif (disposed) return null;\n\t\tconst now = clock();\n\t\tconst cached = cache.get(name);\n\t\tif (cached && now - cached.storedAt < ttl) {\n\t\t\tfireAudit({ at: now, event: 'resolve.hit', fingerprint: cached.fingerprint, name });\n\t\t\treturn { fingerprint: cached.fingerprint, value: cached.value };\n\t\t}\n\t\ttry {\n\t\t\tconst value = await options.adapter.fetch(name);\n\t\t\tif (value === null) {\n\t\t\t\tfireAudit({ at: now, event: 'resolve.miss', name });\n\t\t\t\tcache.delete(name);\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst entry = cacheEntry(name, value, now);\n\t\t\tfireAudit({ at: now, event: 'resolve.miss', fingerprint: entry.fingerprint, name });\n\t\t\treturn { fingerprint: entry.fingerprint, value: entry.value };\n\t\t} catch (error) {\n\t\t\tfireAudit({\n\t\t\t\tat: now,\n\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t\tevent: 'resolve.error',\n\t\t\t\tname,\n\t\t\t});\n\t\t\tthrow error;\n\t\t}\n\t};\n\n\tconst rotate: SecretBroker['rotate'] = async (name) => {\n\t\tif (disposed) throw new Error('Broker is disposed');\n\t\tif (!options.adapter.rotate) {\n\t\t\tthrow new Error('Adapter does not support rotate()');\n\t\t}\n\t\tconst next = await options.adapter.rotate(name);\n\t\tconst now = clock();\n\t\tconst entry = cacheEntry(name, next, now);\n\t\tfireAudit({ at: now, event: 'rotate', fingerprint: entry.fingerprint, name });\n\t\treturn { fingerprint: entry.fingerprint, value: entry.value };\n\t};\n\n\tconst invalidate: SecretBroker['invalidate'] = (name) => {\n\t\tif (name === undefined) {\n\t\t\tcache.clear();\n\t\t} else {\n\t\t\tcache.delete(name);\n\t\t}\n\t\tfireAudit({ at: clock(), event: 'invalidate', name: name ?? null });\n\t};\n\n\tconst redact: SecretBroker['redact'] = (text) => {\n\t\tif (text.length === 0 || cache.size === 0) return text;\n\t\t// Replace longest values first so a substring of a longer secret\n\t\t// isn't blanked before its full match is found.\n\t\tconst ordered = Array.from(cache.entries())\n\t\t\t.filter(([, entry]) => entry.value.length >= minLen)\n\t\t\t.sort(([, a], [, b]) => b.value.length - a.value.length);\n\t\tlet out = text;\n\t\tfor (const [name, entry] of ordered) {\n\t\t\tif (!out.includes(entry.value)) continue;\n\t\t\tout = out.split(entry.value).join(`[REDACTED:${name}]`);\n\t\t}\n\t\treturn out;\n\t};\n\n\treturn {\n\t\tdispose: () => {\n\t\t\tdisposed = true;\n\t\t\tcache.clear();\n\t\t},\n\t\tfingerprint: fingerprintOf,\n\t\tinvalidate,\n\t\tredact,\n\t\tresolve,\n\t\trotate,\n\t};\n};\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "EE866E47E57910C764756E2164756E21",
9
+ "names": []
10
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@absolutejs/secrets",
3
+ "version": "0.0.1",
4
+ "description": "Host-side secret broker for multi-tenant Bun runtimes. Pluggable adapters (env-var, in-memory, composite); audit hook per resolve; safe fingerprints for logs; redact() walks known secrets out of arbitrary text before it lands in a log sink.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/absolutejs/secrets.git"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "module": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "type": "module",
13
+ "license": "BSL-1.1",
14
+ "author": "Alex Kahn",
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "keywords": [
19
+ "absolutejs",
20
+ "bun",
21
+ "paas",
22
+ "multi-tenant",
23
+ "secrets",
24
+ "credential-broker",
25
+ "redaction",
26
+ "rotation"
27
+ ],
28
+ "scripts": {
29
+ "build": "rm -rf dist && bun build src/index.ts --outdir dist --sourcemap --target=bun && tsc --project tsconfig.build.json",
30
+ "test": "bun test tests/",
31
+ "typecheck": "tsc --noEmit",
32
+ "format": "prettier --write \"./**/*.{ts,json,md}\"",
33
+ "check:package": "bun run typecheck && bun run build && bun run test",
34
+ "release": "bun run format && bun run check:package && bun publish"
35
+ },
36
+ "peerDependencies": {
37
+ "bun-types": "^1.3.14"
38
+ },
39
+ "devDependencies": {
40
+ "@types/bun": "^1.3.14",
41
+ "prettier": "^3.8.3",
42
+ "typescript": "^6.0.3"
43
+ },
44
+ "exports": {
45
+ ".": {
46
+ "types": "./dist/index.d.ts",
47
+ "import": "./dist/index.js",
48
+ "default": "./dist/index.js"
49
+ }
50
+ },
51
+ "files": ["dist", "README.md"]
52
+ }