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