@hammr/normalizer 1.0.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/dist/index.cjs +332 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +263 -0
- package/dist/index.d.ts +263 -0
- package/dist/index.js +303 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
Normalizer: () => Normalizer,
|
|
24
|
+
isSha256: () => isSha256,
|
|
25
|
+
sha256: () => sha256
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/hash.ts
|
|
30
|
+
async function sha256(input) {
|
|
31
|
+
const encoder = new TextEncoder();
|
|
32
|
+
const data = encoder.encode(input);
|
|
33
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
34
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
35
|
+
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
36
|
+
return hashHex;
|
|
37
|
+
}
|
|
38
|
+
function isSha256(value) {
|
|
39
|
+
return /^[a-f0-9]{64}$/.test(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/Normalizer.ts
|
|
43
|
+
var Normalizer = class {
|
|
44
|
+
constructor(options = {}) {
|
|
45
|
+
this.options = {
|
|
46
|
+
defaultCountryCode: options.defaultCountryCode ?? "1",
|
|
47
|
+
hashAddressFields: options.hashAddressFields ?? false
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
51
|
+
* NORMALIZATION METHODS
|
|
52
|
+
* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
|
|
53
|
+
/**
|
|
54
|
+
* Normalize email: lowercase, trim whitespace
|
|
55
|
+
* Returns null if invalid format
|
|
56
|
+
*/
|
|
57
|
+
normalizeEmail(email) {
|
|
58
|
+
if (!email || typeof email !== "string") return null;
|
|
59
|
+
const normalized = email.toLowerCase().trim();
|
|
60
|
+
if (!normalized.includes("@") || normalized.length < 5) return null;
|
|
61
|
+
return normalized;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Normalize phone to E.164 format (+15551234567)
|
|
65
|
+
* Returns null if invalid
|
|
66
|
+
*/
|
|
67
|
+
normalizePhone(phone, countryCode) {
|
|
68
|
+
if (!phone || typeof phone !== "string") return null;
|
|
69
|
+
const code = countryCode ?? this.options.defaultCountryCode;
|
|
70
|
+
let digits = phone.replace(/[^\d+]/g, "");
|
|
71
|
+
if (digits.startsWith("+")) digits = digits.slice(1);
|
|
72
|
+
if (digits.length < 10) return null;
|
|
73
|
+
if (digits.length === 10) digits = code + digits;
|
|
74
|
+
return "+" + digits;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Normalize name: lowercase, trim, remove extra spaces
|
|
78
|
+
* Returns null if empty
|
|
79
|
+
*/
|
|
80
|
+
normalizeName(name) {
|
|
81
|
+
if (!name || typeof name !== "string") return null;
|
|
82
|
+
const normalized = name.toLowerCase().trim().replace(/\s+/g, " ");
|
|
83
|
+
if (normalized.length === 0) return null;
|
|
84
|
+
return normalized;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Normalize gender: single lowercase char (m/f)
|
|
88
|
+
* Accepts: m, male, f, female
|
|
89
|
+
* Returns null if invalid
|
|
90
|
+
*/
|
|
91
|
+
normalizeGender(gender) {
|
|
92
|
+
if (!gender || typeof gender !== "string") return null;
|
|
93
|
+
const g = gender.toLowerCase().trim();
|
|
94
|
+
if (g === "m" || g === "male") return "m";
|
|
95
|
+
if (g === "f" || g === "female") return "f";
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Normalize date of birth: YYYYMMDD format
|
|
100
|
+
* Accepts formats: YYYY-MM-DD, YYYY/MM/DD, YYYY.MM.DD, YYYYMMDD
|
|
101
|
+
* Returns null if invalid
|
|
102
|
+
*/
|
|
103
|
+
normalizeDateOfBirth(dob) {
|
|
104
|
+
if (!dob || typeof dob !== "string") return null;
|
|
105
|
+
const cleaned = dob.replace(/[-\/\.]/g, "");
|
|
106
|
+
if (!/^\d{8}$/.test(cleaned)) return null;
|
|
107
|
+
return cleaned;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Normalize city: lowercase, trim
|
|
111
|
+
*/
|
|
112
|
+
normalizeCity(city) {
|
|
113
|
+
if (!city || typeof city !== "string") return null;
|
|
114
|
+
const normalized = city.toLowerCase().trim();
|
|
115
|
+
if (normalized.length === 0) return null;
|
|
116
|
+
return normalized;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Normalize state/region: lowercase, trim
|
|
120
|
+
*/
|
|
121
|
+
normalizeState(state) {
|
|
122
|
+
if (!state || typeof state !== "string") return null;
|
|
123
|
+
const normalized = state.toLowerCase().trim();
|
|
124
|
+
if (normalized.length === 0) return null;
|
|
125
|
+
return normalized;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Normalize zip/postal code: lowercase, no spaces
|
|
129
|
+
*/
|
|
130
|
+
normalizeZipCode(zip) {
|
|
131
|
+
if (!zip || typeof zip !== "string") return null;
|
|
132
|
+
const normalized = zip.toLowerCase().trim().replace(/\s+/g, "");
|
|
133
|
+
if (normalized.length === 0) return null;
|
|
134
|
+
return normalized;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Normalize country: 2-letter code, lowercase
|
|
138
|
+
* Returns null if not exactly 2 characters
|
|
139
|
+
*/
|
|
140
|
+
normalizeCountry(country) {
|
|
141
|
+
if (!country || typeof country !== "string") return null;
|
|
142
|
+
const normalized = country.toLowerCase().trim();
|
|
143
|
+
if (normalized.length !== 2) return null;
|
|
144
|
+
return normalized;
|
|
145
|
+
}
|
|
146
|
+
/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
147
|
+
* HASH METHODS
|
|
148
|
+
* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
|
|
149
|
+
/**
|
|
150
|
+
* Hash email (normalize then hash)
|
|
151
|
+
*/
|
|
152
|
+
async hashEmail(email) {
|
|
153
|
+
const normalized = this.normalizeEmail(email);
|
|
154
|
+
return normalized ? sha256(normalized) : null;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Hash phone (normalize then hash)
|
|
158
|
+
*/
|
|
159
|
+
async hashPhone(phone, countryCode) {
|
|
160
|
+
const normalized = this.normalizePhone(phone, countryCode);
|
|
161
|
+
return normalized ? sha256(normalized) : null;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Hash name (normalize then hash)
|
|
165
|
+
*/
|
|
166
|
+
async hashName(name) {
|
|
167
|
+
const normalized = this.normalizeName(name);
|
|
168
|
+
return normalized ? sha256(normalized) : null;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Hash gender (normalize then hash)
|
|
172
|
+
*/
|
|
173
|
+
async hashGender(gender) {
|
|
174
|
+
const normalized = this.normalizeGender(gender);
|
|
175
|
+
return normalized ? sha256(normalized) : null;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Hash date of birth (normalize then hash)
|
|
179
|
+
*/
|
|
180
|
+
async hashDateOfBirth(dob) {
|
|
181
|
+
const normalized = this.normalizeDateOfBirth(dob);
|
|
182
|
+
return normalized ? sha256(normalized) : null;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Hash city (normalize then hash)
|
|
186
|
+
*/
|
|
187
|
+
async hashCity(city) {
|
|
188
|
+
const normalized = this.normalizeCity(city);
|
|
189
|
+
return normalized ? sha256(normalized) : null;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Hash state (normalize then hash)
|
|
193
|
+
*/
|
|
194
|
+
async hashState(state) {
|
|
195
|
+
const normalized = this.normalizeState(state);
|
|
196
|
+
return normalized ? sha256(normalized) : null;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Hash zip code (normalize then hash)
|
|
200
|
+
*/
|
|
201
|
+
async hashZipCode(zip) {
|
|
202
|
+
const normalized = this.normalizeZipCode(zip);
|
|
203
|
+
return normalized ? sha256(normalized) : null;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Hash country (normalize then hash)
|
|
207
|
+
*/
|
|
208
|
+
async hashCountry(country) {
|
|
209
|
+
const normalized = this.normalizeCountry(country);
|
|
210
|
+
return normalized ? sha256(normalized) : null;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Hash external ID (trim only, no other normalization)
|
|
214
|
+
*/
|
|
215
|
+
async hashExternalId(externalId) {
|
|
216
|
+
if (!externalId || typeof externalId !== "string") return null;
|
|
217
|
+
return sha256(externalId.trim());
|
|
218
|
+
}
|
|
219
|
+
/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
220
|
+
* BATCH NORMALIZATION
|
|
221
|
+
* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
|
|
222
|
+
/**
|
|
223
|
+
* Normalize and hash all user data
|
|
224
|
+
*
|
|
225
|
+
* Raw PII in, hashed data out.
|
|
226
|
+
*
|
|
227
|
+
* @param raw - Raw user data (unhashed PII)
|
|
228
|
+
* @returns Normalized and hashed user data
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* ```typescript
|
|
232
|
+
* const normalizer = new Normalizer();
|
|
233
|
+
* const normalized = await normalizer.normalize({
|
|
234
|
+
* email: 'JOHN@EXAMPLE.COM',
|
|
235
|
+
* phone: '555-123-4567',
|
|
236
|
+
* first_name: 'John',
|
|
237
|
+
* last_name: 'Doe',
|
|
238
|
+
* city: 'San Francisco',
|
|
239
|
+
* state: 'CA',
|
|
240
|
+
* country: 'US',
|
|
241
|
+
* });
|
|
242
|
+
*
|
|
243
|
+
* // Result:
|
|
244
|
+
* // {
|
|
245
|
+
* // email: '5e884898...', (hashed)
|
|
246
|
+
* // phone: 'a1b2c3d4...', (hashed)
|
|
247
|
+
* // first_name: 'e5f6g7h8...', (hashed)
|
|
248
|
+
* // last_name: 'i9j0k1l2...', (hashed)
|
|
249
|
+
* // city: 'san francisco', (normalized, not hashed by default)
|
|
250
|
+
* // state: 'ca', (normalized, not hashed by default)
|
|
251
|
+
* // country: 'us', (normalized, not hashed by default)
|
|
252
|
+
* // }
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
async normalize(raw) {
|
|
256
|
+
const result = {};
|
|
257
|
+
if (raw.email) {
|
|
258
|
+
const h = await this.hashEmail(raw.email);
|
|
259
|
+
if (h) result.email = h;
|
|
260
|
+
}
|
|
261
|
+
if (raw.phone) {
|
|
262
|
+
const h = await this.hashPhone(raw.phone);
|
|
263
|
+
if (h) result.phone = h;
|
|
264
|
+
}
|
|
265
|
+
if (raw.first_name) {
|
|
266
|
+
const h = await this.hashName(raw.first_name);
|
|
267
|
+
if (h) result.first_name = h;
|
|
268
|
+
}
|
|
269
|
+
if (raw.last_name) {
|
|
270
|
+
const h = await this.hashName(raw.last_name);
|
|
271
|
+
if (h) result.last_name = h;
|
|
272
|
+
}
|
|
273
|
+
if (raw.gender) {
|
|
274
|
+
const h = await this.hashGender(raw.gender);
|
|
275
|
+
if (h) result.gender = h;
|
|
276
|
+
}
|
|
277
|
+
if (raw.date_of_birth) {
|
|
278
|
+
const h = await this.hashDateOfBirth(raw.date_of_birth);
|
|
279
|
+
if (h) result.date_of_birth = h;
|
|
280
|
+
}
|
|
281
|
+
if (raw.external_id) {
|
|
282
|
+
const h = await this.hashExternalId(raw.external_id);
|
|
283
|
+
if (h) result.external_id = h;
|
|
284
|
+
}
|
|
285
|
+
if (raw.city) {
|
|
286
|
+
if (this.options.hashAddressFields) {
|
|
287
|
+
const h = await this.hashCity(raw.city);
|
|
288
|
+
if (h) result.city = h;
|
|
289
|
+
} else {
|
|
290
|
+
result.city = raw.city;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (raw.state) {
|
|
294
|
+
if (this.options.hashAddressFields) {
|
|
295
|
+
const h = await this.hashState(raw.state);
|
|
296
|
+
if (h) result.state = h;
|
|
297
|
+
} else {
|
|
298
|
+
result.state = raw.state;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (raw.zip_code) {
|
|
302
|
+
if (this.options.hashAddressFields) {
|
|
303
|
+
const h = await this.hashZipCode(raw.zip_code);
|
|
304
|
+
if (h) result.zip_code = h;
|
|
305
|
+
} else {
|
|
306
|
+
result.zip_code = raw.zip_code;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (raw.country) {
|
|
310
|
+
if (this.options.hashAddressFields) {
|
|
311
|
+
const h = await this.hashCountry(raw.country);
|
|
312
|
+
if (h) result.country = h;
|
|
313
|
+
} else {
|
|
314
|
+
result.country = raw.country;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (raw.ip_address) result.ip_address = raw.ip_address;
|
|
318
|
+
if (raw.user_agent) result.user_agent = raw.user_agent;
|
|
319
|
+
if (raw.subscription_id) result.subscription_id = raw.subscription_id;
|
|
320
|
+
if (raw.lead_id) result.lead_id = raw.lead_id;
|
|
321
|
+
if (raw.anonymous_id) result.anonymous_id = raw.anonymous_id;
|
|
322
|
+
if (raw.traits) result.traits = raw.traits;
|
|
323
|
+
return result;
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
327
|
+
0 && (module.exports = {
|
|
328
|
+
Normalizer,
|
|
329
|
+
isSha256,
|
|
330
|
+
sha256
|
|
331
|
+
});
|
|
332
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/hash.ts","../src/Normalizer.ts"],"sourcesContent":["/**\n * @sygnl/normalizer\n * \n * PII normalization and SHA256 hashing for Meta CAPI and ad platforms.\n * \n * @example\n * ```typescript\n * import { Normalizer } from '@sygnl/normalizer';\n * \n * const normalizer = new Normalizer();\n * \n * // Normalize and hash user data\n * const normalized = await normalizer.normalize({\n * email: 'JOHN@EXAMPLE.COM',\n * phone: '555-123-4567',\n * first_name: 'John',\n * last_name: 'Doe',\n * });\n * \n * // Or use individual methods\n * const emailHash = await normalizer.hashEmail('john@example.com');\n * const phoneHash = await normalizer.hashPhone('555-123-4567');\n * ```\n */\n\nexport { Normalizer } from './Normalizer';\nexport { sha256, isSha256 } from './hash';\nexport type {\n Sha256Hash,\n RawUserData,\n NormalizedUserData,\n NormalizerOptions,\n} from './types';\n","/**\n * SHA256 Hashing Utilities\n * \n * Uses Web Crypto API (available in modern browsers and Node.js 18+)\n * Works in both browser and Cloudflare Workers environments.\n * \n * All PII MUST be normalized before hashing to ensure consistent results.\n */\n\nimport type { Sha256Hash } from './types';\n\n/**\n * SHA256 hash a string using Web Crypto API\n * \n * Returns 64 lowercase hex characters.\n * Works in both browser and Node.js 18+ (Web Crypto API).\n * \n * @param input - String to hash (should be normalized first)\n * @returns Promise resolving to 64-character lowercase hex string\n * \n * @example\n * ```typescript\n * const hash = await sha256('hello@example.com');\n * // Returns: \"5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8\"\n * ```\n */\nexport async function sha256(input: string): Promise<Sha256Hash> {\n const encoder = new TextEncoder();\n const data = encoder.encode(input);\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n const hashArray = Array.from(new Uint8Array(hashBuffer));\n const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');\n return hashHex as Sha256Hash;\n}\n\n/**\n * Validate that a string is a valid SHA256 hash\n * \n * Checks for 64 lowercase hexadecimal characters.\n * \n * @param value - String to validate\n * @returns true if valid SHA256 hash format\n * \n * @example\n * ```typescript\n * isSha256('abc123'); // false\n * isSha256('5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8'); // true\n * ```\n */\nexport function isSha256(value: string): value is Sha256Hash {\n return /^[a-f0-9]{64}$/.test(value);\n}\n","/**\n * Normalizer Class\n * \n * Normalizes and hashes PII fields for ad platform APIs (Meta CAPI, Google Ads, etc.)\n * \n * Normalization rules ensure consistent hashing:\n * - Email: lowercase, trim\n * - Phone: E.164 format (+15551234567)\n * - Name: lowercase, trim, single spaces\n * - Gender: single char (m/f)\n * - Date of Birth: YYYYMMDD format\n * - Address: lowercase, trim, format-specific rules\n * \n * Meta CAPI Hashing Requirements:\n * - HASH: email, phone, first_name, last_name, gender, date_of_birth, city, state, zip_code, country, external_id\n * - DO NOT HASH: ip_address, user_agent, fbc, fbp, subscription_id, lead_id\n */\n\nimport type { NormalizerOptions, RawUserData, NormalizedUserData, Sha256Hash } from './types';\nimport { sha256 } from './hash';\n\nexport class Normalizer {\n private options: Required<NormalizerOptions>;\n\n constructor(options: NormalizerOptions = {}) {\n this.options = {\n defaultCountryCode: options.defaultCountryCode ?? '1',\n hashAddressFields: options.hashAddressFields ?? false,\n };\n }\n\n /* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n * NORMALIZATION METHODS\n * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n /**\n * Normalize email: lowercase, trim whitespace\n * Returns null if invalid format\n */\n normalizeEmail(email: string): string | null {\n if (!email || typeof email !== 'string') return null;\n const normalized = email.toLowerCase().trim();\n if (!normalized.includes('@') || normalized.length < 5) return null;\n return normalized;\n }\n\n /**\n * Normalize phone to E.164 format (+15551234567)\n * Returns null if invalid\n */\n normalizePhone(phone: string, countryCode?: string): string | null {\n if (!phone || typeof phone !== 'string') return null;\n const code = countryCode ?? this.options.defaultCountryCode;\n let digits = phone.replace(/[^\\d+]/g, '');\n if (digits.startsWith('+')) digits = digits.slice(1);\n if (digits.length < 10) return null;\n if (digits.length === 10) digits = code + digits;\n return '+' + digits;\n }\n\n /**\n * Normalize name: lowercase, trim, remove extra spaces\n * Returns null if empty\n */\n normalizeName(name: string): string | null {\n if (!name || typeof name !== 'string') return null;\n const normalized = name.toLowerCase().trim().replace(/\\s+/g, ' ');\n if (normalized.length === 0) return null;\n return normalized;\n }\n\n /**\n * Normalize gender: single lowercase char (m/f)\n * Accepts: m, male, f, female\n * Returns null if invalid\n */\n normalizeGender(gender: string): string | null {\n if (!gender || typeof gender !== 'string') return null;\n const g = gender.toLowerCase().trim();\n if (g === 'm' || g === 'male') return 'm';\n if (g === 'f' || g === 'female') return 'f';\n return null;\n }\n\n /**\n * Normalize date of birth: YYYYMMDD format\n * Accepts formats: YYYY-MM-DD, YYYY/MM/DD, YYYY.MM.DD, YYYYMMDD\n * Returns null if invalid\n */\n normalizeDateOfBirth(dob: string): string | null {\n if (!dob || typeof dob !== 'string') return null;\n const cleaned = dob.replace(/[-\\/\\.]/g, '');\n if (!/^\\d{8}$/.test(cleaned)) return null;\n return cleaned;\n }\n\n /**\n * Normalize city: lowercase, trim\n */\n normalizeCity(city: string): string | null {\n if (!city || typeof city !== 'string') return null;\n const normalized = city.toLowerCase().trim();\n if (normalized.length === 0) return null;\n return normalized;\n }\n\n /**\n * Normalize state/region: lowercase, trim\n */\n normalizeState(state: string): string | null {\n if (!state || typeof state !== 'string') return null;\n const normalized = state.toLowerCase().trim();\n if (normalized.length === 0) return null;\n return normalized;\n }\n\n /**\n * Normalize zip/postal code: lowercase, no spaces\n */\n normalizeZipCode(zip: string): string | null {\n if (!zip || typeof zip !== 'string') return null;\n const normalized = zip.toLowerCase().trim().replace(/\\s+/g, '');\n if (normalized.length === 0) return null;\n return normalized;\n }\n\n /**\n * Normalize country: 2-letter code, lowercase\n * Returns null if not exactly 2 characters\n */\n normalizeCountry(country: string): string | null {\n if (!country || typeof country !== 'string') return null;\n const normalized = country.toLowerCase().trim();\n if (normalized.length !== 2) return null;\n return normalized;\n }\n\n /* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n * HASH METHODS\n * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n /**\n * Hash email (normalize then hash)\n */\n async hashEmail(email: string): Promise<Sha256Hash | null> {\n const normalized = this.normalizeEmail(email);\n return normalized ? sha256(normalized) : null;\n }\n\n /**\n * Hash phone (normalize then hash)\n */\n async hashPhone(phone: string, countryCode?: string): Promise<Sha256Hash | null> {\n const normalized = this.normalizePhone(phone, countryCode);\n return normalized ? sha256(normalized) : null;\n }\n\n /**\n * Hash name (normalize then hash)\n */\n async hashName(name: string): Promise<Sha256Hash | null> {\n const normalized = this.normalizeName(name);\n return normalized ? sha256(normalized) : null;\n }\n\n /**\n * Hash gender (normalize then hash)\n */\n async hashGender(gender: string): Promise<Sha256Hash | null> {\n const normalized = this.normalizeGender(gender);\n return normalized ? sha256(normalized) : null;\n }\n\n /**\n * Hash date of birth (normalize then hash)\n */\n async hashDateOfBirth(dob: string): Promise<Sha256Hash | null> {\n const normalized = this.normalizeDateOfBirth(dob);\n return normalized ? sha256(normalized) : null;\n }\n\n /**\n * Hash city (normalize then hash)\n */\n async hashCity(city: string): Promise<Sha256Hash | null> {\n const normalized = this.normalizeCity(city);\n return normalized ? sha256(normalized) : null;\n }\n\n /**\n * Hash state (normalize then hash)\n */\n async hashState(state: string): Promise<Sha256Hash | null> {\n const normalized = this.normalizeState(state);\n return normalized ? sha256(normalized) : null;\n }\n\n /**\n * Hash zip code (normalize then hash)\n */\n async hashZipCode(zip: string): Promise<Sha256Hash | null> {\n const normalized = this.normalizeZipCode(zip);\n return normalized ? sha256(normalized) : null;\n }\n\n /**\n * Hash country (normalize then hash)\n */\n async hashCountry(country: string): Promise<Sha256Hash | null> {\n const normalized = this.normalizeCountry(country);\n return normalized ? sha256(normalized) : null;\n }\n\n /**\n * Hash external ID (trim only, no other normalization)\n */\n async hashExternalId(externalId: string): Promise<Sha256Hash | null> {\n if (!externalId || typeof externalId !== 'string') return null;\n return sha256(externalId.trim());\n }\n\n /* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n * BATCH NORMALIZATION\n * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n /**\n * Normalize and hash all user data\n * \n * Raw PII in, hashed data out.\n * \n * @param raw - Raw user data (unhashed PII)\n * @returns Normalized and hashed user data\n * \n * @example\n * ```typescript\n * const normalizer = new Normalizer();\n * const normalized = await normalizer.normalize({\n * email: 'JOHN@EXAMPLE.COM',\n * phone: '555-123-4567',\n * first_name: 'John',\n * last_name: 'Doe',\n * city: 'San Francisco',\n * state: 'CA',\n * country: 'US',\n * });\n * \n * // Result:\n * // {\n * // email: '5e884898...', (hashed)\n * // phone: 'a1b2c3d4...', (hashed)\n * // first_name: 'e5f6g7h8...', (hashed)\n * // last_name: 'i9j0k1l2...', (hashed)\n * // city: 'san francisco', (normalized, not hashed by default)\n * // state: 'ca', (normalized, not hashed by default)\n * // country: 'us', (normalized, not hashed by default)\n * // }\n * ```\n */\n async normalize(raw: RawUserData): Promise<NormalizedUserData> {\n const result: NormalizedUserData = {};\n\n // Hash contact PII\n if (raw.email) {\n const h = await this.hashEmail(raw.email);\n if (h) result.email = h;\n }\n if (raw.phone) {\n const h = await this.hashPhone(raw.phone);\n if (h) result.phone = h;\n }\n if (raw.first_name) {\n const h = await this.hashName(raw.first_name);\n if (h) result.first_name = h;\n }\n if (raw.last_name) {\n const h = await this.hashName(raw.last_name);\n if (h) result.last_name = h;\n }\n if (raw.gender) {\n const h = await this.hashGender(raw.gender);\n if (h) result.gender = h;\n }\n if (raw.date_of_birth) {\n const h = await this.hashDateOfBirth(raw.date_of_birth);\n if (h) result.date_of_birth = h;\n }\n if (raw.external_id) {\n const h = await this.hashExternalId(raw.external_id);\n if (h) result.external_id = h;\n }\n\n // Address fields - hash only if configured to do so\n if (raw.city) {\n if (this.options.hashAddressFields) {\n const h = await this.hashCity(raw.city);\n if (h) result.city = h as unknown as string;\n } else {\n result.city = raw.city;\n }\n }\n if (raw.state) {\n if (this.options.hashAddressFields) {\n const h = await this.hashState(raw.state);\n if (h) result.state = h as unknown as string;\n } else {\n result.state = raw.state;\n }\n }\n if (raw.zip_code) {\n if (this.options.hashAddressFields) {\n const h = await this.hashZipCode(raw.zip_code);\n if (h) result.zip_code = h as unknown as string;\n } else {\n result.zip_code = raw.zip_code;\n }\n }\n if (raw.country) {\n if (this.options.hashAddressFields) {\n const h = await this.hashCountry(raw.country);\n if (h) result.country = h as unknown as string;\n } else {\n result.country = raw.country;\n }\n }\n\n // Pass through unhashed fields\n if (raw.ip_address) result.ip_address = raw.ip_address;\n if (raw.user_agent) result.user_agent = raw.user_agent;\n if (raw.subscription_id) result.subscription_id = raw.subscription_id;\n if (raw.lead_id) result.lead_id = raw.lead_id;\n if (raw.anonymous_id) result.anonymous_id = raw.anonymous_id;\n if (raw.traits) result.traits = raw.traits;\n\n return result;\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for PII normalization and hashing
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* SHA256 hash: 64 lowercase hex characters
|
|
6
|
+
*/
|
|
7
|
+
type Sha256Hash = string & {
|
|
8
|
+
__brand: 'Sha256Hash';
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Raw user data (before normalization/hashing)
|
|
12
|
+
*
|
|
13
|
+
* Fields to HASH (according to Meta CAPI requirements):
|
|
14
|
+
* - email, phone, first_name, last_name, gender, date_of_birth
|
|
15
|
+
* - city, state, zip_code, country, external_id
|
|
16
|
+
*
|
|
17
|
+
* Fields to NOT hash (pass through as-is):
|
|
18
|
+
* - ip_address, user_agent, fbc, fbp, subscription_id, lead_id, anonymous_id
|
|
19
|
+
*/
|
|
20
|
+
interface RawUserData {
|
|
21
|
+
email?: string;
|
|
22
|
+
phone?: string;
|
|
23
|
+
first_name?: string;
|
|
24
|
+
last_name?: string;
|
|
25
|
+
gender?: string;
|
|
26
|
+
date_of_birth?: string;
|
|
27
|
+
external_id?: string;
|
|
28
|
+
city?: string;
|
|
29
|
+
state?: string;
|
|
30
|
+
zip_code?: string;
|
|
31
|
+
country?: string;
|
|
32
|
+
ip_address?: string;
|
|
33
|
+
user_agent?: string;
|
|
34
|
+
subscription_id?: string;
|
|
35
|
+
lead_id?: string;
|
|
36
|
+
anonymous_id?: string;
|
|
37
|
+
traits?: Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Normalized user data (after hashing)
|
|
41
|
+
*
|
|
42
|
+
* All PII fields are hashed with SHA256.
|
|
43
|
+
* Tracking IDs and non-PII fields pass through unchanged.
|
|
44
|
+
*/
|
|
45
|
+
interface NormalizedUserData {
|
|
46
|
+
email?: Sha256Hash;
|
|
47
|
+
phone?: Sha256Hash;
|
|
48
|
+
first_name?: Sha256Hash;
|
|
49
|
+
last_name?: Sha256Hash;
|
|
50
|
+
gender?: Sha256Hash;
|
|
51
|
+
date_of_birth?: Sha256Hash;
|
|
52
|
+
external_id?: Sha256Hash;
|
|
53
|
+
city?: string;
|
|
54
|
+
state?: string;
|
|
55
|
+
zip_code?: string;
|
|
56
|
+
country?: string;
|
|
57
|
+
ip_address?: string;
|
|
58
|
+
user_agent?: string;
|
|
59
|
+
subscription_id?: string;
|
|
60
|
+
lead_id?: string;
|
|
61
|
+
anonymous_id?: string;
|
|
62
|
+
traits?: Record<string, unknown>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Options for the Normalizer
|
|
66
|
+
*/
|
|
67
|
+
interface NormalizerOptions {
|
|
68
|
+
/**
|
|
69
|
+
* Default country code for phone normalization (e.g., '1' for USA)
|
|
70
|
+
* @default '1'
|
|
71
|
+
*/
|
|
72
|
+
defaultCountryCode?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Whether to hash address fields (city, state, zip, country)
|
|
75
|
+
* Some platforms require raw values, others require hashed
|
|
76
|
+
* @default false
|
|
77
|
+
*/
|
|
78
|
+
hashAddressFields?: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Normalizer Class
|
|
83
|
+
*
|
|
84
|
+
* Normalizes and hashes PII fields for ad platform APIs (Meta CAPI, Google Ads, etc.)
|
|
85
|
+
*
|
|
86
|
+
* Normalization rules ensure consistent hashing:
|
|
87
|
+
* - Email: lowercase, trim
|
|
88
|
+
* - Phone: E.164 format (+15551234567)
|
|
89
|
+
* - Name: lowercase, trim, single spaces
|
|
90
|
+
* - Gender: single char (m/f)
|
|
91
|
+
* - Date of Birth: YYYYMMDD format
|
|
92
|
+
* - Address: lowercase, trim, format-specific rules
|
|
93
|
+
*
|
|
94
|
+
* Meta CAPI Hashing Requirements:
|
|
95
|
+
* - HASH: email, phone, first_name, last_name, gender, date_of_birth, city, state, zip_code, country, external_id
|
|
96
|
+
* - DO NOT HASH: ip_address, user_agent, fbc, fbp, subscription_id, lead_id
|
|
97
|
+
*/
|
|
98
|
+
|
|
99
|
+
declare class Normalizer {
|
|
100
|
+
private options;
|
|
101
|
+
constructor(options?: NormalizerOptions);
|
|
102
|
+
/**
|
|
103
|
+
* Normalize email: lowercase, trim whitespace
|
|
104
|
+
* Returns null if invalid format
|
|
105
|
+
*/
|
|
106
|
+
normalizeEmail(email: string): string | null;
|
|
107
|
+
/**
|
|
108
|
+
* Normalize phone to E.164 format (+15551234567)
|
|
109
|
+
* Returns null if invalid
|
|
110
|
+
*/
|
|
111
|
+
normalizePhone(phone: string, countryCode?: string): string | null;
|
|
112
|
+
/**
|
|
113
|
+
* Normalize name: lowercase, trim, remove extra spaces
|
|
114
|
+
* Returns null if empty
|
|
115
|
+
*/
|
|
116
|
+
normalizeName(name: string): string | null;
|
|
117
|
+
/**
|
|
118
|
+
* Normalize gender: single lowercase char (m/f)
|
|
119
|
+
* Accepts: m, male, f, female
|
|
120
|
+
* Returns null if invalid
|
|
121
|
+
*/
|
|
122
|
+
normalizeGender(gender: string): string | null;
|
|
123
|
+
/**
|
|
124
|
+
* Normalize date of birth: YYYYMMDD format
|
|
125
|
+
* Accepts formats: YYYY-MM-DD, YYYY/MM/DD, YYYY.MM.DD, YYYYMMDD
|
|
126
|
+
* Returns null if invalid
|
|
127
|
+
*/
|
|
128
|
+
normalizeDateOfBirth(dob: string): string | null;
|
|
129
|
+
/**
|
|
130
|
+
* Normalize city: lowercase, trim
|
|
131
|
+
*/
|
|
132
|
+
normalizeCity(city: string): string | null;
|
|
133
|
+
/**
|
|
134
|
+
* Normalize state/region: lowercase, trim
|
|
135
|
+
*/
|
|
136
|
+
normalizeState(state: string): string | null;
|
|
137
|
+
/**
|
|
138
|
+
* Normalize zip/postal code: lowercase, no spaces
|
|
139
|
+
*/
|
|
140
|
+
normalizeZipCode(zip: string): string | null;
|
|
141
|
+
/**
|
|
142
|
+
* Normalize country: 2-letter code, lowercase
|
|
143
|
+
* Returns null if not exactly 2 characters
|
|
144
|
+
*/
|
|
145
|
+
normalizeCountry(country: string): string | null;
|
|
146
|
+
/**
|
|
147
|
+
* Hash email (normalize then hash)
|
|
148
|
+
*/
|
|
149
|
+
hashEmail(email: string): Promise<Sha256Hash | null>;
|
|
150
|
+
/**
|
|
151
|
+
* Hash phone (normalize then hash)
|
|
152
|
+
*/
|
|
153
|
+
hashPhone(phone: string, countryCode?: string): Promise<Sha256Hash | null>;
|
|
154
|
+
/**
|
|
155
|
+
* Hash name (normalize then hash)
|
|
156
|
+
*/
|
|
157
|
+
hashName(name: string): Promise<Sha256Hash | null>;
|
|
158
|
+
/**
|
|
159
|
+
* Hash gender (normalize then hash)
|
|
160
|
+
*/
|
|
161
|
+
hashGender(gender: string): Promise<Sha256Hash | null>;
|
|
162
|
+
/**
|
|
163
|
+
* Hash date of birth (normalize then hash)
|
|
164
|
+
*/
|
|
165
|
+
hashDateOfBirth(dob: string): Promise<Sha256Hash | null>;
|
|
166
|
+
/**
|
|
167
|
+
* Hash city (normalize then hash)
|
|
168
|
+
*/
|
|
169
|
+
hashCity(city: string): Promise<Sha256Hash | null>;
|
|
170
|
+
/**
|
|
171
|
+
* Hash state (normalize then hash)
|
|
172
|
+
*/
|
|
173
|
+
hashState(state: string): Promise<Sha256Hash | null>;
|
|
174
|
+
/**
|
|
175
|
+
* Hash zip code (normalize then hash)
|
|
176
|
+
*/
|
|
177
|
+
hashZipCode(zip: string): Promise<Sha256Hash | null>;
|
|
178
|
+
/**
|
|
179
|
+
* Hash country (normalize then hash)
|
|
180
|
+
*/
|
|
181
|
+
hashCountry(country: string): Promise<Sha256Hash | null>;
|
|
182
|
+
/**
|
|
183
|
+
* Hash external ID (trim only, no other normalization)
|
|
184
|
+
*/
|
|
185
|
+
hashExternalId(externalId: string): Promise<Sha256Hash | null>;
|
|
186
|
+
/**
|
|
187
|
+
* Normalize and hash all user data
|
|
188
|
+
*
|
|
189
|
+
* Raw PII in, hashed data out.
|
|
190
|
+
*
|
|
191
|
+
* @param raw - Raw user data (unhashed PII)
|
|
192
|
+
* @returns Normalized and hashed user data
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```typescript
|
|
196
|
+
* const normalizer = new Normalizer();
|
|
197
|
+
* const normalized = await normalizer.normalize({
|
|
198
|
+
* email: 'JOHN@EXAMPLE.COM',
|
|
199
|
+
* phone: '555-123-4567',
|
|
200
|
+
* first_name: 'John',
|
|
201
|
+
* last_name: 'Doe',
|
|
202
|
+
* city: 'San Francisco',
|
|
203
|
+
* state: 'CA',
|
|
204
|
+
* country: 'US',
|
|
205
|
+
* });
|
|
206
|
+
*
|
|
207
|
+
* // Result:
|
|
208
|
+
* // {
|
|
209
|
+
* // email: '5e884898...', (hashed)
|
|
210
|
+
* // phone: 'a1b2c3d4...', (hashed)
|
|
211
|
+
* // first_name: 'e5f6g7h8...', (hashed)
|
|
212
|
+
* // last_name: 'i9j0k1l2...', (hashed)
|
|
213
|
+
* // city: 'san francisco', (normalized, not hashed by default)
|
|
214
|
+
* // state: 'ca', (normalized, not hashed by default)
|
|
215
|
+
* // country: 'us', (normalized, not hashed by default)
|
|
216
|
+
* // }
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
normalize(raw: RawUserData): Promise<NormalizedUserData>;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* SHA256 Hashing Utilities
|
|
224
|
+
*
|
|
225
|
+
* Uses Web Crypto API (available in modern browsers and Node.js 18+)
|
|
226
|
+
* Works in both browser and Cloudflare Workers environments.
|
|
227
|
+
*
|
|
228
|
+
* All PII MUST be normalized before hashing to ensure consistent results.
|
|
229
|
+
*/
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* SHA256 hash a string using Web Crypto API
|
|
233
|
+
*
|
|
234
|
+
* Returns 64 lowercase hex characters.
|
|
235
|
+
* Works in both browser and Node.js 18+ (Web Crypto API).
|
|
236
|
+
*
|
|
237
|
+
* @param input - String to hash (should be normalized first)
|
|
238
|
+
* @returns Promise resolving to 64-character lowercase hex string
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* ```typescript
|
|
242
|
+
* const hash = await sha256('hello@example.com');
|
|
243
|
+
* // Returns: "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
|
|
244
|
+
* ```
|
|
245
|
+
*/
|
|
246
|
+
declare function sha256(input: string): Promise<Sha256Hash>;
|
|
247
|
+
/**
|
|
248
|
+
* Validate that a string is a valid SHA256 hash
|
|
249
|
+
*
|
|
250
|
+
* Checks for 64 lowercase hexadecimal characters.
|
|
251
|
+
*
|
|
252
|
+
* @param value - String to validate
|
|
253
|
+
* @returns true if valid SHA256 hash format
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* ```typescript
|
|
257
|
+
* isSha256('abc123'); // false
|
|
258
|
+
* isSha256('5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8'); // true
|
|
259
|
+
* ```
|
|
260
|
+
*/
|
|
261
|
+
declare function isSha256(value: string): value is Sha256Hash;
|
|
262
|
+
|
|
263
|
+
export { type NormalizedUserData, Normalizer, type NormalizerOptions, type RawUserData, type Sha256Hash, isSha256, sha256 };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for PII normalization and hashing
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* SHA256 hash: 64 lowercase hex characters
|
|
6
|
+
*/
|
|
7
|
+
type Sha256Hash = string & {
|
|
8
|
+
__brand: 'Sha256Hash';
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Raw user data (before normalization/hashing)
|
|
12
|
+
*
|
|
13
|
+
* Fields to HASH (according to Meta CAPI requirements):
|
|
14
|
+
* - email, phone, first_name, last_name, gender, date_of_birth
|
|
15
|
+
* - city, state, zip_code, country, external_id
|
|
16
|
+
*
|
|
17
|
+
* Fields to NOT hash (pass through as-is):
|
|
18
|
+
* - ip_address, user_agent, fbc, fbp, subscription_id, lead_id, anonymous_id
|
|
19
|
+
*/
|
|
20
|
+
interface RawUserData {
|
|
21
|
+
email?: string;
|
|
22
|
+
phone?: string;
|
|
23
|
+
first_name?: string;
|
|
24
|
+
last_name?: string;
|
|
25
|
+
gender?: string;
|
|
26
|
+
date_of_birth?: string;
|
|
27
|
+
external_id?: string;
|
|
28
|
+
city?: string;
|
|
29
|
+
state?: string;
|
|
30
|
+
zip_code?: string;
|
|
31
|
+
country?: string;
|
|
32
|
+
ip_address?: string;
|
|
33
|
+
user_agent?: string;
|
|
34
|
+
subscription_id?: string;
|
|
35
|
+
lead_id?: string;
|
|
36
|
+
anonymous_id?: string;
|
|
37
|
+
traits?: Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Normalized user data (after hashing)
|
|
41
|
+
*
|
|
42
|
+
* All PII fields are hashed with SHA256.
|
|
43
|
+
* Tracking IDs and non-PII fields pass through unchanged.
|
|
44
|
+
*/
|
|
45
|
+
interface NormalizedUserData {
|
|
46
|
+
email?: Sha256Hash;
|
|
47
|
+
phone?: Sha256Hash;
|
|
48
|
+
first_name?: Sha256Hash;
|
|
49
|
+
last_name?: Sha256Hash;
|
|
50
|
+
gender?: Sha256Hash;
|
|
51
|
+
date_of_birth?: Sha256Hash;
|
|
52
|
+
external_id?: Sha256Hash;
|
|
53
|
+
city?: string;
|
|
54
|
+
state?: string;
|
|
55
|
+
zip_code?: string;
|
|
56
|
+
country?: string;
|
|
57
|
+
ip_address?: string;
|
|
58
|
+
user_agent?: string;
|
|
59
|
+
subscription_id?: string;
|
|
60
|
+
lead_id?: string;
|
|
61
|
+
anonymous_id?: string;
|
|
62
|
+
traits?: Record<string, unknown>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Options for the Normalizer
|
|
66
|
+
*/
|
|
67
|
+
interface NormalizerOptions {
|
|
68
|
+
/**
|
|
69
|
+
* Default country code for phone normalization (e.g., '1' for USA)
|
|
70
|
+
* @default '1'
|
|
71
|
+
*/
|
|
72
|
+
defaultCountryCode?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Whether to hash address fields (city, state, zip, country)
|
|
75
|
+
* Some platforms require raw values, others require hashed
|
|
76
|
+
* @default false
|
|
77
|
+
*/
|
|
78
|
+
hashAddressFields?: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Normalizer Class
|
|
83
|
+
*
|
|
84
|
+
* Normalizes and hashes PII fields for ad platform APIs (Meta CAPI, Google Ads, etc.)
|
|
85
|
+
*
|
|
86
|
+
* Normalization rules ensure consistent hashing:
|
|
87
|
+
* - Email: lowercase, trim
|
|
88
|
+
* - Phone: E.164 format (+15551234567)
|
|
89
|
+
* - Name: lowercase, trim, single spaces
|
|
90
|
+
* - Gender: single char (m/f)
|
|
91
|
+
* - Date of Birth: YYYYMMDD format
|
|
92
|
+
* - Address: lowercase, trim, format-specific rules
|
|
93
|
+
*
|
|
94
|
+
* Meta CAPI Hashing Requirements:
|
|
95
|
+
* - HASH: email, phone, first_name, last_name, gender, date_of_birth, city, state, zip_code, country, external_id
|
|
96
|
+
* - DO NOT HASH: ip_address, user_agent, fbc, fbp, subscription_id, lead_id
|
|
97
|
+
*/
|
|
98
|
+
|
|
99
|
+
declare class Normalizer {
|
|
100
|
+
private options;
|
|
101
|
+
constructor(options?: NormalizerOptions);
|
|
102
|
+
/**
|
|
103
|
+
* Normalize email: lowercase, trim whitespace
|
|
104
|
+
* Returns null if invalid format
|
|
105
|
+
*/
|
|
106
|
+
normalizeEmail(email: string): string | null;
|
|
107
|
+
/**
|
|
108
|
+
* Normalize phone to E.164 format (+15551234567)
|
|
109
|
+
* Returns null if invalid
|
|
110
|
+
*/
|
|
111
|
+
normalizePhone(phone: string, countryCode?: string): string | null;
|
|
112
|
+
/**
|
|
113
|
+
* Normalize name: lowercase, trim, remove extra spaces
|
|
114
|
+
* Returns null if empty
|
|
115
|
+
*/
|
|
116
|
+
normalizeName(name: string): string | null;
|
|
117
|
+
/**
|
|
118
|
+
* Normalize gender: single lowercase char (m/f)
|
|
119
|
+
* Accepts: m, male, f, female
|
|
120
|
+
* Returns null if invalid
|
|
121
|
+
*/
|
|
122
|
+
normalizeGender(gender: string): string | null;
|
|
123
|
+
/**
|
|
124
|
+
* Normalize date of birth: YYYYMMDD format
|
|
125
|
+
* Accepts formats: YYYY-MM-DD, YYYY/MM/DD, YYYY.MM.DD, YYYYMMDD
|
|
126
|
+
* Returns null if invalid
|
|
127
|
+
*/
|
|
128
|
+
normalizeDateOfBirth(dob: string): string | null;
|
|
129
|
+
/**
|
|
130
|
+
* Normalize city: lowercase, trim
|
|
131
|
+
*/
|
|
132
|
+
normalizeCity(city: string): string | null;
|
|
133
|
+
/**
|
|
134
|
+
* Normalize state/region: lowercase, trim
|
|
135
|
+
*/
|
|
136
|
+
normalizeState(state: string): string | null;
|
|
137
|
+
/**
|
|
138
|
+
* Normalize zip/postal code: lowercase, no spaces
|
|
139
|
+
*/
|
|
140
|
+
normalizeZipCode(zip: string): string | null;
|
|
141
|
+
/**
|
|
142
|
+
* Normalize country: 2-letter code, lowercase
|
|
143
|
+
* Returns null if not exactly 2 characters
|
|
144
|
+
*/
|
|
145
|
+
normalizeCountry(country: string): string | null;
|
|
146
|
+
/**
|
|
147
|
+
* Hash email (normalize then hash)
|
|
148
|
+
*/
|
|
149
|
+
hashEmail(email: string): Promise<Sha256Hash | null>;
|
|
150
|
+
/**
|
|
151
|
+
* Hash phone (normalize then hash)
|
|
152
|
+
*/
|
|
153
|
+
hashPhone(phone: string, countryCode?: string): Promise<Sha256Hash | null>;
|
|
154
|
+
/**
|
|
155
|
+
* Hash name (normalize then hash)
|
|
156
|
+
*/
|
|
157
|
+
hashName(name: string): Promise<Sha256Hash | null>;
|
|
158
|
+
/**
|
|
159
|
+
* Hash gender (normalize then hash)
|
|
160
|
+
*/
|
|
161
|
+
hashGender(gender: string): Promise<Sha256Hash | null>;
|
|
162
|
+
/**
|
|
163
|
+
* Hash date of birth (normalize then hash)
|
|
164
|
+
*/
|
|
165
|
+
hashDateOfBirth(dob: string): Promise<Sha256Hash | null>;
|
|
166
|
+
/**
|
|
167
|
+
* Hash city (normalize then hash)
|
|
168
|
+
*/
|
|
169
|
+
hashCity(city: string): Promise<Sha256Hash | null>;
|
|
170
|
+
/**
|
|
171
|
+
* Hash state (normalize then hash)
|
|
172
|
+
*/
|
|
173
|
+
hashState(state: string): Promise<Sha256Hash | null>;
|
|
174
|
+
/**
|
|
175
|
+
* Hash zip code (normalize then hash)
|
|
176
|
+
*/
|
|
177
|
+
hashZipCode(zip: string): Promise<Sha256Hash | null>;
|
|
178
|
+
/**
|
|
179
|
+
* Hash country (normalize then hash)
|
|
180
|
+
*/
|
|
181
|
+
hashCountry(country: string): Promise<Sha256Hash | null>;
|
|
182
|
+
/**
|
|
183
|
+
* Hash external ID (trim only, no other normalization)
|
|
184
|
+
*/
|
|
185
|
+
hashExternalId(externalId: string): Promise<Sha256Hash | null>;
|
|
186
|
+
/**
|
|
187
|
+
* Normalize and hash all user data
|
|
188
|
+
*
|
|
189
|
+
* Raw PII in, hashed data out.
|
|
190
|
+
*
|
|
191
|
+
* @param raw - Raw user data (unhashed PII)
|
|
192
|
+
* @returns Normalized and hashed user data
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```typescript
|
|
196
|
+
* const normalizer = new Normalizer();
|
|
197
|
+
* const normalized = await normalizer.normalize({
|
|
198
|
+
* email: 'JOHN@EXAMPLE.COM',
|
|
199
|
+
* phone: '555-123-4567',
|
|
200
|
+
* first_name: 'John',
|
|
201
|
+
* last_name: 'Doe',
|
|
202
|
+
* city: 'San Francisco',
|
|
203
|
+
* state: 'CA',
|
|
204
|
+
* country: 'US',
|
|
205
|
+
* });
|
|
206
|
+
*
|
|
207
|
+
* // Result:
|
|
208
|
+
* // {
|
|
209
|
+
* // email: '5e884898...', (hashed)
|
|
210
|
+
* // phone: 'a1b2c3d4...', (hashed)
|
|
211
|
+
* // first_name: 'e5f6g7h8...', (hashed)
|
|
212
|
+
* // last_name: 'i9j0k1l2...', (hashed)
|
|
213
|
+
* // city: 'san francisco', (normalized, not hashed by default)
|
|
214
|
+
* // state: 'ca', (normalized, not hashed by default)
|
|
215
|
+
* // country: 'us', (normalized, not hashed by default)
|
|
216
|
+
* // }
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
normalize(raw: RawUserData): Promise<NormalizedUserData>;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* SHA256 Hashing Utilities
|
|
224
|
+
*
|
|
225
|
+
* Uses Web Crypto API (available in modern browsers and Node.js 18+)
|
|
226
|
+
* Works in both browser and Cloudflare Workers environments.
|
|
227
|
+
*
|
|
228
|
+
* All PII MUST be normalized before hashing to ensure consistent results.
|
|
229
|
+
*/
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* SHA256 hash a string using Web Crypto API
|
|
233
|
+
*
|
|
234
|
+
* Returns 64 lowercase hex characters.
|
|
235
|
+
* Works in both browser and Node.js 18+ (Web Crypto API).
|
|
236
|
+
*
|
|
237
|
+
* @param input - String to hash (should be normalized first)
|
|
238
|
+
* @returns Promise resolving to 64-character lowercase hex string
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* ```typescript
|
|
242
|
+
* const hash = await sha256('hello@example.com');
|
|
243
|
+
* // Returns: "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
|
|
244
|
+
* ```
|
|
245
|
+
*/
|
|
246
|
+
declare function sha256(input: string): Promise<Sha256Hash>;
|
|
247
|
+
/**
|
|
248
|
+
* Validate that a string is a valid SHA256 hash
|
|
249
|
+
*
|
|
250
|
+
* Checks for 64 lowercase hexadecimal characters.
|
|
251
|
+
*
|
|
252
|
+
* @param value - String to validate
|
|
253
|
+
* @returns true if valid SHA256 hash format
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* ```typescript
|
|
257
|
+
* isSha256('abc123'); // false
|
|
258
|
+
* isSha256('5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8'); // true
|
|
259
|
+
* ```
|
|
260
|
+
*/
|
|
261
|
+
declare function isSha256(value: string): value is Sha256Hash;
|
|
262
|
+
|
|
263
|
+
export { type NormalizedUserData, Normalizer, type NormalizerOptions, type RawUserData, type Sha256Hash, isSha256, sha256 };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
// src/hash.ts
|
|
2
|
+
async function sha256(input) {
|
|
3
|
+
const encoder = new TextEncoder();
|
|
4
|
+
const data = encoder.encode(input);
|
|
5
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
6
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
7
|
+
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
8
|
+
return hashHex;
|
|
9
|
+
}
|
|
10
|
+
function isSha256(value) {
|
|
11
|
+
return /^[a-f0-9]{64}$/.test(value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// src/Normalizer.ts
|
|
15
|
+
var Normalizer = class {
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.options = {
|
|
18
|
+
defaultCountryCode: options.defaultCountryCode ?? "1",
|
|
19
|
+
hashAddressFields: options.hashAddressFields ?? false
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
23
|
+
* NORMALIZATION METHODS
|
|
24
|
+
* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
|
|
25
|
+
/**
|
|
26
|
+
* Normalize email: lowercase, trim whitespace
|
|
27
|
+
* Returns null if invalid format
|
|
28
|
+
*/
|
|
29
|
+
normalizeEmail(email) {
|
|
30
|
+
if (!email || typeof email !== "string") return null;
|
|
31
|
+
const normalized = email.toLowerCase().trim();
|
|
32
|
+
if (!normalized.includes("@") || normalized.length < 5) return null;
|
|
33
|
+
return normalized;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Normalize phone to E.164 format (+15551234567)
|
|
37
|
+
* Returns null if invalid
|
|
38
|
+
*/
|
|
39
|
+
normalizePhone(phone, countryCode) {
|
|
40
|
+
if (!phone || typeof phone !== "string") return null;
|
|
41
|
+
const code = countryCode ?? this.options.defaultCountryCode;
|
|
42
|
+
let digits = phone.replace(/[^\d+]/g, "");
|
|
43
|
+
if (digits.startsWith("+")) digits = digits.slice(1);
|
|
44
|
+
if (digits.length < 10) return null;
|
|
45
|
+
if (digits.length === 10) digits = code + digits;
|
|
46
|
+
return "+" + digits;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Normalize name: lowercase, trim, remove extra spaces
|
|
50
|
+
* Returns null if empty
|
|
51
|
+
*/
|
|
52
|
+
normalizeName(name) {
|
|
53
|
+
if (!name || typeof name !== "string") return null;
|
|
54
|
+
const normalized = name.toLowerCase().trim().replace(/\s+/g, " ");
|
|
55
|
+
if (normalized.length === 0) return null;
|
|
56
|
+
return normalized;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Normalize gender: single lowercase char (m/f)
|
|
60
|
+
* Accepts: m, male, f, female
|
|
61
|
+
* Returns null if invalid
|
|
62
|
+
*/
|
|
63
|
+
normalizeGender(gender) {
|
|
64
|
+
if (!gender || typeof gender !== "string") return null;
|
|
65
|
+
const g = gender.toLowerCase().trim();
|
|
66
|
+
if (g === "m" || g === "male") return "m";
|
|
67
|
+
if (g === "f" || g === "female") return "f";
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Normalize date of birth: YYYYMMDD format
|
|
72
|
+
* Accepts formats: YYYY-MM-DD, YYYY/MM/DD, YYYY.MM.DD, YYYYMMDD
|
|
73
|
+
* Returns null if invalid
|
|
74
|
+
*/
|
|
75
|
+
normalizeDateOfBirth(dob) {
|
|
76
|
+
if (!dob || typeof dob !== "string") return null;
|
|
77
|
+
const cleaned = dob.replace(/[-\/\.]/g, "");
|
|
78
|
+
if (!/^\d{8}$/.test(cleaned)) return null;
|
|
79
|
+
return cleaned;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Normalize city: lowercase, trim
|
|
83
|
+
*/
|
|
84
|
+
normalizeCity(city) {
|
|
85
|
+
if (!city || typeof city !== "string") return null;
|
|
86
|
+
const normalized = city.toLowerCase().trim();
|
|
87
|
+
if (normalized.length === 0) return null;
|
|
88
|
+
return normalized;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Normalize state/region: lowercase, trim
|
|
92
|
+
*/
|
|
93
|
+
normalizeState(state) {
|
|
94
|
+
if (!state || typeof state !== "string") return null;
|
|
95
|
+
const normalized = state.toLowerCase().trim();
|
|
96
|
+
if (normalized.length === 0) return null;
|
|
97
|
+
return normalized;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Normalize zip/postal code: lowercase, no spaces
|
|
101
|
+
*/
|
|
102
|
+
normalizeZipCode(zip) {
|
|
103
|
+
if (!zip || typeof zip !== "string") return null;
|
|
104
|
+
const normalized = zip.toLowerCase().trim().replace(/\s+/g, "");
|
|
105
|
+
if (normalized.length === 0) return null;
|
|
106
|
+
return normalized;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Normalize country: 2-letter code, lowercase
|
|
110
|
+
* Returns null if not exactly 2 characters
|
|
111
|
+
*/
|
|
112
|
+
normalizeCountry(country) {
|
|
113
|
+
if (!country || typeof country !== "string") return null;
|
|
114
|
+
const normalized = country.toLowerCase().trim();
|
|
115
|
+
if (normalized.length !== 2) return null;
|
|
116
|
+
return normalized;
|
|
117
|
+
}
|
|
118
|
+
/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
119
|
+
* HASH METHODS
|
|
120
|
+
* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
|
|
121
|
+
/**
|
|
122
|
+
* Hash email (normalize then hash)
|
|
123
|
+
*/
|
|
124
|
+
async hashEmail(email) {
|
|
125
|
+
const normalized = this.normalizeEmail(email);
|
|
126
|
+
return normalized ? sha256(normalized) : null;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Hash phone (normalize then hash)
|
|
130
|
+
*/
|
|
131
|
+
async hashPhone(phone, countryCode) {
|
|
132
|
+
const normalized = this.normalizePhone(phone, countryCode);
|
|
133
|
+
return normalized ? sha256(normalized) : null;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Hash name (normalize then hash)
|
|
137
|
+
*/
|
|
138
|
+
async hashName(name) {
|
|
139
|
+
const normalized = this.normalizeName(name);
|
|
140
|
+
return normalized ? sha256(normalized) : null;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Hash gender (normalize then hash)
|
|
144
|
+
*/
|
|
145
|
+
async hashGender(gender) {
|
|
146
|
+
const normalized = this.normalizeGender(gender);
|
|
147
|
+
return normalized ? sha256(normalized) : null;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Hash date of birth (normalize then hash)
|
|
151
|
+
*/
|
|
152
|
+
async hashDateOfBirth(dob) {
|
|
153
|
+
const normalized = this.normalizeDateOfBirth(dob);
|
|
154
|
+
return normalized ? sha256(normalized) : null;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Hash city (normalize then hash)
|
|
158
|
+
*/
|
|
159
|
+
async hashCity(city) {
|
|
160
|
+
const normalized = this.normalizeCity(city);
|
|
161
|
+
return normalized ? sha256(normalized) : null;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Hash state (normalize then hash)
|
|
165
|
+
*/
|
|
166
|
+
async hashState(state) {
|
|
167
|
+
const normalized = this.normalizeState(state);
|
|
168
|
+
return normalized ? sha256(normalized) : null;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Hash zip code (normalize then hash)
|
|
172
|
+
*/
|
|
173
|
+
async hashZipCode(zip) {
|
|
174
|
+
const normalized = this.normalizeZipCode(zip);
|
|
175
|
+
return normalized ? sha256(normalized) : null;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Hash country (normalize then hash)
|
|
179
|
+
*/
|
|
180
|
+
async hashCountry(country) {
|
|
181
|
+
const normalized = this.normalizeCountry(country);
|
|
182
|
+
return normalized ? sha256(normalized) : null;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Hash external ID (trim only, no other normalization)
|
|
186
|
+
*/
|
|
187
|
+
async hashExternalId(externalId) {
|
|
188
|
+
if (!externalId || typeof externalId !== "string") return null;
|
|
189
|
+
return sha256(externalId.trim());
|
|
190
|
+
}
|
|
191
|
+
/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
192
|
+
* BATCH NORMALIZATION
|
|
193
|
+
* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
|
|
194
|
+
/**
|
|
195
|
+
* Normalize and hash all user data
|
|
196
|
+
*
|
|
197
|
+
* Raw PII in, hashed data out.
|
|
198
|
+
*
|
|
199
|
+
* @param raw - Raw user data (unhashed PII)
|
|
200
|
+
* @returns Normalized and hashed user data
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```typescript
|
|
204
|
+
* const normalizer = new Normalizer();
|
|
205
|
+
* const normalized = await normalizer.normalize({
|
|
206
|
+
* email: 'JOHN@EXAMPLE.COM',
|
|
207
|
+
* phone: '555-123-4567',
|
|
208
|
+
* first_name: 'John',
|
|
209
|
+
* last_name: 'Doe',
|
|
210
|
+
* city: 'San Francisco',
|
|
211
|
+
* state: 'CA',
|
|
212
|
+
* country: 'US',
|
|
213
|
+
* });
|
|
214
|
+
*
|
|
215
|
+
* // Result:
|
|
216
|
+
* // {
|
|
217
|
+
* // email: '5e884898...', (hashed)
|
|
218
|
+
* // phone: 'a1b2c3d4...', (hashed)
|
|
219
|
+
* // first_name: 'e5f6g7h8...', (hashed)
|
|
220
|
+
* // last_name: 'i9j0k1l2...', (hashed)
|
|
221
|
+
* // city: 'san francisco', (normalized, not hashed by default)
|
|
222
|
+
* // state: 'ca', (normalized, not hashed by default)
|
|
223
|
+
* // country: 'us', (normalized, not hashed by default)
|
|
224
|
+
* // }
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
async normalize(raw) {
|
|
228
|
+
const result = {};
|
|
229
|
+
if (raw.email) {
|
|
230
|
+
const h = await this.hashEmail(raw.email);
|
|
231
|
+
if (h) result.email = h;
|
|
232
|
+
}
|
|
233
|
+
if (raw.phone) {
|
|
234
|
+
const h = await this.hashPhone(raw.phone);
|
|
235
|
+
if (h) result.phone = h;
|
|
236
|
+
}
|
|
237
|
+
if (raw.first_name) {
|
|
238
|
+
const h = await this.hashName(raw.first_name);
|
|
239
|
+
if (h) result.first_name = h;
|
|
240
|
+
}
|
|
241
|
+
if (raw.last_name) {
|
|
242
|
+
const h = await this.hashName(raw.last_name);
|
|
243
|
+
if (h) result.last_name = h;
|
|
244
|
+
}
|
|
245
|
+
if (raw.gender) {
|
|
246
|
+
const h = await this.hashGender(raw.gender);
|
|
247
|
+
if (h) result.gender = h;
|
|
248
|
+
}
|
|
249
|
+
if (raw.date_of_birth) {
|
|
250
|
+
const h = await this.hashDateOfBirth(raw.date_of_birth);
|
|
251
|
+
if (h) result.date_of_birth = h;
|
|
252
|
+
}
|
|
253
|
+
if (raw.external_id) {
|
|
254
|
+
const h = await this.hashExternalId(raw.external_id);
|
|
255
|
+
if (h) result.external_id = h;
|
|
256
|
+
}
|
|
257
|
+
if (raw.city) {
|
|
258
|
+
if (this.options.hashAddressFields) {
|
|
259
|
+
const h = await this.hashCity(raw.city);
|
|
260
|
+
if (h) result.city = h;
|
|
261
|
+
} else {
|
|
262
|
+
result.city = raw.city;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (raw.state) {
|
|
266
|
+
if (this.options.hashAddressFields) {
|
|
267
|
+
const h = await this.hashState(raw.state);
|
|
268
|
+
if (h) result.state = h;
|
|
269
|
+
} else {
|
|
270
|
+
result.state = raw.state;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (raw.zip_code) {
|
|
274
|
+
if (this.options.hashAddressFields) {
|
|
275
|
+
const h = await this.hashZipCode(raw.zip_code);
|
|
276
|
+
if (h) result.zip_code = h;
|
|
277
|
+
} else {
|
|
278
|
+
result.zip_code = raw.zip_code;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (raw.country) {
|
|
282
|
+
if (this.options.hashAddressFields) {
|
|
283
|
+
const h = await this.hashCountry(raw.country);
|
|
284
|
+
if (h) result.country = h;
|
|
285
|
+
} else {
|
|
286
|
+
result.country = raw.country;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (raw.ip_address) result.ip_address = raw.ip_address;
|
|
290
|
+
if (raw.user_agent) result.user_agent = raw.user_agent;
|
|
291
|
+
if (raw.subscription_id) result.subscription_id = raw.subscription_id;
|
|
292
|
+
if (raw.lead_id) result.lead_id = raw.lead_id;
|
|
293
|
+
if (raw.anonymous_id) result.anonymous_id = raw.anonymous_id;
|
|
294
|
+
if (raw.traits) result.traits = raw.traits;
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
export {
|
|
299
|
+
Normalizer,
|
|
300
|
+
isSha256,
|
|
301
|
+
sha256
|
|
302
|
+
};
|
|
303
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hash.ts","../src/Normalizer.ts"],"sourcesContent":["/**\n * SHA256 Hashing Utilities\n * \n * Uses Web Crypto API (available in modern browsers and Node.js 18+)\n * Works in both browser and Cloudflare Workers environments.\n * \n * All PII MUST be normalized before hashing to ensure consistent results.\n */\n\nimport type { Sha256Hash } from './types';\n\n/**\n * SHA256 hash a string using Web Crypto API\n * \n * Returns 64 lowercase hex characters.\n * Works in both browser and Node.js 18+ (Web Crypto API).\n * \n * @param input - String to hash (should be normalized first)\n * @returns Promise resolving to 64-character lowercase hex string\n * \n * @example\n * ```typescript\n * const hash = await sha256('hello@example.com');\n * // Returns: \"5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8\"\n * ```\n */\nexport async function sha256(input: string): Promise<Sha256Hash> {\n const encoder = new TextEncoder();\n const data = encoder.encode(input);\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n const hashArray = Array.from(new Uint8Array(hashBuffer));\n const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');\n return hashHex as Sha256Hash;\n}\n\n/**\n * Validate that a string is a valid SHA256 hash\n * \n * Checks for 64 lowercase hexadecimal characters.\n * \n * @param value - String to validate\n * @returns true if valid SHA256 hash format\n * \n * @example\n * ```typescript\n * isSha256('abc123'); // false\n * isSha256('5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8'); // true\n * ```\n */\nexport function isSha256(value: string): value is Sha256Hash {\n return /^[a-f0-9]{64}$/.test(value);\n}\n","/**\n * Normalizer Class\n * \n * Normalizes and hashes PII fields for ad platform APIs (Meta CAPI, Google Ads, etc.)\n * \n * Normalization rules ensure consistent hashing:\n * - Email: lowercase, trim\n * - Phone: E.164 format (+15551234567)\n * - Name: lowercase, trim, single spaces\n * - Gender: single char (m/f)\n * - Date of Birth: YYYYMMDD format\n * - Address: lowercase, trim, format-specific rules\n * \n * Meta CAPI Hashing Requirements:\n * - HASH: email, phone, first_name, last_name, gender, date_of_birth, city, state, zip_code, country, external_id\n * - DO NOT HASH: ip_address, user_agent, fbc, fbp, subscription_id, lead_id\n */\n\nimport type { NormalizerOptions, RawUserData, NormalizedUserData, Sha256Hash } from './types';\nimport { sha256 } from './hash';\n\nexport class Normalizer {\n private options: Required<NormalizerOptions>;\n\n constructor(options: NormalizerOptions = {}) {\n this.options = {\n defaultCountryCode: options.defaultCountryCode ?? '1',\n hashAddressFields: options.hashAddressFields ?? false,\n };\n }\n\n /* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n * NORMALIZATION METHODS\n * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n /**\n * Normalize email: lowercase, trim whitespace\n * Returns null if invalid format\n */\n normalizeEmail(email: string): string | null {\n if (!email || typeof email !== 'string') return null;\n const normalized = email.toLowerCase().trim();\n if (!normalized.includes('@') || normalized.length < 5) return null;\n return normalized;\n }\n\n /**\n * Normalize phone to E.164 format (+15551234567)\n * Returns null if invalid\n */\n normalizePhone(phone: string, countryCode?: string): string | null {\n if (!phone || typeof phone !== 'string') return null;\n const code = countryCode ?? this.options.defaultCountryCode;\n let digits = phone.replace(/[^\\d+]/g, '');\n if (digits.startsWith('+')) digits = digits.slice(1);\n if (digits.length < 10) return null;\n if (digits.length === 10) digits = code + digits;\n return '+' + digits;\n }\n\n /**\n * Normalize name: lowercase, trim, remove extra spaces\n * Returns null if empty\n */\n normalizeName(name: string): string | null {\n if (!name || typeof name !== 'string') return null;\n const normalized = name.toLowerCase().trim().replace(/\\s+/g, ' ');\n if (normalized.length === 0) return null;\n return normalized;\n }\n\n /**\n * Normalize gender: single lowercase char (m/f)\n * Accepts: m, male, f, female\n * Returns null if invalid\n */\n normalizeGender(gender: string): string | null {\n if (!gender || typeof gender !== 'string') return null;\n const g = gender.toLowerCase().trim();\n if (g === 'm' || g === 'male') return 'm';\n if (g === 'f' || g === 'female') return 'f';\n return null;\n }\n\n /**\n * Normalize date of birth: YYYYMMDD format\n * Accepts formats: YYYY-MM-DD, YYYY/MM/DD, YYYY.MM.DD, YYYYMMDD\n * Returns null if invalid\n */\n normalizeDateOfBirth(dob: string): string | null {\n if (!dob || typeof dob !== 'string') return null;\n const cleaned = dob.replace(/[-\\/\\.]/g, '');\n if (!/^\\d{8}$/.test(cleaned)) return null;\n return cleaned;\n }\n\n /**\n * Normalize city: lowercase, trim\n */\n normalizeCity(city: string): string | null {\n if (!city || typeof city !== 'string') return null;\n const normalized = city.toLowerCase().trim();\n if (normalized.length === 0) return null;\n return normalized;\n }\n\n /**\n * Normalize state/region: lowercase, trim\n */\n normalizeState(state: string): string | null {\n if (!state || typeof state !== 'string') return null;\n const normalized = state.toLowerCase().trim();\n if (normalized.length === 0) return null;\n return normalized;\n }\n\n /**\n * Normalize zip/postal code: lowercase, no spaces\n */\n normalizeZipCode(zip: string): string | null {\n if (!zip || typeof zip !== 'string') return null;\n const normalized = zip.toLowerCase().trim().replace(/\\s+/g, '');\n if (normalized.length === 0) return null;\n return normalized;\n }\n\n /**\n * Normalize country: 2-letter code, lowercase\n * Returns null if not exactly 2 characters\n */\n normalizeCountry(country: string): string | null {\n if (!country || typeof country !== 'string') return null;\n const normalized = country.toLowerCase().trim();\n if (normalized.length !== 2) return null;\n return normalized;\n }\n\n /* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n * HASH METHODS\n * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n /**\n * Hash email (normalize then hash)\n */\n async hashEmail(email: string): Promise<Sha256Hash | null> {\n const normalized = this.normalizeEmail(email);\n return normalized ? sha256(normalized) : null;\n }\n\n /**\n * Hash phone (normalize then hash)\n */\n async hashPhone(phone: string, countryCode?: string): Promise<Sha256Hash | null> {\n const normalized = this.normalizePhone(phone, countryCode);\n return normalized ? sha256(normalized) : null;\n }\n\n /**\n * Hash name (normalize then hash)\n */\n async hashName(name: string): Promise<Sha256Hash | null> {\n const normalized = this.normalizeName(name);\n return normalized ? sha256(normalized) : null;\n }\n\n /**\n * Hash gender (normalize then hash)\n */\n async hashGender(gender: string): Promise<Sha256Hash | null> {\n const normalized = this.normalizeGender(gender);\n return normalized ? sha256(normalized) : null;\n }\n\n /**\n * Hash date of birth (normalize then hash)\n */\n async hashDateOfBirth(dob: string): Promise<Sha256Hash | null> {\n const normalized = this.normalizeDateOfBirth(dob);\n return normalized ? sha256(normalized) : null;\n }\n\n /**\n * Hash city (normalize then hash)\n */\n async hashCity(city: string): Promise<Sha256Hash | null> {\n const normalized = this.normalizeCity(city);\n return normalized ? sha256(normalized) : null;\n }\n\n /**\n * Hash state (normalize then hash)\n */\n async hashState(state: string): Promise<Sha256Hash | null> {\n const normalized = this.normalizeState(state);\n return normalized ? sha256(normalized) : null;\n }\n\n /**\n * Hash zip code (normalize then hash)\n */\n async hashZipCode(zip: string): Promise<Sha256Hash | null> {\n const normalized = this.normalizeZipCode(zip);\n return normalized ? sha256(normalized) : null;\n }\n\n /**\n * Hash country (normalize then hash)\n */\n async hashCountry(country: string): Promise<Sha256Hash | null> {\n const normalized = this.normalizeCountry(country);\n return normalized ? sha256(normalized) : null;\n }\n\n /**\n * Hash external ID (trim only, no other normalization)\n */\n async hashExternalId(externalId: string): Promise<Sha256Hash | null> {\n if (!externalId || typeof externalId !== 'string') return null;\n return sha256(externalId.trim());\n }\n\n /* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n * BATCH NORMALIZATION\n * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */\n\n /**\n * Normalize and hash all user data\n * \n * Raw PII in, hashed data out.\n * \n * @param raw - Raw user data (unhashed PII)\n * @returns Normalized and hashed user data\n * \n * @example\n * ```typescript\n * const normalizer = new Normalizer();\n * const normalized = await normalizer.normalize({\n * email: 'JOHN@EXAMPLE.COM',\n * phone: '555-123-4567',\n * first_name: 'John',\n * last_name: 'Doe',\n * city: 'San Francisco',\n * state: 'CA',\n * country: 'US',\n * });\n * \n * // Result:\n * // {\n * // email: '5e884898...', (hashed)\n * // phone: 'a1b2c3d4...', (hashed)\n * // first_name: 'e5f6g7h8...', (hashed)\n * // last_name: 'i9j0k1l2...', (hashed)\n * // city: 'san francisco', (normalized, not hashed by default)\n * // state: 'ca', (normalized, not hashed by default)\n * // country: 'us', (normalized, not hashed by default)\n * // }\n * ```\n */\n async normalize(raw: RawUserData): Promise<NormalizedUserData> {\n const result: NormalizedUserData = {};\n\n // Hash contact PII\n if (raw.email) {\n const h = await this.hashEmail(raw.email);\n if (h) result.email = h;\n }\n if (raw.phone) {\n const h = await this.hashPhone(raw.phone);\n if (h) result.phone = h;\n }\n if (raw.first_name) {\n const h = await this.hashName(raw.first_name);\n if (h) result.first_name = h;\n }\n if (raw.last_name) {\n const h = await this.hashName(raw.last_name);\n if (h) result.last_name = h;\n }\n if (raw.gender) {\n const h = await this.hashGender(raw.gender);\n if (h) result.gender = h;\n }\n if (raw.date_of_birth) {\n const h = await this.hashDateOfBirth(raw.date_of_birth);\n if (h) result.date_of_birth = h;\n }\n if (raw.external_id) {\n const h = await this.hashExternalId(raw.external_id);\n if (h) result.external_id = h;\n }\n\n // Address fields - hash only if configured to do so\n if (raw.city) {\n if (this.options.hashAddressFields) {\n const h = await this.hashCity(raw.city);\n if (h) result.city = h as unknown as string;\n } else {\n result.city = raw.city;\n }\n }\n if (raw.state) {\n if (this.options.hashAddressFields) {\n const h = await this.hashState(raw.state);\n if (h) result.state = h as unknown as string;\n } else {\n result.state = raw.state;\n }\n }\n if (raw.zip_code) {\n if (this.options.hashAddressFields) {\n const h = await this.hashZipCode(raw.zip_code);\n if (h) result.zip_code = h as unknown as string;\n } else {\n result.zip_code = raw.zip_code;\n }\n }\n if (raw.country) {\n if (this.options.hashAddressFields) {\n const h = await this.hashCountry(raw.country);\n if (h) result.country = h as unknown as string;\n } else {\n result.country = raw.country;\n }\n }\n\n // Pass through unhashed fields\n if (raw.ip_address) result.ip_address = raw.ip_address;\n if (raw.user_agent) result.user_agent = raw.user_agent;\n if (raw.subscription_id) result.subscription_id = raw.subscription_id;\n if (raw.lead_id) result.lead_id = raw.lead_id;\n if (raw.anonymous_id) result.anonymous_id = raw.anonymous_id;\n if (raw.traits) result.traits = raw.traits;\n\n return result;\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hammr/normalizer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "PII normalization and SHA256 hashing for Meta CAPI and ad platforms",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"normalization",
|
|
21
|
+
"pii",
|
|
22
|
+
"hashing",
|
|
23
|
+
"sha256",
|
|
24
|
+
"meta",
|
|
25
|
+
"capi",
|
|
26
|
+
"privacy",
|
|
27
|
+
"email",
|
|
28
|
+
"phone",
|
|
29
|
+
"e164",
|
|
30
|
+
"hmac-sha256"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsup",
|
|
34
|
+
"test": "vitest run",
|
|
35
|
+
"test:watch": "vitest",
|
|
36
|
+
"test:coverage": "vitest run --coverage",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"prepublishOnly": "npm run build"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^20.10.0",
|
|
42
|
+
"tsup": "^8.0.1",
|
|
43
|
+
"typescript": "^5.3.3",
|
|
44
|
+
"vitest": "^1.0.4",
|
|
45
|
+
"@vitest/coverage-v8": "^1.0.4"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=18"
|
|
49
|
+
},
|
|
50
|
+
"author": "Edge Foundry, Inc.",
|
|
51
|
+
"license": "Apache-2.0"
|
|
52
|
+
}
|