hazo_secure 1.1.0 → 1.3.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.
- package/CHANGE_LOG.md +11 -0
- package/README.md +24 -2
- package/SETUP_CHECKLIST.md +17 -0
- package/dist/crypto/index.d.ts +34 -1
- package/dist/crypto/index.js +39 -0
- package/dist/mask/index.d.ts +48 -0
- package/dist/mask/index.js +115 -0
- package/dist/secrets/index.d.ts +41 -0
- package/dist/secrets/index.js +84 -0
- package/package.json +28 -14
package/CHANGE_LOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## 1.3.0 — 2026-06-12
|
|
4
|
+
|
|
5
|
+
### New: `hazo_secure/secrets` — SecretsProvider
|
|
6
|
+
|
|
7
|
+
- **`SecretsProvider` interface** with `get(name)` / `resolve(names[])` methods.
|
|
8
|
+
- **`EnvSecretsProvider`** — reads from env vars (`${prefix}_${NAME}`; default prefix `HAZO_SECRET`).
|
|
9
|
+
- **`StaticSecretsProvider`** — in-memory map; primary use is tests.
|
|
10
|
+
- **`LookupSecretsProvider`** — caller-supplied `(name) => string|undefined` function.
|
|
11
|
+
- **`SecretsError`** — extends `HazoError`, code `HAZO_SECURE_SECRET_NOT_FOUND` (httpStatus 400, retryable false).
|
|
12
|
+
- New `./secrets` subpath export.
|
|
13
|
+
|
|
3
14
|
## 1.1.0 — 2026-05-29
|
|
4
15
|
|
|
5
16
|
### hazo workspace v2 — Wave 2 migration
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# hazo_secure
|
|
2
2
|
|
|
3
|
-
Security & compliance primitives for Node.js / Next.js apps.
|
|
3
|
+
Security & compliance primitives for Node.js / Next.js apps. Seven independent subpath modules — import only the ones you need.
|
|
4
4
|
|
|
5
5
|
| Subpath | Purpose |
|
|
6
6
|
|---|---|
|
|
@@ -9,6 +9,8 @@ Security & compliance primitives for Node.js / Next.js apps. Four independent su
|
|
|
9
9
|
| `hazo_secure/crypto` | AES-256-GCM field-level encryption with key versioning and AAD support |
|
|
10
10
|
| `hazo_secure/gdpr` | Exporter / anonymiser registry + lifecycle orchestrator. Uses `hazo_jobs` for async export, `hazo_files` for ZIP delivery |
|
|
11
11
|
| `hazo_secure/csrf` | Double-submit CSRF protection for Next.js routes — token generation, cookie header, constant-time verification |
|
|
12
|
+
| `hazo_secure/mask` | PII masking and anonymization transforms (redact, hash, tokenize, fake_name, jitter_date, …) |
|
|
13
|
+
| `hazo_secure/secrets` | Provider-pattern secret resolution — env vars, static map, or custom lookup |
|
|
12
14
|
|
|
13
15
|
## Install
|
|
14
16
|
|
|
@@ -16,7 +18,7 @@ Security & compliance primitives for Node.js / Next.js apps. Four independent su
|
|
|
16
18
|
npm install hazo_secure
|
|
17
19
|
```
|
|
18
20
|
|
|
19
|
-
`
|
|
21
|
+
`hazo_core` is a required peer dep. `hazo_logs`, `hazo_connect`, `hazo_jobs`, `hazo_files`, and `hazo_ui` are optional peers — `hazo_connect` is needed for `ConnectRateLimitStore`; `hazo_jobs` and `hazo_files` are only needed for `hazo_secure/gdpr`.
|
|
20
22
|
|
|
21
23
|
## Quick start
|
|
22
24
|
|
|
@@ -56,6 +58,24 @@ const stored = await encryptField(plaintext, { keys, aad: `user:${userId}` });
|
|
|
56
58
|
const recovered = await decryptField(stored, { keys });
|
|
57
59
|
```
|
|
58
60
|
|
|
61
|
+
### Resolve application secrets
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { EnvSecretsProvider, StaticSecretsProvider, LookupSecretsProvider } from "hazo_secure/secrets";
|
|
65
|
+
|
|
66
|
+
// Reads from env vars: HAZO_SECRET_DB_PASSWORD, HAZO_SECRET_STRIPE_KEY, …
|
|
67
|
+
const secrets = new EnvSecretsProvider();
|
|
68
|
+
const { db_password, stripe_key } = await secrets.resolve(["db_password", "stripe_key"]);
|
|
69
|
+
|
|
70
|
+
// Tests — in-memory map
|
|
71
|
+
const mock = new StaticSecretsProvider({ api_key: "test-key" });
|
|
72
|
+
|
|
73
|
+
// Custom backend (KMS, HashiCorp Vault, etc.)
|
|
74
|
+
const vault = new LookupSecretsProvider(async (name) => fetchFromVault(name));
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`SecretsError` (code `HAZO_SECURE_SECRET_NOT_FOUND`) is thrown when a name can't be resolved. Use a custom prefix: `new EnvSecretsProvider("MY_APP")` → reads `MY_APP_<NAME>`.
|
|
78
|
+
|
|
59
79
|
### Register GDPR exporters
|
|
60
80
|
|
|
61
81
|
```ts
|
|
@@ -152,6 +172,8 @@ await gdpr.anonymiseUser({ userId });
|
|
|
152
172
|
| `hazo_secure/fetch` | ⚠️ pre-flight only | ✅ full protection | Edge: host/IP checks only; Node: + undici connect-time validation |
|
|
153
173
|
| `hazo_secure/crypto` | ❌ | ✅ | Uses `node:crypto` |
|
|
154
174
|
| `hazo_secure/gdpr` | ❌ | ✅ | Async iterables + optional hazo_connect |
|
|
175
|
+
| `hazo_secure/mask` | ✅ | ✅ | Pure transforms — no I/O |
|
|
176
|
+
| `hazo_secure/secrets` | ✅ | ✅ | Pure async — no Node built-ins |
|
|
155
177
|
|
|
156
178
|
**Node.js ≥ 18 required** for all Node-only subpaths.
|
|
157
179
|
|
package/SETUP_CHECKLIST.md
CHANGED
|
@@ -34,6 +34,20 @@ Default in-memory store works out of the box. For multi-instance deployments, su
|
|
|
34
34
|
- **Custom** — implement the `KeyProvider` interface directly
|
|
35
35
|
3. Decide AAD scheme — usually `domain:entityId` (e.g. `health:${personId}`)
|
|
36
36
|
|
|
37
|
+
### `hazo_secure/secrets`
|
|
38
|
+
|
|
39
|
+
No setup. Choose a provider at startup:
|
|
40
|
+
|
|
41
|
+
- **`EnvSecretsProvider(prefix?)`** — reads from `${prefix}_${NAME}` env vars (default prefix: `HAZO_SECRET`). Set each secret as an env var on your deployment platform.
|
|
42
|
+
- **`StaticSecretsProvider(map)`** — in-memory; use in tests only.
|
|
43
|
+
- **`LookupSecretsProvider(fn)`** — caller-supplied sync or async lookup; useful for KMS / Vault integrations.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { EnvSecretsProvider } from "hazo_secure/secrets";
|
|
47
|
+
const secrets = new EnvSecretsProvider();
|
|
48
|
+
const value = await secrets.get("db_password"); // reads HAZO_SECRET_DB_PASSWORD
|
|
49
|
+
```
|
|
50
|
+
|
|
37
51
|
### `hazo_secure/gdpr`
|
|
38
52
|
|
|
39
53
|
1. Install optional peers if you'll use async export and ZIP delivery:
|
|
@@ -65,4 +79,7 @@ node --input-type=module -e "import * as m from 'hazo_secure/crypto'; console.lo
|
|
|
65
79
|
|
|
66
80
|
# GDPR — expect: createGdprRegistry, GdprRegistryError
|
|
67
81
|
node --input-type=module -e "import * as m from 'hazo_secure/gdpr'; console.log(Object.keys(m).join(', '))"
|
|
82
|
+
|
|
83
|
+
# Secrets — expect: SecretsError, SecretsProvider (type), EnvSecretsProvider, StaticSecretsProvider, LookupSecretsProvider
|
|
84
|
+
node --input-type=module -e "import * as m from 'hazo_secure/secrets'; console.log(Object.keys(m).join(', '))"
|
|
68
85
|
```
|
package/dist/crypto/index.d.ts
CHANGED
|
@@ -140,8 +140,41 @@ declare class StaticKeyProvider implements KeyProvider {
|
|
|
140
140
|
}>;
|
|
141
141
|
byId(keyId: string): Promise<Uint8Array>;
|
|
142
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Lookup-function-backed `KeyProvider` for config-file (INI) key sources.
|
|
145
|
+
* Reads keys from a caller-supplied synchronous lookup function, decoding
|
|
146
|
+
* each value from base64 to a 32-byte AES-256 key.
|
|
147
|
+
*
|
|
148
|
+
* Expected key naming convention (mirrors hazo_config INI layout):
|
|
149
|
+
* `current` → the current key id (e.g. `"v1"`)
|
|
150
|
+
* `key_<id>` → base64-encoded 32-byte key (e.g. `key_v1=<base64>`)
|
|
151
|
+
*
|
|
152
|
+
* Example backed by hazo_config:
|
|
153
|
+
* ```ts
|
|
154
|
+
* const cfg = getConfig('crypto.myapp'); // hazo_config, not imported here
|
|
155
|
+
* const keys = new LookupKeyProvider(name => cfg.get(name));
|
|
156
|
+
* ```
|
|
157
|
+
*
|
|
158
|
+
* hazo_secure takes no dependency on hazo_config — the caller supplies the
|
|
159
|
+
* lookup, keeping this library config-agnostic. `Buffer.from(value, 'base64')`
|
|
160
|
+
* handles standard base64; keys decoded to anything other than 32 bytes are
|
|
161
|
+
* rejected with `invalid_key`.
|
|
162
|
+
*
|
|
163
|
+
* Decoded keys are memoised per id so the lookup runs once per key per
|
|
164
|
+
* process lifetime.
|
|
165
|
+
*/
|
|
166
|
+
declare class LookupKeyProvider implements KeyProvider {
|
|
167
|
+
private readonly lookup;
|
|
168
|
+
private readonly cache;
|
|
169
|
+
constructor(lookup: (name: string) => string | undefined);
|
|
170
|
+
current(): Promise<{
|
|
171
|
+
keyId: string;
|
|
172
|
+
key: Uint8Array;
|
|
173
|
+
}>;
|
|
174
|
+
byId(keyId: string): Promise<Uint8Array>;
|
|
175
|
+
}
|
|
143
176
|
/** Helper: build a `aad` string with a stable, parser-friendly shape.
|
|
144
177
|
* Convention: `<table>:<entityId>:<field>` — collision-free across tables. */
|
|
145
178
|
declare function aadFor(table: string, entityId: string, field: string): string;
|
|
146
179
|
|
|
147
|
-
export { CryptoError, type CryptoErrorCode, type EncryptedField, EnvKeyProvider, HttpKeyProvider, type HttpKeyProviderOptions, type KeyProvider, StaticKeyProvider, aadFor, decryptField, encryptField };
|
|
180
|
+
export { CryptoError, type CryptoErrorCode, type EncryptedField, EnvKeyProvider, HttpKeyProvider, type HttpKeyProviderOptions, type KeyProvider, LookupKeyProvider, StaticKeyProvider, aadFor, decryptField, encryptField };
|
package/dist/crypto/index.js
CHANGED
|
@@ -218,6 +218,44 @@ var StaticKeyProvider = class {
|
|
|
218
218
|
return key;
|
|
219
219
|
}
|
|
220
220
|
};
|
|
221
|
+
var LookupKeyProvider = class {
|
|
222
|
+
constructor(lookup) {
|
|
223
|
+
this.lookup = lookup;
|
|
224
|
+
}
|
|
225
|
+
lookup;
|
|
226
|
+
cache = /* @__PURE__ */ new Map();
|
|
227
|
+
async current() {
|
|
228
|
+
const keyId = this.lookup("current");
|
|
229
|
+
if (!keyId) {
|
|
230
|
+
throw new CryptoError(
|
|
231
|
+
"LookupKeyProvider: no 'current' key id from lookup",
|
|
232
|
+
"missing_env_key"
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
const key = await this.byId(keyId);
|
|
236
|
+
return { keyId, key };
|
|
237
|
+
}
|
|
238
|
+
async byId(keyId) {
|
|
239
|
+
if (this.cache.has(keyId)) return this.cache.get(keyId);
|
|
240
|
+
const lookupName = `key_${keyId}`;
|
|
241
|
+
const raw = this.lookup(lookupName);
|
|
242
|
+
if (!raw) {
|
|
243
|
+
throw new CryptoError(
|
|
244
|
+
`LookupKeyProvider: no key material for id ${keyId} (looked up "${lookupName}")`,
|
|
245
|
+
"key_not_found"
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
const buf = Buffer.from(raw, "base64");
|
|
249
|
+
if (buf.byteLength !== KEY_LEN2) {
|
|
250
|
+
throw new CryptoError(
|
|
251
|
+
`${lookupName} decodes to ${buf.byteLength} bytes; AES-256-GCM needs ${KEY_LEN2}`,
|
|
252
|
+
"invalid_key"
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
this.cache.set(keyId, buf);
|
|
256
|
+
return buf;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
221
259
|
function aadFor(table, entityId, field) {
|
|
222
260
|
return `${table}:${entityId}:${field}`;
|
|
223
261
|
}
|
|
@@ -225,6 +263,7 @@ export {
|
|
|
225
263
|
CryptoError,
|
|
226
264
|
EnvKeyProvider,
|
|
227
265
|
HttpKeyProvider,
|
|
266
|
+
LookupKeyProvider,
|
|
228
267
|
StaticKeyProvider,
|
|
229
268
|
aadFor,
|
|
230
269
|
decryptField,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mask_email — replace with deterministic masked email.
|
|
3
|
+
* alice@example.com → a3f9b12c1d2e@masked.example
|
|
4
|
+
* Same source → same masked value (FK survival).
|
|
5
|
+
*/
|
|
6
|
+
declare function mask_email(value: unknown, _col: string, maskKey: string): unknown;
|
|
7
|
+
/**
|
|
8
|
+
* mask_phone — replace with deterministic masked phone.
|
|
9
|
+
* Preserves leading + and length pattern. +1-555-0001 → +1-XXX-a3f9b1
|
|
10
|
+
*/
|
|
11
|
+
declare function mask_phone(value: unknown, _col: string, maskKey: string): unknown;
|
|
12
|
+
declare function fake_name(value: unknown, _col: string, maskKey: string): unknown;
|
|
13
|
+
/**
|
|
14
|
+
* jitter_date — add a deterministic date offset (±0–180 days) to a date string.
|
|
15
|
+
* Preserves approximate era; different source values → different offsets.
|
|
16
|
+
*/
|
|
17
|
+
declare function jitter_date(value: unknown, _col: string, maskKey: string): unknown;
|
|
18
|
+
/**
|
|
19
|
+
* hash — replace with HMAC-SHA256 hex hash of value.
|
|
20
|
+
* Fully opaque; same source → same hash.
|
|
21
|
+
*/
|
|
22
|
+
declare function hash(value: unknown, col: string, maskKey: string): unknown;
|
|
23
|
+
/**
|
|
24
|
+
* tokenize — replace with a stable short token (FK-safe).
|
|
25
|
+
* tok_<12 hex chars> — same source → same token.
|
|
26
|
+
*/
|
|
27
|
+
declare function tokenize(value: unknown, col: string, maskKey: string): unknown;
|
|
28
|
+
/**
|
|
29
|
+
* drop — always returns null (secret dropped, not masked).
|
|
30
|
+
* Used for passwords, tokens, etc.
|
|
31
|
+
*/
|
|
32
|
+
declare function drop(_value: unknown, _col: string, _maskKey: string): unknown;
|
|
33
|
+
/**
|
|
34
|
+
* nullify — replace with null (less aggressive than drop; preserves column semantics).
|
|
35
|
+
*/
|
|
36
|
+
declare function nullify(_value: unknown, _col: string, _maskKey: string): unknown;
|
|
37
|
+
/**
|
|
38
|
+
* redact_pii — NER-based PII redaction (wink-nlp lite model).
|
|
39
|
+
* Detects named entities (dates, cardinals, etc.) and replaces them with [REDACTED_TYPE].
|
|
40
|
+
* Deterministic: same input + same model → same output.
|
|
41
|
+
* maskKey is accepted but not used (redaction is model-driven, not key-driven).
|
|
42
|
+
*/
|
|
43
|
+
declare function redact_pii(value: unknown, _col: string, _maskKey: string): unknown;
|
|
44
|
+
/** Registry of all built-in transforms */
|
|
45
|
+
declare const BUILTIN_TRANSFORMS: Record<string, (value: unknown, col: string, maskKey: string) => unknown>;
|
|
46
|
+
type MaskTransformFn = (value: unknown, col: string, maskKey: string) => unknown;
|
|
47
|
+
|
|
48
|
+
export { BUILTIN_TRANSFORMS, type MaskTransformFn, drop, fake_name, hash, jitter_date, mask_email, mask_phone, nullify, redact_pii, tokenize };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// src/mask/index.ts
|
|
2
|
+
import { createHmac } from "crypto";
|
|
3
|
+
import { createRequire } from "module";
|
|
4
|
+
function hmacHex(value, maskKey, context) {
|
|
5
|
+
return createHmac("sha256", maskKey).update(`${context}:${value}`).digest("hex");
|
|
6
|
+
}
|
|
7
|
+
function hmacToken(value, maskKey, context) {
|
|
8
|
+
return hmacHex(value, maskKey, context).slice(0, 12);
|
|
9
|
+
}
|
|
10
|
+
function mask_email(value, _col, maskKey) {
|
|
11
|
+
if (value === null || value === void 0) return value;
|
|
12
|
+
const str = String(value);
|
|
13
|
+
const atIdx = str.indexOf("@");
|
|
14
|
+
const domain = atIdx >= 0 ? str.slice(atIdx + 1) : "example.com";
|
|
15
|
+
const token = hmacToken(str, maskKey, "email");
|
|
16
|
+
return `${token}@masked.${domain}`;
|
|
17
|
+
}
|
|
18
|
+
function mask_phone(value, _col, maskKey) {
|
|
19
|
+
if (value === null || value === void 0) return value;
|
|
20
|
+
const str = String(value);
|
|
21
|
+
const token = hmacToken(str, maskKey, "phone").slice(0, 6);
|
|
22
|
+
const hasPlus = str.startsWith("+");
|
|
23
|
+
const digits = str.replace(/[^\d]/g, "");
|
|
24
|
+
const prefix = digits.length > 4 ? digits.slice(0, 2) : "00";
|
|
25
|
+
return `${hasPlus ? "+" : ""}${prefix}-XXX-${token}`;
|
|
26
|
+
}
|
|
27
|
+
var FIRST_NAMES = ["Alex", "Blake", "Casey", "Dana", "Ellis", "Finley", "Gray", "Harper", "Indigo", "Jordan"];
|
|
28
|
+
var LAST_NAMES = ["Smith", "Jones", "Williams", "Brown", "Davis", "Miller", "Wilson", "Moore", "Taylor", "Anderson"];
|
|
29
|
+
function fake_name(value, _col, maskKey) {
|
|
30
|
+
if (value === null || value === void 0) return value;
|
|
31
|
+
const str = String(value);
|
|
32
|
+
const h = hmacHex(str, maskKey, "name");
|
|
33
|
+
const firstIdx = parseInt(h.slice(0, 2), 16) % FIRST_NAMES.length;
|
|
34
|
+
const lastIdx = parseInt(h.slice(2, 4), 16) % LAST_NAMES.length;
|
|
35
|
+
return `${FIRST_NAMES[firstIdx]} ${LAST_NAMES[lastIdx]}`;
|
|
36
|
+
}
|
|
37
|
+
function jitter_date(value, _col, maskKey) {
|
|
38
|
+
if (value === null || value === void 0) return value;
|
|
39
|
+
const str = String(value);
|
|
40
|
+
const h = hmacHex(str, maskKey, "date");
|
|
41
|
+
const jitterDays = parseInt(h.slice(0, 4), 16) % 361 - 180;
|
|
42
|
+
const date = new Date(str);
|
|
43
|
+
if (isNaN(date.getTime())) return value;
|
|
44
|
+
date.setDate(date.getDate() + jitterDays);
|
|
45
|
+
return date.toISOString().slice(0, 10);
|
|
46
|
+
}
|
|
47
|
+
function hash(value, col, maskKey) {
|
|
48
|
+
if (value === null || value === void 0) return value;
|
|
49
|
+
return hmacHex(String(value), maskKey, col);
|
|
50
|
+
}
|
|
51
|
+
function tokenize(value, col, maskKey) {
|
|
52
|
+
if (value === null || value === void 0) return value;
|
|
53
|
+
return `tok_${hmacToken(String(value), maskKey, col)}`;
|
|
54
|
+
}
|
|
55
|
+
function drop(_value, _col, _maskKey) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
function nullify(_value, _col, _maskKey) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
var _esmReq = createRequire(import.meta.url);
|
|
62
|
+
var _nlpInstance = null;
|
|
63
|
+
var _nlpIts = null;
|
|
64
|
+
function getNlp() {
|
|
65
|
+
if (_nlpInstance) return { nlp: _nlpInstance, its: _nlpIts };
|
|
66
|
+
try {
|
|
67
|
+
const winkNLP = _esmReq("wink-nlp");
|
|
68
|
+
const model = _esmReq("wink-eng-lite-web-model");
|
|
69
|
+
const nlp = winkNLP(model, ["ner"]);
|
|
70
|
+
_nlpInstance = nlp;
|
|
71
|
+
_nlpIts = nlp.its;
|
|
72
|
+
return { nlp, its: nlp.its };
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function redact_pii(value, _col, _maskKey) {
|
|
78
|
+
if (value === null || value === void 0) return value;
|
|
79
|
+
const text = String(value);
|
|
80
|
+
const ctx = getNlp();
|
|
81
|
+
if (!ctx) return value;
|
|
82
|
+
const doc = ctx.nlp.readDoc(text);
|
|
83
|
+
const replacements = [];
|
|
84
|
+
doc.entities().each((e) => {
|
|
85
|
+
replacements.push({ text: e.out(), type: e.out(ctx.its.type) });
|
|
86
|
+
});
|
|
87
|
+
let result = text;
|
|
88
|
+
for (const { text: entityText, type } of replacements) {
|
|
89
|
+
result = result.replaceAll(entityText, `[REDACTED_${type}]`);
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
var BUILTIN_TRANSFORMS = {
|
|
94
|
+
mask_email,
|
|
95
|
+
mask_phone,
|
|
96
|
+
fake_name,
|
|
97
|
+
jitter_date,
|
|
98
|
+
hash,
|
|
99
|
+
tokenize,
|
|
100
|
+
drop,
|
|
101
|
+
nullify,
|
|
102
|
+
redact_pii
|
|
103
|
+
};
|
|
104
|
+
export {
|
|
105
|
+
BUILTIN_TRANSFORMS,
|
|
106
|
+
drop,
|
|
107
|
+
fake_name,
|
|
108
|
+
hash,
|
|
109
|
+
jitter_date,
|
|
110
|
+
mask_email,
|
|
111
|
+
mask_phone,
|
|
112
|
+
nullify,
|
|
113
|
+
redact_pii,
|
|
114
|
+
tokenize
|
|
115
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { HazoError } from 'hazo_core';
|
|
2
|
+
|
|
3
|
+
type SecretsErrorCode = 'HAZO_SECURE_SECRET_NOT_FOUND';
|
|
4
|
+
declare class SecretsError extends HazoError {
|
|
5
|
+
readonly code: SecretsErrorCode;
|
|
6
|
+
constructor(code: SecretsErrorCode, message: string);
|
|
7
|
+
}
|
|
8
|
+
interface SecretsProvider {
|
|
9
|
+
/** Resolve multiple secret names at once. Returns all resolved values.
|
|
10
|
+
* Throws SecretsError (HAZO_SECURE_SECRET_NOT_FOUND) for any missing name. */
|
|
11
|
+
resolve(names: string[]): Promise<Record<string, string>>;
|
|
12
|
+
/** Resolve a single secret. Throws if absent. */
|
|
13
|
+
get(name: string): Promise<string>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Reads secrets from environment variables.
|
|
17
|
+
* Variable name = `${prefix}_${NAME.toUpperCase()}` (underscores preserved).
|
|
18
|
+
* Default prefix: `HAZO_SECRET`
|
|
19
|
+
*/
|
|
20
|
+
declare class EnvSecretsProvider implements SecretsProvider {
|
|
21
|
+
private readonly prefix;
|
|
22
|
+
constructor(prefix?: string);
|
|
23
|
+
get(name: string): Promise<string>;
|
|
24
|
+
resolve(names: string[]): Promise<Record<string, string>>;
|
|
25
|
+
}
|
|
26
|
+
/** In-memory map. Primary use: tests. */
|
|
27
|
+
declare class StaticSecretsProvider implements SecretsProvider {
|
|
28
|
+
private readonly map;
|
|
29
|
+
constructor(secrets: Record<string, string>);
|
|
30
|
+
get(name: string): Promise<string>;
|
|
31
|
+
resolve(names: string[]): Promise<Record<string, string>>;
|
|
32
|
+
}
|
|
33
|
+
/** Caller-supplied lookup function. Useful for custom secret backends. */
|
|
34
|
+
declare class LookupSecretsProvider implements SecretsProvider {
|
|
35
|
+
private readonly lookup;
|
|
36
|
+
constructor(lookup: (name: string) => string | undefined | Promise<string | undefined>);
|
|
37
|
+
get(name: string): Promise<string>;
|
|
38
|
+
resolve(names: string[]): Promise<Record<string, string>>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { EnvSecretsProvider, LookupSecretsProvider, SecretsError, type SecretsProvider, StaticSecretsProvider };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// src/secrets/index.ts
|
|
2
|
+
import { HazoError } from "hazo_core";
|
|
3
|
+
var SecretsError = class extends HazoError {
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super({ code, pkg: "hazo_secure", message, httpStatus: 400, retryable: false });
|
|
6
|
+
this.name = "SecretsError";
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
var EnvSecretsProvider = class {
|
|
10
|
+
prefix;
|
|
11
|
+
constructor(prefix = "HAZO_SECRET") {
|
|
12
|
+
this.prefix = prefix;
|
|
13
|
+
}
|
|
14
|
+
async get(name) {
|
|
15
|
+
const envKey = `${this.prefix}_${name.toUpperCase()}`;
|
|
16
|
+
const val = process.env[envKey];
|
|
17
|
+
if (val === void 0) {
|
|
18
|
+
throw new SecretsError(
|
|
19
|
+
"HAZO_SECURE_SECRET_NOT_FOUND",
|
|
20
|
+
`Secret "${name}" not found (checked env var ${envKey})`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return val;
|
|
24
|
+
}
|
|
25
|
+
async resolve(names) {
|
|
26
|
+
const out = {};
|
|
27
|
+
for (const name of names) {
|
|
28
|
+
out[name] = await this.get(name);
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var StaticSecretsProvider = class {
|
|
34
|
+
map;
|
|
35
|
+
constructor(secrets) {
|
|
36
|
+
this.map = new Map(Object.entries(secrets));
|
|
37
|
+
}
|
|
38
|
+
async get(name) {
|
|
39
|
+
const val = this.map.get(name);
|
|
40
|
+
if (val === void 0) {
|
|
41
|
+
throw new SecretsError(
|
|
42
|
+
"HAZO_SECURE_SECRET_NOT_FOUND",
|
|
43
|
+
`Secret "${name}" not found in static map`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return val;
|
|
47
|
+
}
|
|
48
|
+
async resolve(names) {
|
|
49
|
+
const out = {};
|
|
50
|
+
for (const name of names) {
|
|
51
|
+
out[name] = await this.get(name);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
var LookupSecretsProvider = class {
|
|
57
|
+
lookup;
|
|
58
|
+
constructor(lookup) {
|
|
59
|
+
this.lookup = lookup;
|
|
60
|
+
}
|
|
61
|
+
async get(name) {
|
|
62
|
+
const val = await this.lookup(name);
|
|
63
|
+
if (val === void 0) {
|
|
64
|
+
throw new SecretsError(
|
|
65
|
+
"HAZO_SECURE_SECRET_NOT_FOUND",
|
|
66
|
+
`Secret "${name}" not found via lookup`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return val;
|
|
70
|
+
}
|
|
71
|
+
async resolve(names) {
|
|
72
|
+
const out = {};
|
|
73
|
+
for (const name of names) {
|
|
74
|
+
out[name] = await this.get(name);
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
export {
|
|
80
|
+
EnvSecretsProvider,
|
|
81
|
+
LookupSecretsProvider,
|
|
82
|
+
SecretsError,
|
|
83
|
+
StaticSecretsProvider
|
|
84
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hazo_secure",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Security & compliance primitives — SSRF-safe fetch, rate limiting, field-level encryption, GDPR orchestration, CSRF. Five subpath modules, pick the ones you need.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -36,6 +36,16 @@
|
|
|
36
36
|
"types": "./dist/csrf/index.d.ts",
|
|
37
37
|
"import": "./dist/csrf/index.js",
|
|
38
38
|
"default": "./dist/csrf/index.js"
|
|
39
|
+
},
|
|
40
|
+
"./mask": {
|
|
41
|
+
"types": "./dist/mask/index.d.ts",
|
|
42
|
+
"import": "./dist/mask/index.js",
|
|
43
|
+
"default": "./dist/mask/index.js"
|
|
44
|
+
},
|
|
45
|
+
"./secrets": {
|
|
46
|
+
"types": "./dist/secrets/index.d.ts",
|
|
47
|
+
"import": "./dist/secrets/index.js",
|
|
48
|
+
"default": "./dist/secrets/index.js"
|
|
39
49
|
}
|
|
40
50
|
},
|
|
41
51
|
"files": [
|
|
@@ -53,23 +63,25 @@
|
|
|
53
63
|
"build": "tsup",
|
|
54
64
|
"dev": "tsup --watch",
|
|
55
65
|
"lint": "tsc --noEmit",
|
|
56
|
-
"test": "jest",
|
|
57
|
-
"test:watch": "jest --watch",
|
|
66
|
+
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
|
|
67
|
+
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch",
|
|
58
68
|
"dev:test-app": "npm run build && cd test-app && npm run dev",
|
|
59
69
|
"build:test-app": "npm run build && cd test-app && npm run build",
|
|
60
70
|
"install:test-app": "cd test-app && npm install",
|
|
61
71
|
"prepublishOnly": "npm run build"
|
|
62
72
|
},
|
|
63
73
|
"dependencies": {
|
|
64
|
-
"undici": "^7.3.0"
|
|
74
|
+
"undici": "^7.3.0",
|
|
75
|
+
"wink-nlp": "^2.3.1",
|
|
76
|
+
"wink-eng-lite-web-model": "^1.7.0"
|
|
65
77
|
},
|
|
66
78
|
"peerDependencies": {
|
|
67
|
-
"hazo_core": "^1.
|
|
68
|
-
"hazo_logs": "^2.0.
|
|
69
|
-
"hazo_connect": "^3.
|
|
79
|
+
"hazo_core": "^1.1.0",
|
|
80
|
+
"hazo_logs": "^2.0.3",
|
|
81
|
+
"hazo_connect": "^3.5.1",
|
|
70
82
|
"hazo_jobs": "^0.12.0",
|
|
71
|
-
"hazo_files": "^3.
|
|
72
|
-
"hazo_ui": "^3.
|
|
83
|
+
"hazo_files": "^3.1.0",
|
|
84
|
+
"hazo_ui": "^3.5.0"
|
|
73
85
|
},
|
|
74
86
|
"peerDependenciesMeta": {
|
|
75
87
|
"hazo_logs": {
|
|
@@ -91,16 +103,18 @@
|
|
|
91
103
|
"devDependencies": {
|
|
92
104
|
"@types/jest": "^30.0.0",
|
|
93
105
|
"@types/node": "^22.10.0",
|
|
94
|
-
"hazo_core": "^1.
|
|
95
|
-
"hazo_logs": "^2.0.
|
|
96
|
-
"hazo_connect": "^3.
|
|
106
|
+
"hazo_core": "^1.1.0",
|
|
107
|
+
"hazo_logs": "^2.0.3",
|
|
108
|
+
"hazo_connect": "^3.5.1",
|
|
97
109
|
"hazo_jobs": "^0.12.0",
|
|
98
|
-
"hazo_files": "^3.
|
|
99
|
-
"hazo_ui": "^3.
|
|
110
|
+
"hazo_files": "^3.1.0",
|
|
111
|
+
"hazo_ui": "^3.5.0",
|
|
100
112
|
"jest": "^30.2.0",
|
|
101
113
|
"ts-jest": "^29.4.5",
|
|
102
114
|
"tsup": "^8.5.1",
|
|
103
115
|
"typescript": "^5.7.2",
|
|
116
|
+
"wink-nlp": "^2.3.1",
|
|
117
|
+
"wink-eng-lite-web-model": "^1.7.0",
|
|
104
118
|
"zod": "^3.23.8"
|
|
105
119
|
},
|
|
106
120
|
"author": "Pubs Abayasiri",
|