@webaround/openai-ads 0.1.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/LICENSE +21 -0
- package/README.md +149 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +15 -0
- package/dist/errors.js.map +1 -0
- package/dist/eventId.d.ts +26 -0
- package/dist/eventId.d.ts.map +1 -0
- package/dist/eventId.js +56 -0
- package/dist/eventId.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/pixel.d.ts +107 -0
- package/dist/pixel.d.ts.map +1 -0
- package/dist/pixel.js +305 -0
- package/dist/pixel.js.map +1 -0
- package/dist/spec.d.ts +26 -0
- package/dist/spec.d.ts.map +1 -0
- package/dist/spec.js +68 -0
- package/dist/spec.js.map +1 -0
- package/dist/types.d.ts +107 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/userData.d.ts +68 -0
- package/dist/userData.d.ts.map +1 -0
- package/dist/userData.js +172 -0
- package/dist/userData.js.map +1 -0
- package/package.json +57 -0
package/dist/userData.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { OpenAIAdsError } from './errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Identity normalization and hashing in the browser.
|
|
4
|
+
*
|
|
5
|
+
* These rules must produce byte-identical digests to the PHP core, because a
|
|
6
|
+
* Pixel event and its Conversions API twin describe the same person. Both
|
|
7
|
+
* implementations are pinned by
|
|
8
|
+
* `packages/spec/fixtures/normalization.cases.json`, and both test suites read
|
|
9
|
+
* that file.
|
|
10
|
+
*
|
|
11
|
+
* Raw values never leave this module: `hashUser()` returns digests, and nothing
|
|
12
|
+
* here writes a raw identifier to the DOM, to storage, or to the console.
|
|
13
|
+
*/
|
|
14
|
+
/** ASCII punctuation. Non-ASCII characters are preserved, as the spec requires. */
|
|
15
|
+
const ASCII_PUNCTUATION = /[!-/:-@[-`{-~]/g;
|
|
16
|
+
/** Mirrors `normalizations.city_region.max_length` in packages/spec/user.json. */
|
|
17
|
+
const CITY_REGION_MAX_LENGTH = 128;
|
|
18
|
+
/** Mirrors `normalizations.postal_code.max_length` in packages/spec/user.json. */
|
|
19
|
+
const POSTAL_CODE_MAX_LENGTH = 32;
|
|
20
|
+
export function normalizeEmail(value) {
|
|
21
|
+
return value.trim().toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Remove the four documented separators, then a leading '+', then leading
|
|
25
|
+
* zeroes - and nothing else.
|
|
26
|
+
*
|
|
27
|
+
* Upstream documents '8-15 digits after removing a leading +, leading zeroes,
|
|
28
|
+
* whitespace, parentheses, periods, and hyphens'. Removing every non-digit
|
|
29
|
+
* instead looks equivalent and is not: '+1 (555) 123-4567 ext. 89' would become
|
|
30
|
+
* '1555123456789', which passes a length check and hashes to nobody. Whatever
|
|
31
|
+
* is left over is returned as-is so the caller can refuse it.
|
|
32
|
+
*
|
|
33
|
+
* Dialling plans are still not parsed: '+00 44 (0)20 7946 0958' becomes
|
|
34
|
+
* '4402079460958'.
|
|
35
|
+
*/
|
|
36
|
+
export function normalizePhone(value) {
|
|
37
|
+
return value
|
|
38
|
+
.trim()
|
|
39
|
+
.replace(/[\s().-]/g, '')
|
|
40
|
+
.replace(/^\+/, '')
|
|
41
|
+
.replace(/^0+/, '');
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* ISO 3166-1 alpha-2, uppercased.
|
|
45
|
+
*
|
|
46
|
+
* Returns undefined for anything that is not two ASCII letters. A country name
|
|
47
|
+
* rather than a code is dropped by the API without an error, so sending one
|
|
48
|
+
* would look like matching data and be nothing of the sort.
|
|
49
|
+
*/
|
|
50
|
+
export function normalizeCountry(value) {
|
|
51
|
+
const trimmed = value.trim();
|
|
52
|
+
return /^[A-Za-z]{2}$/.test(trimmed) ? trimmed.toUpperCase() : undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Trim, lowercase, cap at 128 characters - what the API does on receipt.
|
|
56
|
+
*
|
|
57
|
+
* Applied here too so the string sent is the string stored, and so the Pixel
|
|
58
|
+
* and the Conversions API carry the same one for the same person.
|
|
59
|
+
*/
|
|
60
|
+
export function normalizeCityOrRegion(value) {
|
|
61
|
+
return value.trim().toLowerCase().slice(0, CITY_REGION_MAX_LENGTH);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Reduce to letters, digits, spaces and hyphens, cap at 32 characters.
|
|
65
|
+
*
|
|
66
|
+
* Disallowed characters are removed rather than refused - a stray period is a
|
|
67
|
+
* formatting artefact. Case is deliberately not folded: upstream states a
|
|
68
|
+
* lowercase rule for cities and regions and states none here.
|
|
69
|
+
*/
|
|
70
|
+
export function normalizePostalCode(value) {
|
|
71
|
+
return value
|
|
72
|
+
.trim()
|
|
73
|
+
.replace(/[^A-Za-z0-9 -]/g, '')
|
|
74
|
+
.slice(0, POSTAL_CODE_MAX_LENGTH)
|
|
75
|
+
.trim();
|
|
76
|
+
}
|
|
77
|
+
export function normalizeExternalId(value) {
|
|
78
|
+
// Case is preserved deliberately.
|
|
79
|
+
return value.trim();
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Lowercase, then strip whitespace and ASCII punctuation.
|
|
83
|
+
*
|
|
84
|
+
* `toLowerCase()` is Unicode-aware, which is what the PHP side gets from
|
|
85
|
+
* `mb_strtolower`. A byte-wise lowercase there would leave a diacritic
|
|
86
|
+
* untouched and produce a digest that never matches this one.
|
|
87
|
+
*/
|
|
88
|
+
export function normalizeName(value) {
|
|
89
|
+
return value.trim().toLowerCase().replace(/\s+/gu, '').replace(ASCII_PUNCTUATION, '');
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* SHA-256 as lowercase hexadecimal.
|
|
93
|
+
*
|
|
94
|
+
* Web Crypto is asynchronous and only available in a secure context, so this is
|
|
95
|
+
* async and fails loudly on plain HTTP rather than degrading to a weaker hash or
|
|
96
|
+
* - far worse - sending the raw value.
|
|
97
|
+
*/
|
|
98
|
+
export async function sha256Hex(value) {
|
|
99
|
+
const subtle = globalThis.crypto?.subtle;
|
|
100
|
+
if (subtle === undefined) {
|
|
101
|
+
throw new OpenAIAdsError('Web Crypto is unavailable, so identity cannot be hashed in the browser. ' +
|
|
102
|
+
'This usually means the page is not served over HTTPS. Raw values are never sent.');
|
|
103
|
+
}
|
|
104
|
+
const digest = await subtle.digest('SHA-256', new TextEncoder().encode(value));
|
|
105
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Normalize and hash raw identity into the Pixel's shape.
|
|
109
|
+
*
|
|
110
|
+
* Pass the result to `init({ user })` when the person becomes known - after a
|
|
111
|
+
* login, a checkout, a lead submission. Geographic values are sent unhashed, as
|
|
112
|
+
* documented.
|
|
113
|
+
*
|
|
114
|
+
* Values that normalize away to nothing are dropped rather than hashed: the
|
|
115
|
+
* digest of an empty string is a valid-looking value that matches nobody.
|
|
116
|
+
*/
|
|
117
|
+
export async function hashUser(raw) {
|
|
118
|
+
const user = {};
|
|
119
|
+
const hashed = [
|
|
120
|
+
['email_sha256', raw.email, normalizeEmail],
|
|
121
|
+
['phone_number_sha256', raw.phone, normalizePhone],
|
|
122
|
+
['external_id_sha256', raw.externalId, normalizeExternalId],
|
|
123
|
+
['first_name_sha256', raw.firstName, normalizeName],
|
|
124
|
+
['last_name_sha256', raw.lastName, normalizeName],
|
|
125
|
+
];
|
|
126
|
+
for (const [key, value, normalize] of hashed) {
|
|
127
|
+
if (value === undefined || value === null) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const normalized = normalize(value);
|
|
131
|
+
if (normalized === '') {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (key === 'phone_number_sha256') {
|
|
135
|
+
// Both messages deliberately omit the number itself.
|
|
136
|
+
if (!/^[0-9]*$/.test(normalized)) {
|
|
137
|
+
throw new OpenAIAdsError('phone must contain only digits once whitespace, parentheses, periods, hyphens, ' +
|
|
138
|
+
'a leading plus and leading zeroes are removed. An extension or a letter is not ' +
|
|
139
|
+
'stripped, because stripping it would silently hash a different number.');
|
|
140
|
+
}
|
|
141
|
+
if (normalized.length < 8 || normalized.length > 15) {
|
|
142
|
+
throw new OpenAIAdsError(`phone must contain between 8 and 15 digits after normalization; got ${normalized.length}.`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
user[key] = await sha256Hex(normalized);
|
|
146
|
+
}
|
|
147
|
+
// Geographic values are sent unhashed but NOT unnormalized: the API documents
|
|
148
|
+
// a rule for each and drops a value that does not satisfy it, silently.
|
|
149
|
+
if (raw.country !== undefined && raw.country.trim() !== '') {
|
|
150
|
+
const country = normalizeCountry(raw.country);
|
|
151
|
+
if (country === undefined) {
|
|
152
|
+
throw new OpenAIAdsError(`country must be a two-letter ISO 3166-1 alpha-2 code such as "US"; got "${raw.country.trim()}".`);
|
|
153
|
+
}
|
|
154
|
+
user.country = country;
|
|
155
|
+
}
|
|
156
|
+
const geographic = [
|
|
157
|
+
['city', raw.city, normalizeCityOrRegion],
|
|
158
|
+
['region', raw.region, normalizeCityOrRegion],
|
|
159
|
+
['postal_code', raw.postalCode, normalizePostalCode],
|
|
160
|
+
];
|
|
161
|
+
for (const [key, value, normalize] of geographic) {
|
|
162
|
+
if (value === undefined || value === null) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const normalized = normalize(value);
|
|
166
|
+
if (normalized !== '') {
|
|
167
|
+
user[key] = normalized;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return user;
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=userData.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"userData.js","sourceRoot":"","sources":["../src/userData.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@webaround/openai-ads",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenAI Ads Measurement Pixel for the browser: a typed, consent-aware wrapper over the official oaiq SDK, with Pixel/Conversions API deduplication.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"openai",
|
|
7
|
+
"openai-ads",
|
|
8
|
+
"pixel",
|
|
9
|
+
"oaiq",
|
|
10
|
+
"conversions-api",
|
|
11
|
+
"measurement",
|
|
12
|
+
"analytics"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/webaroundlabs/openai-ads-toolkit",
|
|
15
|
+
"bugs": "https://github.com/webaroundlabs/openai-ads-toolkit/issues",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/webaroundlabs/openai-ads-toolkit.git",
|
|
19
|
+
"directory": "packages/js"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"import": "./dist/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"main": "./dist/index.js",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=20"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc -p tsconfig.build.json",
|
|
41
|
+
"typecheck": "tsc --noEmit",
|
|
42
|
+
"lint": "eslint .",
|
|
43
|
+
"lint:fix": "eslint . --fix",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"test:watch": "vitest",
|
|
46
|
+
"check": "npm run lint && npm run typecheck && npm test"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@eslint/js": "^9.39.5",
|
|
50
|
+
"@types/node": "^22.20.2",
|
|
51
|
+
"eslint": "^9.39.5",
|
|
52
|
+
"jsdom": "^25.0.1",
|
|
53
|
+
"typescript": "^5.6.3",
|
|
54
|
+
"typescript-eslint": "^8.70.0",
|
|
55
|
+
"vitest": "^5.0.0"
|
|
56
|
+
}
|
|
57
|
+
}
|