@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 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":[]}
@@ -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 };
@@ -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
+ }