@starweave-lab/masker
v0.0.1
Published
Lightweight, fast, type-safe data masking library for Node.js and Bun
Maintainers
Readme
masker
面向 Node.js、Bun 与现代 JS 运行时的轻量、快速、类型安全数据脱敏工具库。
- 零运行时依赖
- ESM + TypeScript 类型声明
- Grapheme 感知(中文 / 日本語 / 한국어 / emoji)
- Schema / 路径脱敏,规则可预编译,适合热路径 Logger
- 绝不修改输入对象(copy-on-write)
文档站: https://yibird.github.io/masker/
本地预览:npm run docs:dev · 构建:npm run docs:build
import { maskEmail, maskPhone, maskGeneric } from 'masker';
maskEmail('[email protected]'); // 'z******[email protected]'
maskPhone('13812345678'); // '138****5678'
maskGeneric('1234567890', { keepStart: 2, keepEnd: 2 }); // '12******90'目录
- 简介
- 安装
- Node.js 用法
- Bun 用法
- 快速开始
- Phone
- Name
- Address
- Credit card
- JWT
- IP
- URL
- Generic
- Object
- Schema / 路径
- Logger
- 自定义策略
- 性能
- 安全说明
- API 参考
- FAQ
1. 简介
masker 是一个小型工具库,用于在应用代码、HTTP 响应、审计日志与 Logger 链路中脱敏 PII 与密钥。
设计目标:
| 目标 | 方式 |
| -------- | -------------------------------------- |
| 简单 | maskEmail(value) 零配置可用 |
| 可配置 | 每种脱敏器都接受语义化 options |
| 可组合 | maskObject / createMasker 组合使用 |
| 类型安全 | 公开 API 无 any |
| 无副作用 | 输入永远不会被修改 |
| 可预测 | 相同输入 + 相同 options → 相同输出 |
| 快 | 常量正则、ASCII 快路径、预编译 schema |
它不校验业务数据(卡号 Luhn 校验可单独使用 isLuhnValid;JWT 永不解码)。
2. 安装
npm install masker
# 或
pnpm add masker
# 或
yarn add masker
# 或
bun add masker要求 Node.js ≥ 18(或 Bun)。包为 ESM-only("type": "module")。
3. Node.js 用法
import { maskPhone, maskObject } from 'masker';使用 tsdown 构建 → dist/index.js + dist/index.d.ts。
4. Bun 用法
import { maskUrl, createMasker } from 'masker';不使用任何 Node 专属 API(从不 import node:*),Bun 可直接使用。
5. 快速开始
import {
maskEmail,
maskPhone,
maskName,
maskAddress,
maskCreditCard,
maskIdCard,
maskBankAccount,
maskMac,
maskVehicle,
maskJwt,
maskIp,
maskUrl,
maskGeneric,
maskObject,
maskLog,
createMasker,
} from 'masker';
maskEmail('[email protected]'); // z******[email protected]
maskPhone('+8613812345678'); // +86138****5678
maskName('张三丰'); // 张*丰
maskAddress('北京市朝阳区xxx街道xxx号'); // 北京市朝阳区*********
maskCreditCard('4111 1111 1111 1111'); // **** **** **** 1111
maskIdCard('110101199001011234'); // 110101********1234
maskBankAccount('6222021234567890123'); // ***************0123
maskMac('00:1A:2B:3C:4D:5E'); // 00:1A:2B:*:*:*
maskVehicle('京A12345'); // 京A*****
maskJwt('eyJhbGciOiJ9.eyJzdWIiOiJ9.sig'); // ********.********.********
maskIp('192.168.1.100'); // 192.168.*.*
maskUrl('https://u:[email protected]?token=s'); // credentials + token masked
maskGeneric('sensitive', { keepStart: 2, keepEnd: 2 }); // se******ve6. Email
保留 domain(含子域);默认遮盖 local part,并保留 +tag。
maskEmail('[email protected]'); // z******[email protected]
maskEmail('[email protected]'); // *@example.com
maskEmail('[email protected]'); // a*@example.com
maskEmail('[email protected]'); // z*******[email protected]
maskEmail('[email protected]'); // u**[email protected]
maskEmail('[email protected]'); // u**[email protected]
maskEmail('[email protected]', { mask: '•' }); // z••••••[email protected]
maskEmail('[email protected]', { keepStart: 2, keepEnd: 3 });
// zh***[email protected]
maskEmail('[email protected]', { maskTag: true }); // u******[email protected]错误行为: 缺少 @、空 local/domain 或空输入 → 原样返回。
7. Phone
保留分隔符(空格、-、())、可选的 + 国家码,只遮盖中间数字。
maskPhone('13812345678'); // 138****5678
maskPhone('+8613812345678'); // +86138****5678
maskPhone('+1 415 555 1234'); // +1 415 *** 1234
maskPhone('(415) 555-1234'); // (415) ***-1234
maskPhone('13812345678', { keepStart: 3, keepEnd: 4, mask: '*' });
// 138****5678国内号段默认:keepStart: 3、keepEnd: 4。国家码按内置 E.164 列表最长优先匹配。
错误行为: 无数字 / 空 → 原样返回。极短数字串会 clamp,尽量至少遮盖 1 位。
8. Name
按 token 处理中文与拉丁姓名(保留空白)。
maskName('张三'); // 张*
maskName('张三丰'); // 张*丰
maskName('欧阳娜娜'); // 欧**娜
maskName('John'); // J**n
maskName('John Smith'); // J**n S***h
maskName('Alice Johnson'); // A***e J*****n规则:保留首 grapheme;token ≥ 3 个 grapheme 时保留末尾。
9. Address
默认:保留中文行政区划前缀(省/市/区/县…),遮盖其余;非中文 / 未匹配时回退为前 4 个 grapheme。
maskAddress('北京市朝阳区xxx街道xxx号'); // 北京市朝阳区*********
maskAddress('123 Main Street', { keepStart: 4 });
maskAddress(addr, { mask: '•', maskLength: 4 });
maskAddress(addr, { disableAutoPrefix: true, keepStart: 0 }); // 全遮盖10. Credit card
保留分隔符(空格 / -)。按品牌感知的默认值:
| 形态 | 默认 |
| ------------- | -------------------------------- |
| 16 位风格 | 保留末 4 → **** **** **** 1111 |
| Amex(15 位) | 保留前 6 + 后 5 |
maskCreditCard('4111111111111111'); // ************1111
maskCreditCard('4111 1111 1111 1111'); // **** **** **** 1111
maskCreditCard('4111-1111-1111-1111'); // ****-****-****-1111
maskCreditCard('378282246310005'); // 378282****10005校验与脱敏解耦 — 脱敏从不要求合法 PAN:
import { isLuhnValid } from 'masker';
isLuhnValid('4111111111111111'); // true
isLuhnValid('4111111111111112'); // false
maskCreditCard('4111111111111112'); // 仍会遮盖: ************1112错误行为: 数字少于 12 位 → 原样返回。
10a. ID card
识别 18 位身份证(末位可为 X)与旧版 15 位。默认保留区划前 6 + 后 4。不做校验位验证。
maskIdCard('110101199001011234'); // 110101********1234
maskIdCard('110101199001011234', { keepStart: 0, keepEnd: 0 }); // ******************10b. Bank account
与信用卡分离:长度更宽、无 PAN/Amex 规则、无 Luhn。支持 IBAN 风格字母数字账号。默认保留末 4。
maskBankAccount('6222021234567890123'); // ***************0123
maskBankAccount('6222 0212 3456 7890'); // **** **** **** 7890
maskBankAccount('GB29NABC60161331926819'); // ******************681910c. MAC address
支持冒号 / 横线 / Cisco 点分 / 裸 12 位十六进制。默认保留前 3 个八位组(OUI)。
maskMac('00:1A:2B:3C:4D:5E'); // 00:1A:2B:*:*:*
maskMac('00-1a-2b-3c-4d-5e'); // 00-1a-2b-*-*-*
maskMac('aabb.ccdd.eeff'); // aabb.cc**.****10d. Vehicle(车牌 / VIN)
maskVehicle 自动识别 VIN(17 位)与车牌。默认:车牌保留前 2;VIN 保留前 3 + 后 4。
maskVehicle('京A12345'); // 京A*****
maskVehicle('1HGCM82633A004352'); // 1HG**********4352
maskLicensePlate('沪AD12345'); // 沪A******
maskVin('1HGCM82633A004352'); // 1HG**********435211. JWT
仅做结构切分 — 从不 base64 解码、从不验签、从不读 claims。
maskJwt(token);
// ********.********.********
maskJwt(token, { header: 'keep' });
// eyJhbGciOiJIUzI1NiJ9.********.********
maskJwt(token, { keepSegmentChars: 3 });
// eyJ********.********.********
maskJwt(token, { mask: '#', maskSegmentLength: 3 });
// ###.###.###| 选项 | 默认 | 含义 |
| ------------------- | -------- | -------------------------- |
| header | 'mask' | 'mask' \| 'keep' |
| payload | 'mask' | 仅在你能接受风险时保留 |
| signature | 'mask' | 通常保持遮盖 |
| keepSegmentChars | 0 | 每段保留的前缀长度 |
| maskSegmentLength | 8 | 固定遮盖长度(隐藏段长度) |
错误行为: 非恰好三段且 header/payload 非空 → 原样返回。
12. IP
IPv4 与 IPv6 使用独立策略。
maskIp('192.168.1.100'); // 192.168.*.*
maskIp('192.168.1.100', { ipv4: { keepStart: 3 } }); // 192.168.1.*
maskIp('2001:db8:85a3:0:0:8a2e:370:7334'); // 2001:db8:*:*:*:*:*:*
maskIp('2001:db8::1'); // 2001:db8::*
maskIp(ip, { ipv6: { keepGroups: 4 } });错误行为: 非法 IPv4/IPv6 / 空 → 原样返回。
13. URL
基于标准 URL / URLSearchParams(无巨型正则)。
默认:
- 凭据遮盖
- hostname + path 保留
- 敏感 query 值遮盖(
token、password、secret、api_key等) - 其它 query 保留(
page=1) - fragment 遮盖
maskUrl('https://user:[email protected]/path');
// https://u***:******@example.com/path
maskUrl('https://example.com/api?token=secret&page=1');
// https://example.com/api?token=******&page=1
maskUrl(url, { query: ['token', 'password', 'secret'] });
maskUrl(url, { maskAllQuery: true });
maskUrl(url, { maskCredentials: false, maskFragment: false });注意:
URL会规范化字符串(编码、默认端口、尾斜杠)。非法 / 相对 URL 原样返回。
14. Generic
maskGeneric('1234567890', { keepStart: 2, keepEnd: 2 }); // 12******90
maskGeneric('sensitive-value'); // ***************
maskGeneric('abc', { mask: '#' }); // ###
maskGeneric('1234567890', { keepStart: 2, keepEnd: 2, maskLength: 3 }); // 12***90Unicode:
- 纯 ASCII → UTF-16 快路径
- 否则 → 可用时使用
Intl.Segmentergrapheme,否则 code point - 支持 中文、日本語、 한국어、emoji(Segmenter 存在时含 ZWJ 序列)
错误行为: 空串 → ''。keepStart + keepEnd ≥ 长度 → 原样返回。
15. Object
深度、非修改脱敏,copy-on-write:未命中子树保持原引用。
const data = {
id: 1,
name: '张三',
phone: '13812345678',
profile: {
address: '北京市朝阳区xxx',
creditCard: '4111111111111111',
},
};
const safe = maskObject(data, {
name: 'name',
phone: 'phone',
'profile.address': 'address',
'profile.creditCard': 'creditCard',
});
data !== safe; // true
data.name === '张三'; // 源数据未被改写maskFields — 路径/schema 精确规则
精确声明哪些路径使用什么 masker,支持 { type, options } 对象写法:
import { maskFields } from 'masker';
maskFields(
{
user: {
email: '[email protected]',
phone: '+14155550123',
},
},
{
'user.email': { type: 'email' },
'user.phone': { type: 'phone' },
},
);
// → user.email: 'j**[email protected]'
// → user.phone: '+1415***0123'规则值也可以是 preset 字符串('email')或自定义函数。通配符与 maskObject 相同。
支持的值:
| 类型 | 行为 |
| ----------------------------- | ---------------------------------------------- |
| plain object | 仅在子节点变化时浅拷贝 |
| array | 仅在元素变化时拷贝 |
| 嵌套 / readonly | 正常处理;冻结对象不会被写入 |
| Date / RegExp / 二进制 | 按引用保留 |
| Map / Set | 仅在条目变化时克隆 |
| null / undefined / 原始值 | 直接返回 |
| 循环引用 | 环边复用原引用;不会死循环 |
| Error | 路径模式下视为原子;maskLog 中会物化(见下) |
空 schema → 返回同一引用。
16. Schema / 路径
路径用 . 分隔。* 匹配一个对象键或数组下标。
maskObject(data, {
email: 'email',
'profile.address': 'address',
'users.*.phone': 'phone',
'users.*.email': maskEmail, // 函数形式
});Preset 名称:email | phone | name | address | creditCard | idCard | bankAccount | mac | vehicle | jwt | ip | url | generic。
非法路径(a..b、空)或未知 preset 在schema 编译期抛出 TypeError — 永不 mid-walk,也永不打印输入值。
重复调用请预编译:
const masker = createMasker({
'users.*.phone': 'phone',
'profile.email': 'email',
});
masker.mask(payload); // schema 在 createMasker() 解析一次17. Logger
字段名规则在任意深度生效(不同于路径 schema):
maskLog(payload, {
fields: {
password: 'generic',
token: 'generic',
email: 'email',
phone: 'phone',
},
});热路径预编译形式:
const masker = createMasker({
fields: {
password: 'generic',
email: 'email',
phone: 'phone',
},
});
logger.info(masker.mask(data));特性:
- 不修改日志对象
- 无
JSON.stringify/parse - 处理嵌套对象、数组、
Map/Set、循环引用 Error物化为{ name, message, stack },使fields.message可用
18. 自定义策略
传入任意 (value: string) => string:
import { maskObject, maskEmail } from 'masker';
const alwaysStars = (v: string) => '*'.repeat(v.length);
maskObject(data, {
email: maskEmail,
secret: alwaysStars,
// 绑定 options
phone: (v: string) => maskPhone(v, { mask: '#' }),
});
ValueMasker类型是逆变的((value: never) => string),因此具体的(value: string) => string可干净赋值而无需断言。
19. 性能
使用的技术:
- 仅模块级常量正则(无每次调用
new RegExp) - grapheme 分段前的 ASCII 快路径
- schema / path / preset 在
createMasker()/ 编译阶段解析 - copy-on-write — 无规则命中时整棵子树跳过
- 数字位单趟扫描(phone / card)
- 原生
URL/URLSearchParams
运行内置基准(需先构建):
npm run bench
# 或
npm run build && node benchmark/bench.mjs
# Bun(构建后):
bun benchmark/bench.mjs诚实说明: 本 README 不引用绝对 ops/s 数字 — 请在你的机器与运行时上跑基准。开发环境未安装 Bun,因此不宣称 Bun 数字。
20. 安全说明
- 生产环境不要在脱敏输出旁同时打印原始输入以便调试。
- 本库抛出的错误类型只含配置信息(错误路径 / preset)— 从不嵌入你的数据值。
- JWT payload 默认遮盖;开启
payload: 'keep'可能暴露 claims(PII、角色)。 - URL 敏感键 query 与 fragment 默认遮盖;请审查自定义
query列表。 - 脱敏不是加密,也不能替代访问控制。
- 请求 / 日志中间件中优先使用
createMasker,使规则错误在启动时暴露,而非每条请求。
21. API 参考
基础脱敏器
| 函数 | 签名 | 非法输入 |
| ------------------ | ------------------------------------------------------------- | ----------------------- |
| maskEmail | (value: string, options?: EmailMaskOptions) => string | 原样返回 |
| maskPhone | (value: string, options?: PhoneMaskOptions) => string | 无数字 → 原样 |
| maskName | (value: string, options?: NameMaskOptions) => string | 空 → '' |
| maskAddress | (value: string, options?: AddressMaskOptions) => string | 空 → '' |
| maskCreditCard | (value: string, options?: CreditCardMaskOptions) => string | <12 位数字 → 原样 |
| maskIdCard | (value: string, options?: IdCardMaskOptions) => string | 非 15/18 位 → 原样 |
| maskBankAccount | (value: string, options?: BankAccountMaskOptions) => string | 过短 → 原样 |
| maskMac | (value: string, options?: MacMaskOptions) => string | 非法 → 原样 |
| maskVehicle | (value: string, options?: VehicleMaskOptions) => string | 未知 → 原样 |
| maskLicensePlate | (value: string, options?: VehicleMaskOptions) => string | 非法 → 原样 |
| maskVin | (value: string, options?: VehicleMaskOptions) => string | 非 17 字符 → 原样 |
| maskJwt | (value: string, options?: JwtMaskOptions) => string | 形态错误 → 原样 |
| maskIp | (value: string, options?: IpMaskOptions) => string | 非法 → 原样 |
| maskUrl | (value: string, options?: UrlMaskOptions) => string | 非法 → 原样 |
| maskGeneric | (value: string, options?: SliceMaskOptions) => string | 空 → '' |
| isLuhnValid | (value: string) => boolean | 非字符串 / 空 → false |
运行时非字符串值(纯 JS 调用方)原样返回 — TypeScript 类型有意只接受 string。
Object / logger
| 函数 | 签名 |
| -------------- | --------------------------------------------------------- |
| maskObject | <T>(value: T, schema: ObjectSchema) => T |
| maskFields | <T>(value: T, fields: ObjectSchema) => T |
| maskLog | <T>(value: T, options?: MaskLogOptions) => T |
| createMasker | (schema: ObjectSchema \| CreateMaskerOptions) => Masker |
Masker:
interface Masker {
mask<T>(value: T): T;
}Option 类型
EmailMaskOptions、PhoneMaskOptions、NameMaskOptions、AddressMaskOptions、CreditCardMaskOptions、IdCardMaskOptions、BankAccountMaskOptions、MacMaskOptions、VehicleKind、VehicleMaskOptions、JwtMaskOptions、IpMaskOptions、Ipv4MaskOptions、Ipv6MaskOptions、UrlMaskOptions、SliceMaskOptions、BaseMaskOptions、ObjectSchema、FieldMap、MaskLogOptions、CreateMaskerOptions、SchemaMasker、MaskFieldConfig、PresetName、ValueMasker、Masker。
抛错条件
| API | 抛出 |
| --------------------------- | -------------------------------------------------------------------------------------------------- |
| maskObject(value, schema) | schema 非对象、路径非法或 preset 未知 → TypeError |
| createMasker(args) | args 为 null/非对象、空路径、未知 preset,或 options 不含 fields 也不含 schema → TypeError |
| 基础脱敏器 | 数据类从不抛错 — 非法数据原样返回 |
22. FAQ
为什么只有 ESM?
双格式 CJS/ESM 会大致加倍打包面(条件导出、interop 边角)。现代 Node(18+)与 Bun 原生加载 ESM;打包器对 sideEffects: false 的 ESM 树摇友好。若有明确需求可再加 CJS。
为什么 maskCreditCard 不校验卡号?
脱敏与校验是不同关注点。需要校验时请显式调用 isLuhnValid。
maskJwt 会解码 payload 吗?
不会。只按 . 切分 header.payload.signature。无 base64、无 JSON。
通配符性能如何?
路径在 createMasker / 首次 maskObject 时解析为 trie。每次对象遍历推进节点引用 — 不会每个键重复 split('.')。
为什么 ValueMasker 类型是 (value: never) => string?
使函数参数逆变,从而 (value: string) => string 可无 as any 赋值,同时仍表达「我接受字符串」。
会修改我的对象吗?
不会。Copy-on-write:只有真正变化的分支才浅拷贝。
循环结构?
支持。遍历器用 Set 记录当前路径;环边返回原引用而不是死循环。
Bun 支持?
库只用标准 JS(URL、Intl.Segmenter、普通对象)。无 node: 导入。开发环境未安装 Bun — 运行时支持是结构保证,而非本仓库执行过的 CI。
CommonJS require('masker')?
不支持(见上)。CJS 中如需使用请 await import('masker')。
开发
npm install
npm run typecheck
npm test
npm run build
npm run lint
npm run format
npm run benchLicense
MIT
