@formo/analytics 1.33.0 → 1.34.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/README.md +3 -0
- package/dist/cjs/src/FormoAnalytics.d.ts +65 -19
- package/dist/cjs/src/FormoAnalytics.js +187 -94
- package/dist/cjs/src/core.d.ts +2 -2
- package/dist/cjs/src/core.js +2 -1
- package/dist/cjs/src/event/EventFactory.d.ts +10 -4
- package/dist/cjs/src/event/EventFactory.js +50 -23
- package/dist/cjs/src/event/constants.d.ts +10 -1
- package/dist/cjs/src/event/constants.js +20 -8
- package/dist/cjs/src/event/sanitize.d.ts +13 -0
- package/dist/cjs/src/event/sanitize.js +94 -0
- package/dist/cjs/src/privy/index.d.ts +8 -3
- package/dist/cjs/src/privy/index.js +8 -3
- package/dist/cjs/src/privy/types.d.ts +25 -2
- package/dist/cjs/src/privy/utils.d.ts +100 -0
- package/dist/cjs/src/privy/utils.js +375 -16
- package/dist/cjs/src/session/index.d.ts +73 -6
- package/dist/cjs/src/session/index.js +309 -12
- package/dist/cjs/src/solana/SolanaManager.d.ts +1 -1
- package/dist/cjs/src/solana/SolanaManager.js +1 -1
- package/dist/cjs/src/solana/storeTypes.d.ts +1 -1
- package/dist/cjs/src/solana/storeTypes.js +1 -1
- package/dist/cjs/src/solana/types.d.ts +2 -2
- package/dist/cjs/src/types/base.d.ts +19 -3
- package/dist/cjs/src/version.d.ts +1 -1
- package/dist/cjs/src/version.js +1 -1
- package/dist/esm/src/FormoAnalytics.d.ts +65 -19
- package/dist/esm/src/FormoAnalytics.js +187 -94
- package/dist/esm/src/core.d.ts +2 -2
- package/dist/esm/src/core.js +1 -1
- package/dist/esm/src/event/EventFactory.d.ts +10 -4
- package/dist/esm/src/event/EventFactory.js +51 -24
- package/dist/esm/src/event/constants.d.ts +10 -1
- package/dist/esm/src/event/constants.js +19 -8
- package/dist/esm/src/event/sanitize.d.ts +13 -0
- package/dist/esm/src/event/sanitize.js +88 -0
- package/dist/esm/src/privy/index.d.ts +8 -3
- package/dist/esm/src/privy/index.js +7 -3
- package/dist/esm/src/privy/types.d.ts +25 -2
- package/dist/esm/src/privy/utils.d.ts +100 -0
- package/dist/esm/src/privy/utils.js +374 -16
- package/dist/esm/src/session/index.d.ts +73 -6
- package/dist/esm/src/session/index.js +309 -12
- package/dist/esm/src/solana/SolanaManager.d.ts +1 -1
- package/dist/esm/src/solana/SolanaManager.js +1 -1
- package/dist/esm/src/solana/storeTypes.d.ts +1 -1
- package/dist/esm/src/solana/storeTypes.js +1 -1
- package/dist/esm/src/solana/types.d.ts +2 -2
- package/dist/esm/src/types/base.d.ts +19 -3
- package/dist/esm/src/version.d.ts +1 -1
- package/dist/esm/src/version.js +1 -1
- package/dist/index.umd.min.js +1 -1
- package/package.json +8 -8
|
@@ -63,7 +63,8 @@ import { logger } from "../logger";
|
|
|
63
63
|
import mergeDeepRight from "../ramda/mergeDeepRight";
|
|
64
64
|
import { session } from "../storage";
|
|
65
65
|
import { version } from "../version";
|
|
66
|
-
import { CHANNEL, CLICK_ID_PARAMS, DEFAULT_EXCLUDED_QUERY_PARAMS, PAGE_PROPERTIES_EXCLUDED_FIELDS, VERSION, } from "./constants";
|
|
66
|
+
import { CHANNEL, CLICK_ID_PARAMS, DEFAULT_EXCLUDED_QUERY_PARAMS, DEFAULT_REFERRAL_PARAMS, PAGE_PROPERTIES_EXCLUDED_FIELDS, VERSION, } from "./constants";
|
|
67
|
+
import { sanitizeTrafficSources } from "./sanitize";
|
|
67
68
|
import { generateAnonymousId } from "./utils";
|
|
68
69
|
import { detectBrowser } from "../browser/browsers";
|
|
69
70
|
var ISO_3166_ALPHA_2_REGEX = /^[A-Z]{2}$/;
|
|
@@ -107,10 +108,9 @@ var EventFactory = /** @class */ (function () {
|
|
|
107
108
|
// - If no referral config exists → use defaults
|
|
108
109
|
// - If referral config exists but queryParams is undefined → use defaults
|
|
109
110
|
// - If referral config exists with queryParams → use those
|
|
110
|
-
var defaultParams = ["ref", "referral", "refcode"];
|
|
111
111
|
var referralParams = !((_a = _this.options) === null || _a === void 0 ? void 0 : _a.referral)
|
|
112
|
-
?
|
|
113
|
-
: ((_b = _this.options.referral.queryParams) !== null && _b !== void 0 ? _b :
|
|
112
|
+
? DEFAULT_REFERRAL_PARAMS // No referral config at all → use defaults
|
|
113
|
+
: ((_b = _this.options.referral.queryParams) !== null && _b !== void 0 ? _b : DEFAULT_REFERRAL_PARAMS); // Has config → use queryParams or defaults
|
|
114
114
|
// Check query parameters (if any configured)
|
|
115
115
|
for (var _i = 0, referralParams_1 = referralParams; _i < referralParams_1.length; _i++) {
|
|
116
116
|
var param = referralParams_1[_i];
|
|
@@ -133,7 +133,7 @@ var EventFactory = /** @class */ (function () {
|
|
|
133
133
|
/**
|
|
134
134
|
* Returns the document referrer with same-host referrers filtered out.
|
|
135
135
|
* Internal navigation populates `document.referrer` with the previous page
|
|
136
|
-
* on the same site, which is not an attribution signal
|
|
136
|
+
* on the same site, which is not an attribution signal - treating it as
|
|
137
137
|
* "external" would otherwise let an internal URL become the session's
|
|
138
138
|
* first-touch referrer after a direct landing.
|
|
139
139
|
*/
|
|
@@ -152,12 +152,16 @@ var EventFactory = /** @class */ (function () {
|
|
|
152
152
|
};
|
|
153
153
|
this.getTrafficSources = function (url) {
|
|
154
154
|
var urlObj = new URL(url);
|
|
155
|
-
|
|
155
|
+
// Sanitize at the source so scanner-injected garbage (XSS probes in
|
|
156
|
+
// utm_*/click-id/ref query params) never wins the context-over-stored
|
|
157
|
+
// merge below, never gets persisted, and never reaches an event.
|
|
158
|
+
var contextTrafficSources = sanitizeTrafficSources(__assign(__assign(__assign({}, _this.extractUTMParameters(url)), _this.extractClickIdParameters(urlObj)), { ref: _this.extractReferralParameter(urlObj), referrer: _this.getExternalReferrer() }));
|
|
156
159
|
// Sticky traffic sources may have been persisted by an older SDK version or
|
|
157
160
|
// a looser config, before the current excludeQueryParams was in effect.
|
|
158
161
|
// Honor the current denylist on the way out so excluded values can never
|
|
159
|
-
// resurface from session storage (or get re-persisted below).
|
|
160
|
-
|
|
162
|
+
// resurface from session storage (or get re-persisted below). Sanitizing
|
|
163
|
+
// here too flushes poisoned values persisted by a pre-sanitization SDK.
|
|
164
|
+
var storedTrafficSources = sanitizeTrafficSources(_this.redactStoredTrafficSources(session().get(SESSION_TRAFFIC_SOURCE_KEY) || {}));
|
|
161
165
|
var mergedClickIds = {};
|
|
162
166
|
for (var _i = 0, CLICK_ID_PARAMS_2 = CLICK_ID_PARAMS; _i < CLICK_ID_PARAMS_2.length; _i++) {
|
|
163
167
|
var p = CLICK_ID_PARAMS_2[_i];
|
|
@@ -205,13 +209,14 @@ var EventFactory = /** @class */ (function () {
|
|
|
205
209
|
try {
|
|
206
210
|
urlObj = new URL(globalThis.location.href);
|
|
207
211
|
_this.redactQueryParams(urlObj);
|
|
212
|
+
_this.normalizeUrlPath(urlObj);
|
|
208
213
|
}
|
|
209
214
|
catch (_a) { }
|
|
210
215
|
if (isUndefined(pageProps.url)) {
|
|
211
216
|
pageProps.url = urlObj ? urlObj.href : globalThis.location.href;
|
|
212
217
|
}
|
|
213
218
|
if (isUndefined(pageProps.path)) {
|
|
214
|
-
pageProps.path = globalThis.location.pathname;
|
|
219
|
+
pageProps.path = urlObj ? urlObj.pathname : globalThis.location.pathname;
|
|
215
220
|
}
|
|
216
221
|
if (isUndefined(pageProps.hash)) {
|
|
217
222
|
pageProps.hash = globalThis.location.hash;
|
|
@@ -306,6 +311,16 @@ var EventFactory = /** @class */ (function () {
|
|
|
306
311
|
EventFactory.prototype.isExcludedQueryParam = function (key) {
|
|
307
312
|
return this.excludedQueryParams.has(key.toLowerCase());
|
|
308
313
|
};
|
|
314
|
+
/**
|
|
315
|
+
* Normalize URL paths for analytics aggregation by stripping trailing slashes
|
|
316
|
+
* from non-root paths. Query strings and hash fragments are preserved by
|
|
317
|
+
* mutating only the URL pathname.
|
|
318
|
+
*/
|
|
319
|
+
EventFactory.prototype.normalizeUrlPath = function (url) {
|
|
320
|
+
if (url.pathname !== "/") {
|
|
321
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
322
|
+
}
|
|
323
|
+
};
|
|
309
324
|
/**
|
|
310
325
|
* Strip excluded (sensitive) query parameters from a URL in place. Only the
|
|
311
326
|
* query string is touched; the path and hash/fragment are left as-is.
|
|
@@ -323,9 +338,9 @@ var EventFactory = /** @class */ (function () {
|
|
|
323
338
|
keysToDelete.forEach(function (key) { return url.searchParams.delete(key); });
|
|
324
339
|
};
|
|
325
340
|
/**
|
|
326
|
-
* Return the given absolute URL with excluded query parameters removed
|
|
327
|
-
*
|
|
328
|
-
* empty referrer).
|
|
341
|
+
* Return the given absolute URL with excluded query parameters removed and
|
|
342
|
+
* trailing slashes stripped from non-root paths. The input is returned
|
|
343
|
+
* unchanged when it is empty or cannot be parsed (e.g. an empty referrer).
|
|
329
344
|
*/
|
|
330
345
|
EventFactory.prototype.redactUrl = function (href) {
|
|
331
346
|
if (!href)
|
|
@@ -333,6 +348,7 @@ var EventFactory = /** @class */ (function () {
|
|
|
333
348
|
try {
|
|
334
349
|
var url = new URL(href);
|
|
335
350
|
this.redactQueryParams(url);
|
|
351
|
+
this.normalizeUrlPath(url);
|
|
336
352
|
return url.href;
|
|
337
353
|
}
|
|
338
354
|
catch (_a) {
|
|
@@ -576,8 +592,9 @@ var EventFactory = /** @class */ (function () {
|
|
|
576
592
|
EventFactory.prototype.create = function (event, address, userId) {
|
|
577
593
|
return __awaiter(this, void 0, void 0, function () {
|
|
578
594
|
var formoEvent, _a, chainId;
|
|
579
|
-
|
|
580
|
-
|
|
595
|
+
var _b, _c;
|
|
596
|
+
return __generator(this, function (_d) {
|
|
597
|
+
switch (_d.label) {
|
|
581
598
|
case 0:
|
|
582
599
|
formoEvent = {};
|
|
583
600
|
_a = event.type;
|
|
@@ -595,39 +612,39 @@ var EventFactory = /** @class */ (function () {
|
|
|
595
612
|
return [3 /*break*/, 17];
|
|
596
613
|
case 1: return [4 /*yield*/, this.generatePageEvent(event.category, event.name, event.properties, event.context)];
|
|
597
614
|
case 2:
|
|
598
|
-
formoEvent =
|
|
615
|
+
formoEvent = _d.sent();
|
|
599
616
|
return [3 /*break*/, 19];
|
|
600
617
|
case 3: return [4 /*yield*/, this.generateDetectWalletEvent(event.providerName, event.rdns, event.properties, event.context)];
|
|
601
618
|
case 4:
|
|
602
|
-
formoEvent =
|
|
619
|
+
formoEvent = _d.sent();
|
|
603
620
|
return [3 /*break*/, 19];
|
|
604
621
|
case 5: return [4 /*yield*/, this.generateIdentifyEvent(event.providerName, event.rdns, event.address, event.userId, event.properties, event.context)];
|
|
605
622
|
case 6:
|
|
606
|
-
formoEvent =
|
|
623
|
+
formoEvent = _d.sent();
|
|
607
624
|
return [3 /*break*/, 19];
|
|
608
625
|
case 7: return [4 /*yield*/, this.generateChainChangedEvent(event.chainId, event.address, event.properties, event.context)];
|
|
609
626
|
case 8:
|
|
610
|
-
formoEvent =
|
|
627
|
+
formoEvent = _d.sent();
|
|
611
628
|
return [3 /*break*/, 19];
|
|
612
629
|
case 9: return [4 /*yield*/, this.generateConnectEvent(event.chainId, event.address, event.properties, event.context)];
|
|
613
630
|
case 10:
|
|
614
|
-
formoEvent =
|
|
631
|
+
formoEvent = _d.sent();
|
|
615
632
|
return [3 /*break*/, 19];
|
|
616
633
|
case 11: return [4 /*yield*/, this.generateDisconnectEvent(event.chainId, event.address, event.properties, event.context)];
|
|
617
634
|
case 12:
|
|
618
|
-
formoEvent =
|
|
635
|
+
formoEvent = _d.sent();
|
|
619
636
|
return [3 /*break*/, 19];
|
|
620
637
|
case 13: return [4 /*yield*/, this.generateSignatureEvent(event.status, event.chainId, event.address, event.message, event.properties, event.context)];
|
|
621
638
|
case 14:
|
|
622
|
-
formoEvent =
|
|
639
|
+
formoEvent = _d.sent();
|
|
623
640
|
return [3 /*break*/, 19];
|
|
624
641
|
case 15: return [4 /*yield*/, this.generateTransactionEvent(event.status, event.chainId, event.address, event.data, event.to, event.value, event.transactionHash, event.function_name, event.function_args, event.properties, event.context)];
|
|
625
642
|
case 16:
|
|
626
|
-
formoEvent =
|
|
643
|
+
formoEvent = _d.sent();
|
|
627
644
|
return [3 /*break*/, 19];
|
|
628
645
|
case 17: return [4 /*yield*/, this.generateTrackEvent(event.event, event.properties, event.context)];
|
|
629
646
|
case 18:
|
|
630
|
-
formoEvent =
|
|
647
|
+
formoEvent = _d.sent();
|
|
631
648
|
return [3 /*break*/, 19];
|
|
632
649
|
case 19:
|
|
633
650
|
// Set address if not already set by the specific event generator
|
|
@@ -638,7 +655,17 @@ var EventFactory = /** @class */ (function () {
|
|
|
638
655
|
chainId = 'chainId' in event ? event.chainId : undefined;
|
|
639
656
|
formoEvent.address = this.validateEventAddress(address, chainId);
|
|
640
657
|
}
|
|
641
|
-
|
|
658
|
+
// An identify event asserts an explicit identity in its own payload (e.g. a
|
|
659
|
+
// Privy DID for each wallet being clustered). Keep that payload user_id
|
|
660
|
+
// rather than overwriting it with the active-session user id - otherwise a
|
|
661
|
+
// clustering identify that intentionally leaves the active user unchanged
|
|
662
|
+
// (setActive:false) would be stripped of its DID, defeating server-side
|
|
663
|
+
// wallet clustering. Fall back to the active-session user id when the
|
|
664
|
+
// identify payload carries none; all other events use the session user id.
|
|
665
|
+
formoEvent.user_id =
|
|
666
|
+
event.type === "identify"
|
|
667
|
+
? (_c = (_b = formoEvent.user_id) !== null && _b !== void 0 ? _b : userId) !== null && _c !== void 0 ? _c : null
|
|
668
|
+
: userId || null;
|
|
642
669
|
return [2 /*return*/, formoEvent];
|
|
643
670
|
}
|
|
644
671
|
});
|
|
@@ -18,6 +18,15 @@ declare const CLICK_ID_PARAMS: readonly ["gclid", "gad_source", "fbclid", "msclk
|
|
|
18
18
|
* cannot remove these built-ins. Matched case-insensitively.
|
|
19
19
|
*/
|
|
20
20
|
declare const DEFAULT_EXCLUDED_QUERY_PARAMS: readonly ["privy_oauth_code", "privy_oauth_state", "privy_oauth_provider"];
|
|
21
|
+
/**
|
|
22
|
+
* Default query parameter names checked (in order) for a referral code on the
|
|
23
|
+
* landing-page URL. The first parameter present supplies the `ref` traffic
|
|
24
|
+
* source. Consumers can override this list via `referral.queryParams`.
|
|
25
|
+
*
|
|
26
|
+
* Keep in sync with the ReferralOptions.queryParams @default in
|
|
27
|
+
* src/types/base.ts.
|
|
28
|
+
*/
|
|
29
|
+
declare const DEFAULT_REFERRAL_PARAMS: readonly ["ref", "referral", "refcode", "af", "referrer"];
|
|
21
30
|
/**
|
|
22
31
|
* Fields that should be excluded from page event properties parsing
|
|
23
32
|
* These are either:
|
|
@@ -25,5 +34,5 @@ declare const DEFAULT_EXCLUDED_QUERY_PARAMS: readonly ["privy_oauth_code", "priv
|
|
|
25
34
|
* - Semantic event properties that should not be overridden by URL params
|
|
26
35
|
*/
|
|
27
36
|
declare const PAGE_PROPERTIES_EXCLUDED_FIELDS: Set<string>;
|
|
28
|
-
export { CHANNEL, VERSION, CLICK_ID_PARAMS, DEFAULT_EXCLUDED_QUERY_PARAMS, PAGE_PROPERTIES_EXCLUDED_FIELDS, };
|
|
37
|
+
export { CHANNEL, VERSION, CLICK_ID_PARAMS, DEFAULT_EXCLUDED_QUERY_PARAMS, DEFAULT_REFERRAL_PARAMS, PAGE_PROPERTIES_EXCLUDED_FIELDS, };
|
|
29
38
|
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -40,24 +40,35 @@ var DEFAULT_EXCLUDED_QUERY_PARAMS = [
|
|
|
40
40
|
"privy_oauth_state",
|
|
41
41
|
"privy_oauth_provider",
|
|
42
42
|
];
|
|
43
|
+
/**
|
|
44
|
+
* Default query parameter names checked (in order) for a referral code on the
|
|
45
|
+
* landing-page URL. The first parameter present supplies the `ref` traffic
|
|
46
|
+
* source. Consumers can override this list via `referral.queryParams`.
|
|
47
|
+
*
|
|
48
|
+
* Keep in sync with the ReferralOptions.queryParams @default in
|
|
49
|
+
* src/types/base.ts.
|
|
50
|
+
*/
|
|
51
|
+
var DEFAULT_REFERRAL_PARAMS = [
|
|
52
|
+
'ref',
|
|
53
|
+
'referral',
|
|
54
|
+
'refcode',
|
|
55
|
+
'af',
|
|
56
|
+
'referrer',
|
|
57
|
+
];
|
|
43
58
|
/**
|
|
44
59
|
* Fields that should be excluded from page event properties parsing
|
|
45
60
|
* These are either:
|
|
46
61
|
* - Already captured in event context (UTM params, referral params, click IDs)
|
|
47
62
|
* - Semantic event properties that should not be overridden by URL params
|
|
48
63
|
*/
|
|
49
|
-
var PAGE_PROPERTIES_EXCLUDED_FIELDS = new Set(__spreadArray(__spreadArray([
|
|
64
|
+
var PAGE_PROPERTIES_EXCLUDED_FIELDS = new Set(__spreadArray(__spreadArray(__spreadArray([
|
|
50
65
|
// Context fields (already captured in event context)
|
|
51
66
|
'utm_source',
|
|
52
67
|
'utm_medium',
|
|
53
68
|
'utm_campaign',
|
|
54
69
|
'utm_term',
|
|
55
|
-
'utm_content'
|
|
56
|
-
|
|
57
|
-
'referral',
|
|
58
|
-
'refcode',
|
|
59
|
-
'referrer'
|
|
60
|
-
], CLICK_ID_PARAMS, true), [
|
|
70
|
+
'utm_content'
|
|
71
|
+
], DEFAULT_REFERRAL_PARAMS, true), CLICK_ID_PARAMS, true), [
|
|
61
72
|
// Semantic event properties (should not be overridden by URL params)
|
|
62
73
|
'category',
|
|
63
74
|
'name',
|
|
@@ -66,5 +77,5 @@ var PAGE_PROPERTIES_EXCLUDED_FIELDS = new Set(__spreadArray(__spreadArray([
|
|
|
66
77
|
'hash',
|
|
67
78
|
'query',
|
|
68
79
|
], false));
|
|
69
|
-
export { CHANNEL, VERSION, CLICK_ID_PARAMS, DEFAULT_EXCLUDED_QUERY_PARAMS, PAGE_PROPERTIES_EXCLUDED_FIELDS, };
|
|
80
|
+
export { CHANNEL, VERSION, CLICK_ID_PARAMS, DEFAULT_EXCLUDED_QUERY_PARAMS, DEFAULT_REFERRAL_PARAMS, PAGE_PROPERTIES_EXCLUDED_FIELDS, };
|
|
70
81
|
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ITrafficSource } from "../types";
|
|
2
|
+
declare const sanitizeClickId: (value: string) => string;
|
|
3
|
+
declare const sanitizeRef: (value: string) => string;
|
|
4
|
+
declare const sanitizeUtm: (value: string) => string;
|
|
5
|
+
/**
|
|
6
|
+
* Sanitize every traffic-source field of a (possibly sparse) traffic-source
|
|
7
|
+
* object. `referrer` is left untouched: it is a browser-set URL already
|
|
8
|
+
* handled by redactUrl, not an attacker-controlled query parameter. Unknown
|
|
9
|
+
* keys fall through to the UTM rule, the most permissive one.
|
|
10
|
+
*/
|
|
11
|
+
declare const sanitizeTrafficSources: <T extends Partial<ITrafficSource>>(trafficSources: T) => T;
|
|
12
|
+
export { sanitizeClickId, sanitizeRef, sanitizeUtm, sanitizeTrafficSources };
|
|
13
|
+
//# sourceMappingURL=sanitize.d.ts.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
var __assign = (this && this.__assign) || function () {
|
|
2
|
+
__assign = Object.assign || function(t) {
|
|
3
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
4
|
+
s = arguments[i];
|
|
5
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
6
|
+
t[p] = s[p];
|
|
7
|
+
}
|
|
8
|
+
return t;
|
|
9
|
+
};
|
|
10
|
+
return __assign.apply(this, arguments);
|
|
11
|
+
};
|
|
12
|
+
import { CLICK_ID_PARAMS } from "./constants";
|
|
13
|
+
/**
|
|
14
|
+
* Traffic-source value sanitization.
|
|
15
|
+
*
|
|
16
|
+
* Vulnerability scanners (e.g. Acunetix) crawl customer sites injecting XSS
|
|
17
|
+
* probes such as `javascript:domxssExecutionSink(1,"'\"><xsstag>()locxss")`
|
|
18
|
+
* or `<script>alert(1)</script>` into every query parameter. Without
|
|
19
|
+
* validation those payloads are captured verbatim as utm_* / click-id / ref
|
|
20
|
+
* values, persisted as sticky session traffic sources, and pollute the
|
|
21
|
+
* customer's attribution reporting. Each field class gets the tightest rule
|
|
22
|
+
* its legitimate values allow (verified against production data):
|
|
23
|
+
*
|
|
24
|
+
* - Click IDs are opaque platform-generated tokens (base64url-ish); every
|
|
25
|
+
* legitimate production value matches the strict token pattern.
|
|
26
|
+
* - Referral codes are short tokens; >99.5% of production values match the
|
|
27
|
+
* strict pattern and none of the remainder are legitimate (scanner
|
|
28
|
+
* payloads, mangled encodings, URLs glued to codes).
|
|
29
|
+
* - UTM values are free-form (spaces, unicode, `+` are legitimate), so they
|
|
30
|
+
* only reject markup/quote characters, dangerous URL schemes, control and
|
|
31
|
+
* zero-width characters, and absurd lengths.
|
|
32
|
+
*
|
|
33
|
+
* Invalid values are dropped to "" — the same representation as "parameter
|
|
34
|
+
* absent" — rather than repaired, so a poisoned value can never be persisted
|
|
35
|
+
* or reported.
|
|
36
|
+
*/
|
|
37
|
+
var CLICK_ID_PATTERN = /^[A-Za-z0-9._-]{1,255}$/;
|
|
38
|
+
var REF_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
|
|
39
|
+
var UTM_MAX_LENGTH = 255;
|
|
40
|
+
// Markup/quote/backslash characters plus C0/C1 control characters and
|
|
41
|
+
// zero-width / bidi / BOM / replacement characters (mangled-encoding
|
|
42
|
+
// markers). Explicit ranges instead of \p{C} to avoid the `u`-flag
|
|
43
|
+
// property-escape requirement.
|
|
44
|
+
var UTM_FORBIDDEN_CHARS = /[<>"'`\\\u0000-\u001f\u007f-\u009f\u200b-\u200f\u2028-\u202e\u2060\ufeff\ufffd]/;
|
|
45
|
+
// Values smuggling an executable/URL scheme, e.g. `javascript:alert(1)`.
|
|
46
|
+
var FORBIDDEN_SCHEME_PREFIX = /^\s*(javascript|data|vbscript):/i;
|
|
47
|
+
var sanitizeClickId = function (value) {
|
|
48
|
+
return CLICK_ID_PATTERN.test(value) ? value : "";
|
|
49
|
+
};
|
|
50
|
+
var sanitizeRef = function (value) {
|
|
51
|
+
return REF_PATTERN.test(value) ? value : "";
|
|
52
|
+
};
|
|
53
|
+
var sanitizeUtm = function (value) {
|
|
54
|
+
return value.length <= UTM_MAX_LENGTH &&
|
|
55
|
+
!UTM_FORBIDDEN_CHARS.test(value) &&
|
|
56
|
+
!FORBIDDEN_SCHEME_PREFIX.test(value)
|
|
57
|
+
? value
|
|
58
|
+
: "";
|
|
59
|
+
};
|
|
60
|
+
var CLICK_ID_KEYS = new Set(CLICK_ID_PARAMS);
|
|
61
|
+
/**
|
|
62
|
+
* Sanitize every traffic-source field of a (possibly sparse) traffic-source
|
|
63
|
+
* object. `referrer` is left untouched: it is a browser-set URL already
|
|
64
|
+
* handled by redactUrl, not an attacker-controlled query parameter. Unknown
|
|
65
|
+
* keys fall through to the UTM rule, the most permissive one.
|
|
66
|
+
*/
|
|
67
|
+
var sanitizeTrafficSources = function (trafficSources) {
|
|
68
|
+
var result = __assign({}, trafficSources);
|
|
69
|
+
for (var _i = 0, _a = Object.keys(result); _i < _a.length; _i++) {
|
|
70
|
+
var key = _a[_i];
|
|
71
|
+
var value = result[key];
|
|
72
|
+
if (typeof value !== "string" || value === "" || key === "referrer") {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (CLICK_ID_KEYS.has(key)) {
|
|
76
|
+
result[key] = sanitizeClickId(value);
|
|
77
|
+
}
|
|
78
|
+
else if (key === "ref") {
|
|
79
|
+
result[key] = sanitizeRef(value);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
result[key] = sanitizeUtm(value);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
};
|
|
87
|
+
export { sanitizeClickId, sanitizeRef, sanitizeUtm, sanitizeTrafficSources };
|
|
88
|
+
//# sourceMappingURL=sanitize.js.map
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Privy integration module
|
|
3
3
|
*
|
|
4
|
-
* Provides utilities for enriching wallet profiles with Privy user data
|
|
5
|
-
*
|
|
4
|
+
* Provides utilities for enriching wallet profiles with Privy user data:
|
|
5
|
+
* `parsePrivyProperties` (low-level parsing) and `identifyPrivyUser` (identify
|
|
6
|
+
* every linked wallet under the user's DID). The same behavior is also
|
|
7
|
+
* available as `formo.identify(user, { privy: true })`.
|
|
8
|
+
*
|
|
9
|
+
* This module is React-free so it can be used from the `core` entry.
|
|
6
10
|
*/
|
|
7
|
-
export { parsePrivyProperties } from "./utils";
|
|
11
|
+
export { parsePrivyProperties, identifyPrivyUser } from "./utils";
|
|
12
|
+
export type { IdentifyPrivyUserOptions } from "./utils";
|
|
8
13
|
export type { PrivyUser, PrivyLinkedAccount, PrivyAccountType, PrivyProfileProperties, PrivyWalletInfo, } from "./types";
|
|
9
14
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Privy integration module
|
|
3
3
|
*
|
|
4
|
-
* Provides utilities for enriching wallet profiles with Privy user data
|
|
5
|
-
*
|
|
4
|
+
* Provides utilities for enriching wallet profiles with Privy user data:
|
|
5
|
+
* `parsePrivyProperties` (low-level parsing) and `identifyPrivyUser` (identify
|
|
6
|
+
* every linked wallet under the user's DID). The same behavior is also
|
|
7
|
+
* available as `formo.identify(user, { privy: true })`.
|
|
8
|
+
*
|
|
9
|
+
* This module is React-free so it can be used from the `core` entry.
|
|
6
10
|
*/
|
|
7
|
-
export { parsePrivyProperties } from "./utils";
|
|
11
|
+
export { parsePrivyProperties, identifyPrivyUser } from "./utils";
|
|
8
12
|
//# sourceMappingURL=index.js.map
|
|
@@ -38,6 +38,16 @@ export interface PrivyLinkedAccount {
|
|
|
38
38
|
telegramUserId?: string | null;
|
|
39
39
|
firstName?: string | null;
|
|
40
40
|
lastName?: string | null;
|
|
41
|
+
credentialId?: string | null;
|
|
42
|
+
customUserId?: string | null;
|
|
43
|
+
embeddedWallets?: Array<{
|
|
44
|
+
address: string;
|
|
45
|
+
chainType?: string | null;
|
|
46
|
+
}> | null;
|
|
47
|
+
smartWallets?: Array<{
|
|
48
|
+
address: string;
|
|
49
|
+
chainType?: string | null;
|
|
50
|
+
}> | null;
|
|
41
51
|
firstVerifiedAt?: Date | null;
|
|
42
52
|
latestVerifiedAt?: Date | null;
|
|
43
53
|
}
|
|
@@ -51,8 +61,12 @@ export interface PrivyLinkedAccount {
|
|
|
51
61
|
export interface PrivyUser {
|
|
52
62
|
/** Privy user ID in DID format (e.g., "did:privy:cm3np...") */
|
|
53
63
|
id: string;
|
|
54
|
-
/**
|
|
55
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Account creation timestamp. Privy's React SDK supplies a `Date`, but a user
|
|
66
|
+
* object from the REST API or one that made a JSON round-trip carries an ISO
|
|
67
|
+
* string or epoch number, so all three are accepted and normalized.
|
|
68
|
+
*/
|
|
69
|
+
createdAt?: Date | string | number;
|
|
56
70
|
/** All linked accounts */
|
|
57
71
|
linkedAccounts?: PrivyLinkedAccount[];
|
|
58
72
|
/** Optional custom metadata */
|
|
@@ -123,6 +137,11 @@ export interface PrivyUser {
|
|
|
123
137
|
username: string | null;
|
|
124
138
|
name: string | null;
|
|
125
139
|
};
|
|
140
|
+
/** Privy's Twitch account exposes only subject/username - no email. */
|
|
141
|
+
twitch?: {
|
|
142
|
+
subject: string;
|
|
143
|
+
username: string | null;
|
|
144
|
+
};
|
|
126
145
|
line?: {
|
|
127
146
|
subject: string;
|
|
128
147
|
name: string | null;
|
|
@@ -150,9 +169,11 @@ export interface PrivyProfileProperties {
|
|
|
150
169
|
privyDid: string;
|
|
151
170
|
privyCreatedAt?: number;
|
|
152
171
|
email?: string;
|
|
172
|
+
phone?: string;
|
|
153
173
|
apple?: string;
|
|
154
174
|
discord?: string;
|
|
155
175
|
twitter?: string;
|
|
176
|
+
twitch?: string;
|
|
156
177
|
farcaster?: string;
|
|
157
178
|
github?: string;
|
|
158
179
|
google?: string;
|
|
@@ -162,6 +183,8 @@ export interface PrivyProfileProperties {
|
|
|
162
183
|
telegram?: string;
|
|
163
184
|
tiktok?: string;
|
|
164
185
|
instagram?: string;
|
|
186
|
+
/** The `customUserId` of a linked `custom_auth` account, when present. */
|
|
187
|
+
customUserId?: string;
|
|
165
188
|
[key: string]: unknown;
|
|
166
189
|
}
|
|
167
190
|
/**
|
|
@@ -2,12 +2,19 @@
|
|
|
2
2
|
* Utility functions for extracting profile properties from Privy user objects.
|
|
3
3
|
*/
|
|
4
4
|
import { PrivyProfileProperties, PrivyUser, PrivyWalletInfo } from "./types";
|
|
5
|
+
import { IFormoAnalytics } from "../types/base";
|
|
6
|
+
import { IFormoEventProperties } from "../types/events";
|
|
5
7
|
/**
|
|
6
8
|
* Extract profile properties and wallet addresses from a Privy user object.
|
|
7
9
|
*
|
|
8
10
|
* Parses the Privy user's linked accounts into a flat properties object
|
|
9
11
|
* (email, social accounts, etc.) and extracts all linked wallet addresses.
|
|
10
12
|
*
|
|
13
|
+
* For most apps prefer the {@link identifyPrivyUser} one-liner, which builds on
|
|
14
|
+
* this function and also forwards per-wallet metadata and handles event
|
|
15
|
+
* attribution. Use `parsePrivyProperties` directly only for advanced/custom
|
|
16
|
+
* flows.
|
|
17
|
+
*
|
|
11
18
|
* @param user - The Privy user object from `usePrivy()`
|
|
12
19
|
* @returns An object with `properties` and `wallets`
|
|
13
20
|
*
|
|
@@ -29,4 +36,97 @@ export declare function parsePrivyProperties(user: PrivyUser): {
|
|
|
29
36
|
properties: PrivyProfileProperties;
|
|
30
37
|
wallets: PrivyWalletInfo[];
|
|
31
38
|
};
|
|
39
|
+
/**
|
|
40
|
+
* Options for {@link identifyPrivyUser}.
|
|
41
|
+
*/
|
|
42
|
+
export interface IdentifyPrivyUserOptions {
|
|
43
|
+
/**
|
|
44
|
+
* Optional override for the wallet that should own event attribution - the
|
|
45
|
+
* one promoted to the SDK's current address/user, while every other linked
|
|
46
|
+
* wallet is recorded only for clustering.
|
|
47
|
+
*
|
|
48
|
+
* You usually don't need this. When omitted, the helper first uses the wallet
|
|
49
|
+
* the SDK already treats as active (a prior wagmi/EIP-1193 connect), then
|
|
50
|
+
* Privy's own surfaced wallet (`user.wallet`), then a best-effort guess
|
|
51
|
+
* (embedded wallets deprioritized, so the last external wallet). Pass it only
|
|
52
|
+
* when you want to pin attribution to a specific wallet - e.g. the currently
|
|
53
|
+
* connected wallet from `useWallets()[0]?.address`, which reflects the live
|
|
54
|
+
* active wallet more precisely than `user.wallet`.
|
|
55
|
+
*
|
|
56
|
+
* Matched strictly: if it doesn't correspond to one of the user's linked
|
|
57
|
+
* wallets, no wallet is promoted and the SDK's current wallet is left as-is
|
|
58
|
+
* (so a connected wallet that isn't linked in Privy is preserved).
|
|
59
|
+
*/
|
|
60
|
+
activeAddress?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Extra properties merged into every identify call, on top of the profile
|
|
63
|
+
* properties parsed from the Privy user (email, socials, DID, …) and the
|
|
64
|
+
* per-wallet metadata (`wallet_client`, `chain_type`, `is_embedded`).
|
|
65
|
+
*
|
|
66
|
+
* Identify events are deduped per `(wallet, user, properties)` within a
|
|
67
|
+
* session, so changing these properties re-emits with the updated profile
|
|
68
|
+
* rather than being swallowed. The flip side: a value that changes on every
|
|
69
|
+
* call (a timestamp, a random id) makes every identify a new event. Pass
|
|
70
|
+
* stable identity metadata here and put volatile values on `track()` events.
|
|
71
|
+
*/
|
|
72
|
+
properties?: IFormoEventProperties;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Identify every wallet linked to a Privy user under that user's Privy DID.
|
|
76
|
+
*
|
|
77
|
+
* This is the one-liner replacement for hand-rolling a loop over
|
|
78
|
+
* {@link parsePrivyProperties}. For each linked wallet it calls
|
|
79
|
+
* `analytics.identify({ address, userId: user.id }, …)` with the shared
|
|
80
|
+
* profile properties plus that wallet's `wallet_client`, `chain_type`, and
|
|
81
|
+
* `is_embedded` metadata. Because every wallet is tagged with the same Privy
|
|
82
|
+
* `userId`, Formo can cluster them server-side into a single user.
|
|
83
|
+
*
|
|
84
|
+
* Attribution: only the active wallet (see
|
|
85
|
+
* {@link IdentifyPrivyUserOptions.activeAddress}) promotes the SDK's current
|
|
86
|
+
* address/user; every other linked wallet is recorded purely for clustering and
|
|
87
|
+
* does not repoint attribution. Because the clustering identifies don't touch
|
|
88
|
+
* active state, a connected wallet that isn't linked in Privy is left untouched
|
|
89
|
+
* rather than overwritten. This does not change the public `identify()` API.
|
|
90
|
+
*
|
|
91
|
+
* Before emitting, it reconciles the SDK's chain id with the active wallet's
|
|
92
|
+
* chain namespace (clearing a stale EVM chain id when a Solana wallet becomes
|
|
93
|
+
* active, and vice versa), so identifies aren't dropped by an `excludeChains`
|
|
94
|
+
* gate and later events aren't paired with the wrong chain. This happens here,
|
|
95
|
+
* so the direct helper and the `formo.identify(user, { privy: true })` form
|
|
96
|
+
* behave identically.
|
|
97
|
+
*
|
|
98
|
+
* Returns the active linked wallet's `{ address, chainType }` (the one now
|
|
99
|
+
* owning attribution), or `undefined` when no linked wallet matched the
|
|
100
|
+
* requested active address (or the user had no wallets).
|
|
101
|
+
*
|
|
102
|
+
* All linked wallet addresses used here come from `user.linkedAccounts`, which
|
|
103
|
+
* is fully available on the frontend from Privy's `usePrivy()` hook.
|
|
104
|
+
*
|
|
105
|
+
* Note: `identify()` is keyed on a wallet address, so a Privy user with no
|
|
106
|
+
* linked wallet is a no-op (nothing is emitted). Attaching a user identity that
|
|
107
|
+
* has no wallet is out of scope for this address-keyed helper.
|
|
108
|
+
*
|
|
109
|
+
* @param analytics - The Formo analytics instance (e.g. from `useFormo()`)
|
|
110
|
+
* @param user - The Privy user object from `usePrivy()`
|
|
111
|
+
* @param options - See {@link IdentifyPrivyUserOptions}
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* import { identifyPrivyUser } from '@formo/analytics';
|
|
116
|
+
*
|
|
117
|
+
* const { user } = usePrivy();
|
|
118
|
+
* const { wallets } = useWallets();
|
|
119
|
+
* if (user) {
|
|
120
|
+
* // activeAddress is optional - omit it if the SDK already tracks the
|
|
121
|
+
* // connected wallet via a wagmi/EIP-1193 connect.
|
|
122
|
+
* await identifyPrivyUser(formo, user, {
|
|
123
|
+
* activeAddress: wallets[0]?.address,
|
|
124
|
+
* });
|
|
125
|
+
* }
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export declare function identifyPrivyUser(analytics: IFormoAnalytics, user: PrivyUser, options?: IdentifyPrivyUserOptions): Promise<{
|
|
129
|
+
address: string;
|
|
130
|
+
chainType?: string;
|
|
131
|
+
} | undefined>;
|
|
32
132
|
//# sourceMappingURL=utils.d.ts.map
|