hazo_secure 1.4.0 → 1.6.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 +82 -0
- package/README.md +27 -0
- package/dist/crypto/index.d.ts +46 -1
- package/dist/crypto/index.js +39 -3
- package/dist/fetch/index.d.ts +17 -0
- package/dist/fetch/index.js +18 -6
- package/dist/mask/display.d.ts +28 -0
- package/dist/mask/display.js +17 -0
- package/dist/mask/index.d.ts +3 -0
- package/dist/mask/index.js +18 -0
- package/package.json +6 -1
package/CHANGE_LOG.md
CHANGED
|
@@ -1,5 +1,87 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## Unreleased — fetch dispatcher bugfix (2026-07-22)
|
|
4
|
+
|
|
5
|
+
**NOTE (extractor_asx build session):** found and fixed while wiring
|
|
6
|
+
`extractor_asx`'s live dividends-extraction smoke test. Left un-numbered
|
|
7
|
+
since `1.6.0` above already has substantial unrelated in-progress work
|
|
8
|
+
staged in this working tree — fold into `1.6.0` or ship as `1.6.1`,
|
|
9
|
+
whichever fits how you're cutting the release.
|
|
10
|
+
|
|
11
|
+
### Fixed: `hazo_secure/fetch` — `createSecureDispatcher`'s DNS lookup broke EVERY real fetch
|
|
12
|
+
|
|
13
|
+
- `src/fetch/dispatcher.ts`'s custom undici `Agent` `connect.lookup` ignored
|
|
14
|
+
the `options` undici actually passes (`{hints, all:true}` on every real
|
|
15
|
+
request — confirmed by logging it), always calling back with a single
|
|
16
|
+
`(address, family)` pair regardless. Node's `net` connect logic expects an
|
|
17
|
+
array of `{address,family}` records when `all:true` was requested; getting
|
|
18
|
+
a bare string instead threw `TypeError [ERR_INVALID_IP_ADDRESS]: Invalid
|
|
19
|
+
IP address: undefined` deep inside `net`, which undici's `fetch` wrapped
|
|
20
|
+
as an opaque `TypeError: fetch failed` with the real cause hidden in
|
|
21
|
+
`.cause`. **This broke every real HTTPS fetch through `safeFetch`** —
|
|
22
|
+
reproduced against multiple unrelated hosts, not specific to one site.
|
|
23
|
+
- Fix: `lookup` now always resolves via `dns.lookup(hostname, {all:true},
|
|
24
|
+
cb)` internally, validates (SSRF-checks) **every** returned address (not
|
|
25
|
+
just the first — a multi-record response could mix a public and a
|
|
26
|
+
private/rebound IP), then reshapes the result to match what the caller
|
|
27
|
+
actually asked for (single pair vs array) via a new exported pure
|
|
28
|
+
function, `shapeLookupResult`, added specifically so this logic is
|
|
29
|
+
unit-testable without mocking `node:dns` (this package's `test/` has no
|
|
30
|
+
existing DNS-mocking seam).
|
|
31
|
+
- New tests in `test/fetch/dispatcher.test.ts` (`describe('shapeLookupResult', ...)`)
|
|
32
|
+
cover the `all:true`/`all:false` shaping and confirm a private IP anywhere
|
|
33
|
+
in a multi-record response is caught, not just the first record.
|
|
34
|
+
- Verified against the real failing case: `safeFetch('https://www.macquarie.com/...')`
|
|
35
|
+
went from throwing `fetch failed` to a real `200` with the full page body.
|
|
36
|
+
|
|
37
|
+
## 1.6.0 — 2026-07-22
|
|
38
|
+
|
|
39
|
+
### New: `hazo_secure/mask/display` — client-safe display masks
|
|
40
|
+
|
|
41
|
+
- **`maskTFN` / `maskABN` moved to a new `hazo_secure/mask/display` subpath**
|
|
42
|
+
(`src/mask/display.ts`), a pure module with **no Node built-in imports**.
|
|
43
|
+
`hazo_secure/mask` (`src/mask/index.ts`) statically imports `node:crypto`
|
|
44
|
+
and `node:module` (`createRequire`, for the lazy `wink-nlp` load in
|
|
45
|
+
`redact_pii`), so it cannot be bundled for the browser. Consumers that only
|
|
46
|
+
need the display masks in client/browser code must import from
|
|
47
|
+
`hazo_secure/mask/display`.
|
|
48
|
+
- **Back-compat:** `hazo_secure/mask` still re-exports `maskTFN`/`maskABN`
|
|
49
|
+
(via `export { maskTFN, maskABN } from './display'`), so existing
|
|
50
|
+
server-side imports are unchanged. No API surface removed.
|
|
51
|
+
|
|
52
|
+
## 1.5.0 — 2026-07-21
|
|
53
|
+
|
|
54
|
+
### New: `hazo_secure/mask` — `maskTFN` / `maskABN` display masks
|
|
55
|
+
|
|
56
|
+
- **`maskTFN(tfn)`** — masks an Australian Tax File Number for display,
|
|
57
|
+
revealing only the last 3 digits (`"***-***-789"`). Strips whitespace
|
|
58
|
+
before masking so both `"123456789"` and `"123 456 789"` normalise the
|
|
59
|
+
same way. Returns `"***"` for inputs too short to safely reveal 3 digits,
|
|
60
|
+
`""` for null/undefined/empty.
|
|
61
|
+
- **`maskABN(abn)`** — same shape for Australian Business Numbers
|
|
62
|
+
(`"**-***-***-901"`).
|
|
63
|
+
- Deliberately not added to `BUILTIN_TRANSFORMS` — these take no `maskKey`
|
|
64
|
+
and aren't deterministic-by-key like the existing DB-anonymisation
|
|
65
|
+
transforms; they're pure display formatting. Ported from Hazodocs
|
|
66
|
+
`lib/formatting.ts` (`maskTFN`/`maskABN`, previously app-local per D-011).
|
|
67
|
+
|
|
68
|
+
### New: `hazo_secure/crypto` — `randomCode` / `randomToken` / `hashToken` / `verifyToken`
|
|
69
|
+
|
|
70
|
+
- **`randomCode(length = 16)`** — cryptographically secure code from a
|
|
71
|
+
human-readable alphabet (`[A-Z2-9]`, excludes ambiguous `0`/`O`/`1`/`I`/`L`).
|
|
72
|
+
Throws for `length < 8` (inadequate entropy).
|
|
73
|
+
- **`randomToken(bytes = 32)`** — cryptographically secure base64url token
|
|
74
|
+
(43 chars at the default byte length).
|
|
75
|
+
- **`hashToken(raw)`** — scrypt-hashes a raw token under a random 16-byte
|
|
76
|
+
salt for at-rest storage; returns `"<saltHex>:<hashHex>"`.
|
|
77
|
+
- **`verifyToken(raw, stored)`** — recomputes the scrypt hash under the
|
|
78
|
+
embedded salt and compares in constant time via `timingSafeEqual`. Fails
|
|
79
|
+
closed (`false`, never throws) for a malformed `stored` value.
|
|
80
|
+
- Added as a sibling module (`src/crypto/tokens.ts`) alongside the existing
|
|
81
|
+
`signHmac`/`verifyHmac` primitives in the same `./crypto` subpath — no new
|
|
82
|
+
subpath export. Ported from Hazodocs `lib/security/random.ts` and
|
|
83
|
+
`lib/invitations/tokens.ts`.
|
|
84
|
+
|
|
3
85
|
## 1.4.0 — 2026-07-12
|
|
4
86
|
|
|
5
87
|
### New: `hazo_secure/crypto` — generic HMAC sign/verify primitive
|
package/README.md
CHANGED
|
@@ -93,6 +93,33 @@ gdpr.registerExporter({
|
|
|
93
93
|
});
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
+
## New in 1.5.0
|
|
97
|
+
|
|
98
|
+
### `hazo_secure/mask` — `maskTFN` / `maskABN` display masks
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { maskTFN, maskABN } from "hazo_secure/mask";
|
|
102
|
+
|
|
103
|
+
maskTFN("123 456 789"); // "***-***-789"
|
|
104
|
+
maskABN("12 345 678 901"); // "**-***-***-901"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Pure display formatting for Australian Tax File Numbers / ABNs — reveals only the last 3 digits, tolerant of space-formatted input, `""` for null/undefined/empty. Not part of `BUILTIN_TRANSFORMS` (no `maskKey`, not a DB-anonymisation transform).
|
|
108
|
+
|
|
109
|
+
### `hazo_secure/crypto` — `randomCode` / `randomToken` / `hashToken` / `verifyToken`
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { randomCode, randomToken, hashToken, verifyToken } from "hazo_secure/crypto";
|
|
113
|
+
|
|
114
|
+
const code = randomCode(); // "K7XQ9M2Z4RHY8VNC" — human-readable, no 0/O/1/I/L
|
|
115
|
+
const token = randomToken(); // base64url, 32 random bytes by default
|
|
116
|
+
|
|
117
|
+
const stored = await hashToken(token); // "<saltHex>:<hashHex>" — store this
|
|
118
|
+
const ok = await verifyToken(token, stored); // true
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`hashToken`/`verifyToken` use scrypt with a random per-token salt; `verifyToken` fails closed (`false`, never throws) for a malformed stored value.
|
|
122
|
+
|
|
96
123
|
## New in 1.0.0
|
|
97
124
|
|
|
98
125
|
### `hazo_secure/ratelimit` — ConnectRateLimitStore (token bucket)
|
package/dist/crypto/index.d.ts
CHANGED
|
@@ -55,6 +55,51 @@ type HmacVerifyResult = {
|
|
|
55
55
|
*/
|
|
56
56
|
declare function verifyHmac(token: string, secret: string): HmacVerifyResult;
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Generate a cryptographically secure random code using a human-readable
|
|
60
|
+
* base32-ish alphabet. Excludes visually ambiguous characters (`0`/`O`,
|
|
61
|
+
* `1`/`I`/`L`) to minimize transcription errors — suitable for codes a
|
|
62
|
+
* person reads aloud or copies from a screen/print-out (invitation codes,
|
|
63
|
+
* short-lived verification codes, etc.).
|
|
64
|
+
*
|
|
65
|
+
* @param length - desired code length (default: 16 chars)
|
|
66
|
+
* @throws {Error} if `length < 8` (inadequate entropy)
|
|
67
|
+
* @returns a string of `length` characters drawn from `[A-Z2-9]` excluding `0`/`O`/`1`/`I`/`L`
|
|
68
|
+
*/
|
|
69
|
+
declare function randomCode(length?: number): string;
|
|
70
|
+
/**
|
|
71
|
+
* Generate a cryptographically secure random token in base64url encoding.
|
|
72
|
+
* Suitable for session tokens, API keys, password-reset tokens, and
|
|
73
|
+
* invitation tokens.
|
|
74
|
+
*
|
|
75
|
+
* @param bytes - number of random bytes (default: 32, resulting in 43 base64url chars)
|
|
76
|
+
* @returns a base64url string (no `+`, `/`, or `=` padding)
|
|
77
|
+
*/
|
|
78
|
+
declare function randomToken(bytes?: number): string;
|
|
79
|
+
/**
|
|
80
|
+
* Hash a raw token for at-rest storage using scrypt with a random 16-byte
|
|
81
|
+
* salt. The returned string embeds the salt so `verifyToken` doesn't need it
|
|
82
|
+
* supplied separately — store the returned string verbatim (e.g. in a
|
|
83
|
+
* `token_hash` column) and discard the raw token.
|
|
84
|
+
*
|
|
85
|
+
* Format: `<saltHex>:<hashHex>` (scrypt output is 64 bytes / 128 hex chars).
|
|
86
|
+
*
|
|
87
|
+
* @param raw - the plaintext token to hash (e.g. output of `randomToken`)
|
|
88
|
+
* @returns `"<saltHex>:<hashHex>"`
|
|
89
|
+
*/
|
|
90
|
+
declare function hashToken(raw: string): Promise<string>;
|
|
91
|
+
/**
|
|
92
|
+
* Verify a raw token against a hash produced by `hashToken`. Recomputes the
|
|
93
|
+
* scrypt hash under the embedded salt and compares in constant time via
|
|
94
|
+
* `timingSafeEqual`. Fails closed — returns `false` (never throws) for a
|
|
95
|
+
* malformed `stored` value (wrong shape, non-hex segments, etc.).
|
|
96
|
+
*
|
|
97
|
+
* @param raw - the plaintext token supplied by the caller
|
|
98
|
+
* @param stored - the `"<saltHex>:<hashHex>"` string previously returned by `hashToken`
|
|
99
|
+
* @returns `true` if `raw` hashes to `stored` under its embedded salt, else `false`
|
|
100
|
+
*/
|
|
101
|
+
declare function verifyToken(raw: string, stored: string): Promise<boolean>;
|
|
102
|
+
|
|
58
103
|
/**
|
|
59
104
|
* Source of symmetric keys for `encryptField` / `decryptField`. The current
|
|
60
105
|
* key is used for encryption; `byId` resolves any key (current or rotated-out)
|
|
@@ -201,4 +246,4 @@ declare class LookupKeyProvider implements KeyProvider {
|
|
|
201
246
|
* Convention: `<table>:<entityId>:<field>` — collision-free across tables. */
|
|
202
247
|
declare function aadFor(table: string, entityId: string, field: string): string;
|
|
203
248
|
|
|
204
|
-
export { CryptoError, type CryptoErrorCode, type EncryptedField, EnvKeyProvider, type HmacVerifyResult, HttpKeyProvider, type HttpKeyProviderOptions, type KeyProvider, LookupKeyProvider, StaticKeyProvider, aadFor, decryptField, encryptField, signHmac, verifyHmac };
|
|
249
|
+
export { CryptoError, type CryptoErrorCode, type EncryptedField, EnvKeyProvider, type HmacVerifyResult, HttpKeyProvider, type HttpKeyProviderOptions, type KeyProvider, LookupKeyProvider, StaticKeyProvider, aadFor, decryptField, encryptField, hashToken, randomCode, randomToken, signHmac, verifyHmac, verifyToken };
|
package/dist/crypto/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/crypto/index.ts
|
|
2
|
-
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
|
|
2
|
+
import { createCipheriv, createDecipheriv, randomBytes as randomBytes2 } from "crypto";
|
|
3
3
|
import { HazoError } from "hazo_core";
|
|
4
4
|
|
|
5
5
|
// src/crypto/provider-http.ts
|
|
@@ -128,6 +128,38 @@ function verifyHmac(token, secret) {
|
|
|
128
128
|
return { valid: true, payload };
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
// src/crypto/tokens.ts
|
|
132
|
+
import { randomBytes, randomInt, scryptSync, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
133
|
+
var CODE_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
|
|
134
|
+
var SCRYPT_KEY_LEN = 64;
|
|
135
|
+
function randomCode(length = 16) {
|
|
136
|
+
if (length < 8) {
|
|
137
|
+
throw new Error(`randomCode length must be >= 8 for adequate entropy (got ${length})`);
|
|
138
|
+
}
|
|
139
|
+
let out = "";
|
|
140
|
+
for (let i = 0; i < length; i++) {
|
|
141
|
+
out += CODE_ALPHABET[randomInt(0, CODE_ALPHABET.length)];
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
function randomToken(bytes = 32) {
|
|
146
|
+
return randomBytes(bytes).toString("base64url");
|
|
147
|
+
}
|
|
148
|
+
async function hashToken(raw) {
|
|
149
|
+
const salt = randomBytes(16);
|
|
150
|
+
const hash = scryptSync(raw, salt, SCRYPT_KEY_LEN);
|
|
151
|
+
return `${salt.toString("hex")}:${hash.toString("hex")}`;
|
|
152
|
+
}
|
|
153
|
+
async function verifyToken(raw, stored) {
|
|
154
|
+
const [saltHex, hashHex] = stored.split(":");
|
|
155
|
+
if (!saltHex || !hashHex) return false;
|
|
156
|
+
const salt = Buffer.from(saltHex, "hex");
|
|
157
|
+
const expected = Buffer.from(hashHex, "hex");
|
|
158
|
+
if (salt.length === 0 || expected.length === 0) return false;
|
|
159
|
+
const candidate = scryptSync(raw, salt, SCRYPT_KEY_LEN);
|
|
160
|
+
return candidate.length === expected.length && timingSafeEqual2(candidate, expected);
|
|
161
|
+
}
|
|
162
|
+
|
|
131
163
|
// src/crypto/index.ts
|
|
132
164
|
var CryptoError = class extends HazoError {
|
|
133
165
|
constructor(message, code) {
|
|
@@ -159,7 +191,7 @@ async function encryptField(plaintext, opts) {
|
|
|
159
191
|
"invalid_key"
|
|
160
192
|
);
|
|
161
193
|
}
|
|
162
|
-
const iv =
|
|
194
|
+
const iv = randomBytes2(IV_LEN);
|
|
163
195
|
const cipher = createCipheriv(ALG, key, iv, { authTagLength: TAG_LEN });
|
|
164
196
|
if (opts.aad) cipher.setAAD(Buffer.from(opts.aad, "utf8"));
|
|
165
197
|
const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
@@ -317,6 +349,10 @@ export {
|
|
|
317
349
|
aadFor,
|
|
318
350
|
decryptField,
|
|
319
351
|
encryptField,
|
|
352
|
+
hashToken,
|
|
353
|
+
randomCode,
|
|
354
|
+
randomToken,
|
|
320
355
|
signHmac,
|
|
321
|
-
verifyHmac
|
|
356
|
+
verifyHmac,
|
|
357
|
+
verifyToken
|
|
322
358
|
};
|
package/dist/fetch/index.d.ts
CHANGED
|
@@ -16,6 +16,23 @@ declare function validateConnectIp(ip: string): void;
|
|
|
16
16
|
* record flips after the pre-flight check, the IP is re-validated here.
|
|
17
17
|
*
|
|
18
18
|
* Node 18+ ships undici as a built-in; we pin an explicit dep for stability.
|
|
19
|
+
*
|
|
20
|
+
* BUGFIX (2026-07-22): undici's Agent invokes this `lookup` with
|
|
21
|
+
* `{hints, all:true}` on every real request (confirmed by logging the actual
|
|
22
|
+
* options undici passes) — the previous implementation ignored `_options`
|
|
23
|
+
* entirely and always called back with a single `(address, family)` pair.
|
|
24
|
+
* When `net`'s internal connect logic expects an array (because it asked for
|
|
25
|
+
* `all:true`) but gets a bare string instead, it throws
|
|
26
|
+
* `TypeError [ERR_INVALID_IP_ADDRESS]: Invalid IP address: undefined` —
|
|
27
|
+
* which undici's fetch wraps as a generic `TypeError: fetch failed` with no
|
|
28
|
+
* indication of the real cause. This broke EVERY real HTTPS fetch through
|
|
29
|
+
* this dispatcher (reproduced against multiple unrelated hosts, not
|
|
30
|
+
* something specific to one site) — pre-flight `validateUrl`'s DNS check
|
|
31
|
+
* passing gave false confidence that fetches would actually succeed.
|
|
32
|
+
* `dns.lookup` is now always called with `{all:true}` internally regardless
|
|
33
|
+
* of what the caller asked for, so every resolved address gets SSRF-checked
|
|
34
|
+
* (see `shapeLookupResult`'s doc comment), then reshaped to match what the
|
|
35
|
+
* caller actually requested.
|
|
19
36
|
*/
|
|
20
37
|
declare function createSecureDispatcher(): Agent;
|
|
21
38
|
|
package/dist/fetch/index.js
CHANGED
|
@@ -86,18 +86,30 @@ function validateConnectIp(ip) {
|
|
|
86
86
|
throw err;
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
+
function shapeLookupResult(records, wantsAll) {
|
|
90
|
+
for (const record of records) validateConnectIp(record.address);
|
|
91
|
+
if (wantsAll) {
|
|
92
|
+
return { address: records.map((r) => ({ address: r.address, family: r.family })), family: 0 };
|
|
93
|
+
}
|
|
94
|
+
const first = records[0];
|
|
95
|
+
return { address: first.address, family: first.family };
|
|
96
|
+
}
|
|
89
97
|
function createSecureDispatcher() {
|
|
90
98
|
return new Agent({
|
|
91
99
|
connect: {
|
|
92
|
-
lookup(hostname,
|
|
93
|
-
|
|
94
|
-
|
|
100
|
+
lookup(hostname, options, callback) {
|
|
101
|
+
const wantsAll = typeof options === "object" && options !== null && options.all === true;
|
|
102
|
+
dns.lookup(hostname, { all: true }, (err, records) => {
|
|
103
|
+
if (err) return callback(err, wantsAll ? [] : "", 4);
|
|
104
|
+
if (records.length === 0) {
|
|
105
|
+
return callback(new Error(`DNS lookup for ${hostname} returned no records`), wantsAll ? [] : "", 4);
|
|
106
|
+
}
|
|
95
107
|
try {
|
|
96
|
-
|
|
108
|
+
const shaped = shapeLookupResult(records, wantsAll);
|
|
109
|
+
callback(null, shaped.address, shaped.family);
|
|
97
110
|
} catch (ssrfErr) {
|
|
98
|
-
|
|
111
|
+
callback(ssrfErr, wantsAll ? [] : "", 4);
|
|
99
112
|
}
|
|
100
|
-
callback(null, address, family);
|
|
101
113
|
});
|
|
102
114
|
}
|
|
103
115
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* maskTFN — mask an Australian Tax File Number for display, revealing only
|
|
3
|
+
* the last 3 digits.
|
|
4
|
+
*
|
|
5
|
+
* TFNs are commonly rendered/entered as `123 456 789` or `123456789`;
|
|
6
|
+
* whitespace is stripped before masking so both forms produce the same
|
|
7
|
+
* output. Returns `"***"` for inputs too short to safely reveal 3 digits,
|
|
8
|
+
* and `""` for null/undefined/empty input.
|
|
9
|
+
*
|
|
10
|
+
* @param tfn - raw or space-formatted TFN, or null/undefined
|
|
11
|
+
* @returns a `"***-***-789"`-shaped string, `"***"`, or `""`
|
|
12
|
+
*/
|
|
13
|
+
declare function maskTFN(tfn: string | null | undefined): string;
|
|
14
|
+
/**
|
|
15
|
+
* maskABN — mask an Australian Business Number for display, revealing only
|
|
16
|
+
* the last 3 digits.
|
|
17
|
+
*
|
|
18
|
+
* ABNs are commonly rendered/entered as `12 345 678 901` or `12345678901`;
|
|
19
|
+
* whitespace is stripped before masking so both forms produce the same
|
|
20
|
+
* output. Returns `"***"` for inputs too short to safely reveal 3 digits,
|
|
21
|
+
* and `""` for null/undefined/empty input.
|
|
22
|
+
*
|
|
23
|
+
* @param abn - raw or space-formatted ABN, or null/undefined
|
|
24
|
+
* @returns a `"**-***-***-901"`-shaped string, `"***"`, or `""`
|
|
25
|
+
*/
|
|
26
|
+
declare function maskABN(abn: string | null | undefined): string;
|
|
27
|
+
|
|
28
|
+
export { maskABN, maskTFN };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// src/mask/display.ts
|
|
2
|
+
function maskTFN(tfn) {
|
|
3
|
+
if (!tfn) return "";
|
|
4
|
+
const cleaned = tfn.replace(/\s/g, "");
|
|
5
|
+
if (cleaned.length < 3) return "***";
|
|
6
|
+
return `***-***-${cleaned.slice(-3)}`;
|
|
7
|
+
}
|
|
8
|
+
function maskABN(abn) {
|
|
9
|
+
if (!abn) return "";
|
|
10
|
+
const cleaned = abn.replace(/\s/g, "");
|
|
11
|
+
if (cleaned.length < 3) return "***";
|
|
12
|
+
return `**-***-***-${cleaned.slice(-3)}`;
|
|
13
|
+
}
|
|
14
|
+
export {
|
|
15
|
+
maskABN,
|
|
16
|
+
maskTFN
|
|
17
|
+
};
|
package/dist/mask/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { maskABN, maskTFN } from './display.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* mask_email — replace with deterministic masked email.
|
|
3
5
|
* alice@example.com → a3f9b12c1d2e@masked.example
|
|
@@ -34,6 +36,7 @@ declare function drop(_value: unknown, _col: string, _maskKey: string): unknown;
|
|
|
34
36
|
* nullify — replace with null (less aggressive than drop; preserves column semantics).
|
|
35
37
|
*/
|
|
36
38
|
declare function nullify(_value: unknown, _col: string, _maskKey: string): unknown;
|
|
39
|
+
|
|
37
40
|
/**
|
|
38
41
|
* redact_pii — NER-based PII redaction (wink-nlp lite model).
|
|
39
42
|
* Detects named entities (dates, cardinals, etc.) and replaces them with [REDACTED_TYPE].
|
package/dist/mask/index.js
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
// src/mask/index.ts
|
|
2
2
|
import { createHmac } from "crypto";
|
|
3
|
+
|
|
4
|
+
// src/mask/display.ts
|
|
5
|
+
function maskTFN(tfn) {
|
|
6
|
+
if (!tfn) return "";
|
|
7
|
+
const cleaned = tfn.replace(/\s/g, "");
|
|
8
|
+
if (cleaned.length < 3) return "***";
|
|
9
|
+
return `***-***-${cleaned.slice(-3)}`;
|
|
10
|
+
}
|
|
11
|
+
function maskABN(abn) {
|
|
12
|
+
if (!abn) return "";
|
|
13
|
+
const cleaned = abn.replace(/\s/g, "");
|
|
14
|
+
if (cleaned.length < 3) return "***";
|
|
15
|
+
return `**-***-***-${cleaned.slice(-3)}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/mask/index.ts
|
|
3
19
|
import { createRequire } from "module";
|
|
4
20
|
function hmacHex(value, maskKey, context) {
|
|
5
21
|
return createHmac("sha256", maskKey).update(`${context}:${value}`).digest("hex");
|
|
@@ -107,6 +123,8 @@ export {
|
|
|
107
123
|
fake_name,
|
|
108
124
|
hash,
|
|
109
125
|
jitter_date,
|
|
126
|
+
maskABN,
|
|
127
|
+
maskTFN,
|
|
110
128
|
mask_email,
|
|
111
129
|
mask_phone,
|
|
112
130
|
nullify,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hazo_secure",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.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",
|
|
@@ -42,6 +42,11 @@
|
|
|
42
42
|
"import": "./dist/mask/index.js",
|
|
43
43
|
"default": "./dist/mask/index.js"
|
|
44
44
|
},
|
|
45
|
+
"./mask/display": {
|
|
46
|
+
"types": "./dist/mask/display.d.ts",
|
|
47
|
+
"import": "./dist/mask/display.js",
|
|
48
|
+
"default": "./dist/mask/display.js"
|
|
49
|
+
},
|
|
45
50
|
"./secrets": {
|
|
46
51
|
"types": "./dist/secrets/index.d.ts",
|
|
47
52
|
"import": "./dist/secrets/index.js",
|