@funnelsgrove/runtime 0.1.60 → 0.1.61
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.
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { buildSdkHeaders, FUNNEL_ID, getFunnelSdkPublishableKey, } from './runtime-api.config.js';
|
|
1
|
+
import { buildSdkHeaders, FUNNEL_ID, FUNNEL_VERSION_ID, getFunnelSdkPublishableKey, } from './runtime-api.config.js';
|
|
2
2
|
import { funnelSdkService } from './funnel-sdk.service.js';
|
|
3
3
|
import { mergeFunnelUserAttribution, } from '../runtime/funnel-attribution.js';
|
|
4
4
|
import { applyUrlUserAttributesToUser, hasUrlUserProfileAttributes, resolveUrlUserAttributes, } from '../runtime/url-user-attributes.js';
|
|
5
5
|
const DEFAULT_USER_ID_STORAGE_KEY = 'funnel:user-id';
|
|
6
|
+
const DEFAULT_BOOTSTRAP_IDEMPOTENCY_KEY_STORAGE_KEY = 'funnel:bootstrap-idempotency-key';
|
|
6
7
|
const DEFAULT_FUNNEL_DOMAIN_SIGNAL = 'funnelsgrove';
|
|
7
8
|
const canUseDom = () => {
|
|
8
9
|
return typeof window !== 'undefined';
|
|
@@ -102,6 +103,100 @@ const buildUserIdStorageKey = (funnelId = FUNNEL_ID) => {
|
|
|
102
103
|
}
|
|
103
104
|
return `funnel:${normalizedFunnelId}:user-id`;
|
|
104
105
|
};
|
|
106
|
+
const buildBootstrapIdempotencyKeyStorageKey = (funnelId = FUNNEL_ID) => {
|
|
107
|
+
const normalizedFunnelId = normalizeFunnelId(funnelId);
|
|
108
|
+
return normalizedFunnelId
|
|
109
|
+
? `funnel:${normalizedFunnelId}:bootstrap-idempotency-key`
|
|
110
|
+
: DEFAULT_BOOTSTRAP_IDEMPOTENCY_KEY_STORAGE_KEY;
|
|
111
|
+
};
|
|
112
|
+
const generateBootstrapIdempotencyKey = () => {
|
|
113
|
+
var _a;
|
|
114
|
+
if (canUseDom() && typeof ((_a = window.crypto) === null || _a === void 0 ? void 0 : _a.randomUUID) === 'function') {
|
|
115
|
+
return window.crypto.randomUUID();
|
|
116
|
+
}
|
|
117
|
+
return `${Date.now()}_${Math.random().toString(16).slice(2, 10)}`;
|
|
118
|
+
};
|
|
119
|
+
const parseBootstrapIdempotencyRecord = (value) => {
|
|
120
|
+
if (!value) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const parsed = JSON.parse(value);
|
|
125
|
+
const key = asString(parsed.key);
|
|
126
|
+
const fingerprint = asString(parsed.fingerprint);
|
|
127
|
+
return parsed.version === 1 && key && fingerprint
|
|
128
|
+
? { version: 1, key, fingerprint }
|
|
129
|
+
: null;
|
|
130
|
+
}
|
|
131
|
+
catch (_a) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const toStableJsonValue = (value) => {
|
|
136
|
+
if (Array.isArray(value)) {
|
|
137
|
+
return value.map(toStableJsonValue);
|
|
138
|
+
}
|
|
139
|
+
if (!isRecord(value)) {
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
return Object.keys(value).sort().reduce((result, key) => {
|
|
143
|
+
if (value[key] !== undefined) {
|
|
144
|
+
result[key] = toStableJsonValue(value[key]);
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}, {});
|
|
148
|
+
};
|
|
149
|
+
const buildFallbackDigest = (value) => {
|
|
150
|
+
var _a;
|
|
151
|
+
const bytes = new TextEncoder().encode(value);
|
|
152
|
+
const hashes = Array.from({ length: 8 }, (_, index) => ((0x811c9dc5 ^ Math.imul(index + 1, 0x9e3779b1)) >>> 0));
|
|
153
|
+
for (const byte of bytes) {
|
|
154
|
+
for (let index = 0; index < hashes.length; index += 1) {
|
|
155
|
+
hashes[index] = Math.imul(((_a = hashes[index]) !== null && _a !== void 0 ? _a : 0) ^ byte, 0x01000193 + index * 2) >>> 0;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return hashes.map((hash) => hash.toString(16).padStart(8, '0')).join('');
|
|
159
|
+
};
|
|
160
|
+
const digestBootstrapRequest = async (value) => {
|
|
161
|
+
var _a;
|
|
162
|
+
const serialized = JSON.stringify(toStableJsonValue(value));
|
|
163
|
+
const bytes = new TextEncoder().encode(serialized);
|
|
164
|
+
const subtle = (_a = globalThis.crypto) === null || _a === void 0 ? void 0 : _a.subtle;
|
|
165
|
+
if (!subtle) {
|
|
166
|
+
return `fallback:${buildFallbackDigest(serialized)}`;
|
|
167
|
+
}
|
|
168
|
+
const digest = await subtle.digest('SHA-256', bytes);
|
|
169
|
+
const hex = Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
170
|
+
return `sha256:${hex}`;
|
|
171
|
+
};
|
|
172
|
+
const getOrCreateBootstrapIdempotencyRecord = (fingerprint) => {
|
|
173
|
+
const created = () => ({
|
|
174
|
+
version: 1,
|
|
175
|
+
key: generateBootstrapIdempotencyKey(),
|
|
176
|
+
fingerprint,
|
|
177
|
+
});
|
|
178
|
+
if (!canUseDom()) {
|
|
179
|
+
return created();
|
|
180
|
+
}
|
|
181
|
+
const storageKey = buildBootstrapIdempotencyKeyStorageKey();
|
|
182
|
+
const existing = parseBootstrapIdempotencyRecord(window.localStorage.getItem(storageKey));
|
|
183
|
+
if ((existing === null || existing === void 0 ? void 0 : existing.fingerprint) === fingerprint) {
|
|
184
|
+
return existing;
|
|
185
|
+
}
|
|
186
|
+
const next = created();
|
|
187
|
+
window.localStorage.setItem(storageKey, JSON.stringify(next));
|
|
188
|
+
return next;
|
|
189
|
+
};
|
|
190
|
+
const clearBootstrapIdempotencyRecord = (completed) => {
|
|
191
|
+
if (!canUseDom()) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const storageKey = buildBootstrapIdempotencyKeyStorageKey();
|
|
195
|
+
const current = parseBootstrapIdempotencyRecord(window.localStorage.getItem(storageKey));
|
|
196
|
+
if ((current === null || current === void 0 ? void 0 : current.key) === completed.key && current.fingerprint === completed.fingerprint) {
|
|
197
|
+
window.localStorage.removeItem(storageKey);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
105
200
|
const generateUserId = () => {
|
|
106
201
|
var _a;
|
|
107
202
|
if (canUseDom() && typeof ((_a = window.crypto) === null || _a === void 0 ? void 0 : _a.randomUUID) === 'function') {
|
|
@@ -217,11 +312,12 @@ class ApiService {
|
|
|
217
312
|
return persistUserId(resolvedCandidateUserId || fallbackUserId, FUNNEL_ID);
|
|
218
313
|
}
|
|
219
314
|
async bootstrapSession(input) {
|
|
220
|
-
var _a;
|
|
315
|
+
var _a, _b, _c;
|
|
221
316
|
const urlUserAttributes = resolveUrlUserAttributes();
|
|
222
317
|
const inputEmail = (input === null || input === void 0 ? void 0 : input.email) || urlUserAttributes.email || undefined;
|
|
223
318
|
const inputName = (input === null || input === void 0 ? void 0 : input.name) || urlUserAttributes.name || undefined;
|
|
224
|
-
const
|
|
319
|
+
const candidateUserId = (input === null || input === void 0 ? void 0 : input.userId) || urlUserAttributes.userId;
|
|
320
|
+
const userId = await this.resolveBootstrapUserId(candidateUserId);
|
|
225
321
|
const publishableKey = getFunnelSdkPublishableKey();
|
|
226
322
|
if (!publishableKey) {
|
|
227
323
|
return toAppUser({
|
|
@@ -231,17 +327,29 @@ class ApiService {
|
|
|
231
327
|
fallbackDocument: withMergedAttributionDocument({}, input === null || input === void 0 ? void 0 : input.attribution),
|
|
232
328
|
});
|
|
233
329
|
}
|
|
330
|
+
const metadata = {
|
|
331
|
+
source: 'funnel-session',
|
|
332
|
+
environment: resolveRuntimeAnalyticsEnvironment(),
|
|
333
|
+
};
|
|
334
|
+
const fingerprint = await digestBootstrapRequest({
|
|
335
|
+
funnelVersionId: asString(FUNNEL_VERSION_ID),
|
|
336
|
+
userId,
|
|
337
|
+
email: (_b = (_a = asString(inputEmail)) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== null && _b !== void 0 ? _b : null,
|
|
338
|
+
fullName: asString(inputName),
|
|
339
|
+
metadata,
|
|
340
|
+
attribution: mergeFunnelUserAttribution({}, input === null || input === void 0 ? void 0 : input.attribution),
|
|
341
|
+
});
|
|
342
|
+
const idempotencyRecord = getOrCreateBootstrapIdempotencyRecord(fingerprint);
|
|
234
343
|
const payload = await funnelSdkService.bootstrapUser({
|
|
235
344
|
userId,
|
|
345
|
+
idempotencyKey: idempotencyRecord.key,
|
|
236
346
|
email: inputEmail,
|
|
237
347
|
fullName: inputName,
|
|
238
|
-
metadata
|
|
239
|
-
source: 'funnel-session',
|
|
240
|
-
environment: resolveRuntimeAnalyticsEnvironment(),
|
|
241
|
-
},
|
|
348
|
+
metadata,
|
|
242
349
|
attribution: input === null || input === void 0 ? void 0 : input.attribution,
|
|
243
350
|
});
|
|
244
|
-
|
|
351
|
+
clearBootstrapIdempotencyRecord(idempotencyRecord);
|
|
352
|
+
const persistedUserId = persistUserId(asString((_c = payload.user) === null || _c === void 0 ? void 0 : _c.user_id) ||
|
|
245
353
|
asString(payload.user_id) ||
|
|
246
354
|
userId, FUNNEL_ID) || userId;
|
|
247
355
|
return toAppUser({
|
package/package.json
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@funnelsgrove/runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.61",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/The-Solid-Grove/funnelsgrove.git",
|
|
7
|
+
"directory": "apps/funnel-runtime"
|
|
8
|
+
},
|
|
4
9
|
"type": "module",
|
|
5
10
|
"private": false,
|
|
6
11
|
"main": "./dist/index.js",
|