@blotoutio/providers-dataflywheel-sdk 1.75.1 → 1.76.1

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.
Files changed (4) hide show
  1. package/index.cjs.js +145 -345
  2. package/index.js +145 -345
  3. package/index.mjs +145 -345
  4. package/package.json +1 -1
package/index.cjs.js CHANGED
@@ -1,385 +1,185 @@
1
1
  'use strict';
2
2
 
3
- const packageName = 'dataFlywheel';
4
- // Browser identifiers the edge persists against the EdgeTag user id, the way
5
- // Facebook stores _fbp/_fbc. The identify request carries no providerData and
6
- // dfw_nid is third-party to the merchant, so this is n_id's only route there.
7
- const storedNIdKey = 'n_id';
8
- const storedSessionIdKey = 'session_id';
9
- const storedSessionExpiryKey = 'session_expires_at';
10
- // The same identifiers published into the EdgeTag SDK's kv store, which rides
11
- // on every request including identify. Namespaced the way the edge splits them
12
- // back apart, as Facebook does with facebook::_fbp.
13
- const kvKey = (key) => `${packageName}::${key}`;
14
- const sessionStateKeyPrefix = 'dfw_sdk_state';
15
- const sessionBootstrapPath = 'v1/session';
3
+ // A network ID is 128 random bits, spelled like Qinto's IDs: `n_` and a hyphenated UUID. Earlier IDs, still in some
4
+ // cookies and copies, are `n_<32 hex>`; they are read, and spelled the new way from then on. PlanetScale stores the UUID.
5
+ // Same rules as qinto-apps/data-flywheel/src/nid.ts; keep the two in step.
6
+ const NID_RE = /^n_([0-9a-f]{8})-?([0-9a-f]{4})-?([0-9a-f]{4})-?([0-9a-f]{4})-?([0-9a-f]{12})$/;
7
+ // The ID in its current spelling, or null when the value is not a network ID.
8
+ const toNid = (value) => {
9
+ const parts = typeof value === 'string' ? NID_RE.exec(value) : null;
10
+ return parts === null ? null : `n_${parts.slice(1).join('-')}`;
11
+ };
16
12
 
17
- const keyPrefix = `_worker`;
13
+ const packageName = 'dataFlywheel';
14
+ // The provider's own endpoints on the merchant's EdgeTag Worker (cdn/src/lib/api.ts), named like the network Worker's:
15
+ // /nid for approach 1, /nid1 for approach 2.
16
+ const nidPath = '/providers/dataFlywheel/nid';
17
+ const nidLookupPath = '/providers/dataFlywheel/nid1';
18
18
 
19
- const canLog = () => {
20
- try {
21
- return localStorage.getItem('edgeTagDebug') === '1';
22
- }
23
- catch {
24
- return false;
25
- }
26
- };
27
- const prefix = `[EdgeTag]`;
28
- const logger = {
29
- log: (...args) => {
30
- if (canLog()) {
31
- console.log(prefix, ...args);
19
+ // eslint-disable-next-line @nx/enforce-module-boundaries
20
+ // The network ID, shared by every Data Flywheel merchant in one browser (on Qinto and EdgeTag), lives in a cookie on the
21
+ // network domain, which the network Worker reads and keeps. The same two approaches as the Qinto app
22
+ // (qinto-apps/data-flywheel/src/network.ts), each with its own cookie, so they can be compared:
23
+ // - approach 1, the primary (`dfw_nid`): the Worker mints an ID on the spot, with no lookup anywhere;
24
+ // - approach 2, the lookup (`dfw_nid_1`): the provider's endpoint on the merchant's EdgeTag Worker mints it, handing a
25
+ // visitor back the ID it had before, so a cleared cookie comes back the same.
26
+ // Each merchant keeps its own copy in localStorage, with when it last checked it (`<key>_at`). A page whose copy was
27
+ // checked in the last 7 days does nothing more: it calls neither the Worker nor the endpoint. Any other page (a first
28
+ // visit, cleared storage, a copy from before the timestamp, or 7 days on) runs the approach, which also sets the cookie
29
+ // again so its lifetime starts over, and checks the copy afresh. The endpoint only hears about an ID this merchant has
30
+ // not kept before.
31
+ const WAIT_MS = 5000;
32
+ const RECHECK_MS = 7 * 24 * 60 * 60 * 1000;
33
+ // Storage can be missing or refuse writes (private windows, blocked site data), which makes every page due.
34
+ const localCache = (key, userId) => {
35
+ const get = () => {
36
+ try {
37
+ return toNid(localStorage.getItem(key));
32
38
  }
33
- },
34
- error: (...args) => {
35
- if (canLog()) {
36
- console.error(prefix, ...args);
39
+ catch {
40
+ return null;
37
41
  }
38
- },
39
- info: (...args) => {
40
- if (canLog()) {
41
- console.info(prefix, ...args);
42
+ };
43
+ const owned = () => {
44
+ try {
45
+ return localStorage.getItem(`${key}_uid`) === userId;
42
46
  }
43
- },
44
- trace: (...args) => {
45
- if (canLog()) {
46
- console.trace(prefix, ...args);
47
+ catch {
48
+ return false;
47
49
  }
48
- },
49
- };
50
-
51
- const initKey = `${keyPrefix}StoreMultiple`;
52
- const saveKV = (destination, data) => {
53
- const currentSession = getData(destination, 'session');
54
- if (!currentSession['kv']) {
55
- currentSession['kv'] = {};
56
- }
57
- currentSession['kv'] = {
58
- ...currentSession['kv'],
59
- ...data,
60
50
  };
61
- saveData(destination, 'session', currentSession);
62
- };
63
- const saveData = (destination, persistType, value, key = initKey) => {
64
- if (persistType === 'session') {
65
- const data = getSession(key);
66
- data[destination] = value;
67
- saveSession(data, key);
68
- return;
69
- }
70
- const data = getLocal(key);
71
- data[destination] = value;
72
- saveLocal(data, key);
73
- };
74
- const getData = (destination, persistType, key = initKey) => {
75
- let data;
76
- if (persistType === 'session') {
77
- data = getSession(key);
78
- }
79
- else {
80
- data = getLocal(key);
81
- }
82
- return (data === null || data === void 0 ? void 0 : data[destination]) || {};
51
+ return {
52
+ get,
53
+ owned,
54
+ set: (nId) => {
55
+ try {
56
+ localStorage.setItem(key, nId);
57
+ localStorage.setItem(`${key}_at`, String(Date.now()));
58
+ localStorage.setItem(`${key}_uid`, userId);
59
+ }
60
+ catch {
61
+ // nothing to keep
62
+ }
63
+ },
64
+ due: () => {
65
+ try {
66
+ const age = Date.now() - Number(localStorage.getItem(`${key}_at`));
67
+ return !owned() || get() === null || !(age >= 0 && age < RECHECK_MS);
68
+ }
69
+ catch {
70
+ return true;
71
+ }
72
+ },
73
+ };
83
74
  };
84
- const saveLocal = (value, key) => {
85
- try {
86
- if (!localStorage) {
87
- return;
88
- }
89
- localStorage.setItem(key, JSON.stringify(value));
90
- }
91
- catch {
92
- logger.log('Local storage not supported.');
93
- }
75
+ const withTimeout = (work) => new Promise((resolve, reject) => {
76
+ const timer = setTimeout(() => reject(new Error('timeout')), WAIT_MS);
77
+ work.then(resolve, reject).finally(() => clearTimeout(timer));
78
+ });
79
+ // One JSON call, answering its `nId` when it is a well-formed ID.
80
+ const nIdOf = async (response) => {
81
+ const body = (await withTimeout((await withTimeout(response)).json()));
82
+ return toNid(body.nId);
83
+ };
84
+ const endpointFor = (destination, userId) => (path, init = {}) => {
85
+ const headers = new Headers(init.headers);
86
+ if (userId) {
87
+ headers.set('EdgeTagUserId', userId);
88
+ }
89
+ return fetch(new URL(path, destination).toString(), {
90
+ ...init,
91
+ headers,
92
+ credentials: 'include',
93
+ });
94
94
  };
95
- const getLocal = (key) => {
95
+ const recorder = (endpoint, path) => async (nId) => {
96
+ const response = await withTimeout(endpoint(path, {
97
+ method: 'POST',
98
+ headers: { 'content-type': 'application/json' },
99
+ body: JSON.stringify({ nId, pageUrl: location.href }),
100
+ }));
101
+ return ((await withTimeout(response.json())).linked ===
102
+ true);
103
+ };
104
+ const workerNetwork = (origin) => (existing) => nIdOf(fetch(`${origin}/nid${existing === null ? '' : `?existing=${existing}`}`, { credentials: 'include' }));
105
+ const runNetworkCheck = async (network, record, cache) => {
96
106
  try {
97
- if (!localStorage) {
98
- return {};
99
- }
100
- const data = localStorage.getItem(key);
101
- if (!data) {
102
- return {};
107
+ const copy = cache.get();
108
+ const nId = await network(copy);
109
+ if (nId !== null &&
110
+ ((nId === copy && cache.owned()) || (await record(nId)))) {
111
+ cache.set(nId);
103
112
  }
104
- return JSON.parse(data) || {};
105
113
  }
106
114
  catch {
107
- return {};
108
- }
109
- };
110
- const saveSession = (value, key) => {
115
+ // fails open
116
+ }
117
+ };
118
+ const workerLookup = (origin) => ({
119
+ read: (existing) => nIdOf(fetch(`${origin}/nid1${existing === null ? '' : `?existing=${existing}`}`, {
120
+ credentials: 'include',
121
+ })),
122
+ set: (nId) => nIdOf(fetch(`${origin}/nid1`, {
123
+ method: 'POST',
124
+ credentials: 'include',
125
+ body: JSON.stringify({ set: nId }),
126
+ })),
127
+ });
128
+ const lookupServer = (endpoint) => ({
129
+ lookup: () => nIdOf(endpoint(nidLookupPath)),
130
+ confirm: recorder(endpoint, nidLookupPath),
131
+ });
132
+ const runLookupCheck = async (network, server, cache) => {
111
133
  try {
112
- if (!sessionStorage) {
134
+ const copy = cache.get();
135
+ const held = await network.read(copy);
136
+ if (held !== null) {
137
+ if ((held === copy && cache.owned()) || (await server.confirm(held))) {
138
+ cache.set(held);
139
+ }
113
140
  return;
114
141
  }
115
- sessionStorage.setItem(key, JSON.stringify(value));
116
- }
117
- catch {
118
- logger.log('Session storage not supported.');
119
- }
120
- };
121
- const getSession = (key) => {
122
- try {
123
- if (!sessionStorage) {
124
- return {};
125
- }
126
- const data = sessionStorage.getItem(key);
127
- if (!data) {
128
- return {};
142
+ const nId = await server.lookup();
143
+ if (nId !== null && (await network.set(nId)) === nId) {
144
+ cache.set(nId);
129
145
  }
130
- return JSON.parse(data) || {};
131
146
  }
132
147
  catch {
133
- return {};
148
+ // fails open
134
149
  }
135
150
  };
136
-
137
- // eslint-disable-next-line @nx/enforce-module-boundaries
138
- // Coalesces concurrent cold bootstraps per merchant: events firing in the same
139
- // tick share one in-flight /v1/session call instead of racing N handshakes.
140
- const inFlight = new Map();
141
- const memoryStates = new Map();
142
- const stringValue = (value) => typeof value === 'string' && value.trim() ? value.trim() : undefined;
143
- const numberValue = (value) => typeof value === 'number' && Number.isFinite(value) ? value : undefined;
144
- const isEnabledFlag = (value, defaultValue = true) => {
145
- if (value == null || value === '')
146
- return defaultValue;
147
- return value !== '0' && value.toLowerCase() !== 'false';
148
- };
149
- const shouldBootstrap = (now, expiresAt) => expiresAt == null || now >= expiresAt;
150
- const browserOrigin = () => {
151
- var _a, _b;
152
- try {
153
- if (typeof window === 'undefined')
154
- return null;
155
- return typeof window.origin === 'string'
156
- ? window.origin
157
- : (_b = (_a = window.location) === null || _a === void 0 ? void 0 : _a.origin) !== null && _b !== void 0 ? _b : null;
158
- }
159
- catch {
160
- // If a sandbox prevents origin inspection, fail closed before issuing a
161
- // credentialed request that the DFW Worker cannot safely authorize.
162
- return 'null';
151
+ const startNetworkId = (endpoint, network, userId) => {
152
+ const cache = localCache('dfw_nid', userId);
153
+ if (cache.due()) {
154
+ void runNetworkCheck(network, recorder(endpoint, nidPath), cache);
163
155
  }
164
156
  };
165
- const stateKey = (merchantId) => `${sessionStateKeyPrefix}:${merchantId}`;
166
- const browserStorage = () => {
167
- var _a;
168
- try {
169
- if (typeof window === 'undefined')
170
- return null;
171
- return (_a = window.sessionStorage) !== null && _a !== void 0 ? _a : null;
172
- }
173
- catch {
174
- return null;
175
- }
176
- };
177
- const sessionStorageFor = (storage) => (storage === undefined ? browserStorage() : storage);
178
- const readSessionState = (storage, merchantId) => {
179
- var _a;
180
- const key = stateKey(merchantId);
181
- const fallback = () => { var _a; return (_a = memoryStates.get(key)) !== null && _a !== void 0 ? _a : null; };
182
- if (!storage)
183
- return fallback();
184
- try {
185
- const raw = storage.getItem(key);
186
- if (!raw)
187
- return fallback();
188
- const parsed = JSON.parse(raw);
189
- const nId = stringValue(parsed.n_id);
190
- const sessionId = stringValue(parsed.session_id);
191
- const expiresAt = stringValue(parsed.expires_at);
192
- if (!nId || !sessionId || !expiresAt)
193
- return fallback();
194
- return {
195
- n_id: nId,
196
- session_id: sessionId,
197
- expires_at: expiresAt,
198
- last_touch_at: (_a = numberValue(parsed.last_touch_at)) !== null && _a !== void 0 ? _a : 0,
199
- };
200
- }
201
- catch {
202
- return fallback();
157
+ const startLookupNetworkId = (endpoint, network, userId) => {
158
+ const cache = localCache('dfw_nid_1', userId);
159
+ if (cache.due()) {
160
+ void runLookupCheck(network, lookupServer(endpoint), cache);
203
161
  }
204
162
  };
205
- const writeSessionState = (storage, merchantId, state) => {
206
- const key = stateKey(merchantId);
207
- if (!storage) {
208
- memoryStates.set(key, state);
209
- return;
210
- }
211
- try {
212
- storage.setItem(key, JSON.stringify(state));
213
- }
214
- catch {
215
- memoryStates.set(key, state);
216
- // Storage can be disabled or full. The worker cookies remain authoritative.
217
- }
218
- };
219
- function expiresAtMs(state) {
220
- if (!state)
221
- return null;
222
- const expiresAt = Date.parse(state.expires_at);
223
- return Number.isFinite(expiresAt) ? expiresAt : null;
224
- }
225
- // The dfw_nid cookie belongs to the DataFlywheel identity host, so the merchant's
226
- // edge never receives it. Publishing into the SDK's kv store puts the same values
227
- // on every request body instead -- identify included, which providerData is not.
228
- const published = new Map();
229
- function publishToKv(destination, state) {
230
- if (!destination || !state)
231
- return;
232
- // saveKV rewrites the whole multi-destination blob synchronously, and the
233
- // cached path runs on every event, so skip when nothing moved.
234
- const last = published.get(destination);
235
- if (last &&
236
- last.n_id === state.n_id &&
237
- last.session_id === state.session_id &&
238
- last.expires_at === state.expires_at) {
163
+
164
+ // Every page load: each network ID approach runs unless the merchant already keeps its copy (network.ts).
165
+ const init = ({ manifest, destination, userId, }) => {
166
+ const variables = manifest.variables;
167
+ if (!(variables === null || variables === void 0 ? void 0 : variables.networkOrigin)) {
239
168
  return;
240
169
  }
241
- published.set(destination, {
242
- n_id: state.n_id,
243
- session_id: state.session_id,
244
- expires_at: state.expires_at,
245
- });
246
- try {
247
- saveKV(destination, {
248
- [kvKey(storedNIdKey)]: state.n_id,
249
- [kvKey(storedSessionIdKey)]: state.session_id,
250
- [kvKey(storedSessionExpiryKey)]: state.expires_at,
251
- });
252
- }
253
- catch {
254
- // Storage can be unavailable; the providerData and provider-key routes remain.
255
- }
256
- }
257
- function bootstrapUrl(input) {
258
- const base = input.dataFlywheelUrl.endsWith('/')
259
- ? input.dataFlywheelUrl
260
- : `${input.dataFlywheelUrl}/`;
261
- const url = new URL(sessionBootstrapPath, base);
262
- url.searchParams.set('merchant_id', input.merchantId);
263
- const pageUrl = stringValue(input.pageUrl);
264
- if (pageUrl) {
265
- url.searchParams.set('page_url', pageUrl);
266
- }
267
- return url.toString();
268
- }
269
- function parseBootstrapResponse(body, now) {
270
- var _a, _b;
271
- const nId = stringValue(body.n_id);
272
- const sessionId = stringValue((_a = body.session) === null || _a === void 0 ? void 0 : _a.session_id);
273
- const expiresAt = stringValue((_b = body.session) === null || _b === void 0 ? void 0 : _b.expires_at);
274
- if (!nId || !sessionId || !expiresAt)
275
- return null;
276
- return {
277
- n_id: nId,
278
- session_id: sessionId,
279
- expires_at: expiresAt,
280
- last_touch_at: now,
281
- };
282
- }
283
- async function bootstrapSession(input) {
284
- try {
285
- const response = await input.fetcher(bootstrapUrl({
286
- dataFlywheelUrl: input.dataFlywheelUrl,
287
- merchantId: input.merchantId,
288
- pageUrl: input.pageUrl,
289
- }), { credentials: 'include' });
290
- if (!response.ok)
291
- return null;
292
- const state = parseBootstrapResponse((await response.json()), input.now);
293
- if (!state)
294
- return null;
295
- writeSessionState(input.storage, input.merchantId, state);
296
- return state;
297
- }
298
- catch {
299
- return null;
300
- }
301
- }
302
- function ensureSession(input) {
303
- var _a, _b, _c, _d;
304
- const variables = (_a = input.variables) !== null && _a !== void 0 ? _a : {};
305
- const dataFlywheelUrl = stringValue(variables.dataFlywheelUrl);
306
- const merchantId = stringValue(variables.merchantId);
307
- if (!dataFlywheelUrl ||
308
- !merchantId ||
309
- !isEnabledFlag(variables.dataFlywheelSessionEnabled)) {
310
- return Promise.resolve(null);
170
+ const endpoint = endpointFor(destination, userId);
171
+ if (variables.nidEnabled === '1') {
172
+ startNetworkId(endpoint, workerNetwork(variables.networkOrigin), userId);
311
173
  }
312
- const storage = sessionStorageFor(input.storage);
313
- const now = (_c = (_b = input.now) === null || _b === void 0 ? void 0 : _b.call(input)) !== null && _c !== void 0 ? _c : Date.now();
314
- const existing = readSessionState(storage, merchantId);
315
- if (!shouldBootstrap(now, expiresAtMs(existing))) {
316
- publishToKv(input.destination, existing);
317
- return Promise.resolve(existing);
174
+ if (variables.nidLookupEnabled === '1') {
175
+ startLookupNetworkId(endpoint, workerLookup(variables.networkOrigin), userId);
318
176
  }
319
- // Opaque origins (sandboxed iframes like Shopify pixels) serialize to the
320
- // string 'null' per the HTML spec — that is what window.origin returns and
321
- // what the fail-closed browserOrigin() catch emits. JS null means no window
322
- // at all (non-browser host), which is not an opaque-origin sandbox.
323
- if ((input.origin === undefined ? browserOrigin() : input.origin) === 'null') {
324
- return Promise.resolve(null);
325
- }
326
- const key = stateKey(merchantId);
327
- const existingBootstrap = inFlight.get(key);
328
- if (existingBootstrap)
329
- return existingBootstrap;
330
- const fetcher = (_d = input.fetcher) !== null && _d !== void 0 ? _d : (typeof fetch === 'function' ? fetch.bind(globalThis) : null);
331
- if (!fetcher)
332
- return Promise.resolve(null);
333
- const bootstrap = bootstrapSession({
334
- dataFlywheelUrl,
335
- merchantId,
336
- pageUrl: input.pageUrl,
337
- storage,
338
- fetcher,
339
- now,
340
- })
341
- .then((state) => {
342
- publishToKv(input.destination, state);
343
- return state;
344
- })
345
- .finally(() => {
346
- inFlight.delete(key);
347
- });
348
- inFlight.set(key, bootstrap);
349
- return bootstrap;
350
- }
351
- function toProviderData(state) {
352
- if (!state)
353
- return {};
354
- return {
355
- n_id: state.n_id,
356
- session_id: state.session_id,
357
- expires_at: state.expires_at,
358
- };
359
- }
360
-
361
- const init = (params) => {
362
- void ensureSession({
363
- variables: params.manifest.variables,
364
- pageUrl: params.pageUrl,
365
- destination: params.destination,
366
- });
367
- };
368
-
369
- const tag = async ({ manifestVariables, pageUrl, destination, }) => {
370
- const state = await ensureSession({
371
- variables: manifestVariables,
372
- pageUrl,
373
- destination,
374
- });
375
- return toProviderData(state);
376
177
  };
377
178
 
378
179
  // eslint-disable-next-line @nx/enforce-module-boundaries
379
180
  const data = {
380
181
  name: packageName,
381
182
  init,
382
- tag,
383
183
  };
384
184
  try {
385
185
  if (typeof window !== 'undefined') {