@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.
Files changed (53) hide show
  1. package/README.md +3 -0
  2. package/dist/cjs/src/FormoAnalytics.d.ts +65 -19
  3. package/dist/cjs/src/FormoAnalytics.js +187 -94
  4. package/dist/cjs/src/core.d.ts +2 -2
  5. package/dist/cjs/src/core.js +2 -1
  6. package/dist/cjs/src/event/EventFactory.d.ts +10 -4
  7. package/dist/cjs/src/event/EventFactory.js +50 -23
  8. package/dist/cjs/src/event/constants.d.ts +10 -1
  9. package/dist/cjs/src/event/constants.js +20 -8
  10. package/dist/cjs/src/event/sanitize.d.ts +13 -0
  11. package/dist/cjs/src/event/sanitize.js +94 -0
  12. package/dist/cjs/src/privy/index.d.ts +8 -3
  13. package/dist/cjs/src/privy/index.js +8 -3
  14. package/dist/cjs/src/privy/types.d.ts +25 -2
  15. package/dist/cjs/src/privy/utils.d.ts +100 -0
  16. package/dist/cjs/src/privy/utils.js +375 -16
  17. package/dist/cjs/src/session/index.d.ts +73 -6
  18. package/dist/cjs/src/session/index.js +309 -12
  19. package/dist/cjs/src/solana/SolanaManager.d.ts +1 -1
  20. package/dist/cjs/src/solana/SolanaManager.js +1 -1
  21. package/dist/cjs/src/solana/storeTypes.d.ts +1 -1
  22. package/dist/cjs/src/solana/storeTypes.js +1 -1
  23. package/dist/cjs/src/solana/types.d.ts +2 -2
  24. package/dist/cjs/src/types/base.d.ts +19 -3
  25. package/dist/cjs/src/version.d.ts +1 -1
  26. package/dist/cjs/src/version.js +1 -1
  27. package/dist/esm/src/FormoAnalytics.d.ts +65 -19
  28. package/dist/esm/src/FormoAnalytics.js +187 -94
  29. package/dist/esm/src/core.d.ts +2 -2
  30. package/dist/esm/src/core.js +1 -1
  31. package/dist/esm/src/event/EventFactory.d.ts +10 -4
  32. package/dist/esm/src/event/EventFactory.js +51 -24
  33. package/dist/esm/src/event/constants.d.ts +10 -1
  34. package/dist/esm/src/event/constants.js +19 -8
  35. package/dist/esm/src/event/sanitize.d.ts +13 -0
  36. package/dist/esm/src/event/sanitize.js +88 -0
  37. package/dist/esm/src/privy/index.d.ts +8 -3
  38. package/dist/esm/src/privy/index.js +7 -3
  39. package/dist/esm/src/privy/types.d.ts +25 -2
  40. package/dist/esm/src/privy/utils.d.ts +100 -0
  41. package/dist/esm/src/privy/utils.js +374 -16
  42. package/dist/esm/src/session/index.d.ts +73 -6
  43. package/dist/esm/src/session/index.js +309 -12
  44. package/dist/esm/src/solana/SolanaManager.d.ts +1 -1
  45. package/dist/esm/src/solana/SolanaManager.js +1 -1
  46. package/dist/esm/src/solana/storeTypes.d.ts +1 -1
  47. package/dist/esm/src/solana/storeTypes.js +1 -1
  48. package/dist/esm/src/solana/types.d.ts +2 -2
  49. package/dist/esm/src/types/base.d.ts +19 -3
  50. package/dist/esm/src/version.d.ts +1 -1
  51. package/dist/esm/src/version.js +1 -1
  52. package/dist/index.umd.min.js +1 -1
  53. package/package.json +8 -8
@@ -70,6 +70,7 @@ var mergeDeepRight_1 = __importDefault(require("../ramda/mergeDeepRight"));
70
70
  var storage_1 = require("../storage");
71
71
  var version_1 = require("../version");
72
72
  var constants_2 = require("./constants");
73
+ var sanitize_1 = require("./sanitize");
73
74
  var utils_2 = require("./utils");
74
75
  var browsers_1 = require("../browser/browsers");
75
76
  var ISO_3166_ALPHA_2_REGEX = /^[A-Z]{2}$/;
@@ -113,10 +114,9 @@ var EventFactory = /** @class */ (function () {
113
114
  // - If no referral config exists → use defaults
114
115
  // - If referral config exists but queryParams is undefined → use defaults
115
116
  // - If referral config exists with queryParams → use those
116
- var defaultParams = ["ref", "referral", "refcode"];
117
117
  var referralParams = !((_a = _this.options) === null || _a === void 0 ? void 0 : _a.referral)
118
- ? defaultParams // No referral config at all → use defaults
119
- : ((_b = _this.options.referral.queryParams) !== null && _b !== void 0 ? _b : defaultParams); // Has config → use queryParams or defaults
118
+ ? constants_2.DEFAULT_REFERRAL_PARAMS // No referral config at all → use defaults
119
+ : ((_b = _this.options.referral.queryParams) !== null && _b !== void 0 ? _b : constants_2.DEFAULT_REFERRAL_PARAMS); // Has config → use queryParams or defaults
120
120
  // Check query parameters (if any configured)
121
121
  for (var _i = 0, referralParams_1 = referralParams; _i < referralParams_1.length; _i++) {
122
122
  var param = referralParams_1[_i];
@@ -139,7 +139,7 @@ var EventFactory = /** @class */ (function () {
139
139
  /**
140
140
  * Returns the document referrer with same-host referrers filtered out.
141
141
  * Internal navigation populates `document.referrer` with the previous page
142
- * on the same site, which is not an attribution signal treating it as
142
+ * on the same site, which is not an attribution signal - treating it as
143
143
  * "external" would otherwise let an internal URL become the session's
144
144
  * first-touch referrer after a direct landing.
145
145
  */
@@ -158,12 +158,16 @@ var EventFactory = /** @class */ (function () {
158
158
  };
159
159
  this.getTrafficSources = function (url) {
160
160
  var urlObj = new URL(url);
161
- var contextTrafficSources = __assign(__assign(__assign({}, _this.extractUTMParameters(url)), _this.extractClickIdParameters(urlObj)), { ref: _this.extractReferralParameter(urlObj), referrer: _this.getExternalReferrer() });
161
+ // Sanitize at the source so scanner-injected garbage (XSS probes in
162
+ // utm_*/click-id/ref query params) never wins the context-over-stored
163
+ // merge below, never gets persisted, and never reaches an event.
164
+ var contextTrafficSources = (0, sanitize_1.sanitizeTrafficSources)(__assign(__assign(__assign({}, _this.extractUTMParameters(url)), _this.extractClickIdParameters(urlObj)), { ref: _this.extractReferralParameter(urlObj), referrer: _this.getExternalReferrer() }));
162
165
  // Sticky traffic sources may have been persisted by an older SDK version or
163
166
  // a looser config, before the current excludeQueryParams was in effect.
164
167
  // Honor the current denylist on the way out so excluded values can never
165
- // resurface from session storage (or get re-persisted below).
166
- var storedTrafficSources = _this.redactStoredTrafficSources((0, storage_1.session)().get(constants_1.SESSION_TRAFFIC_SOURCE_KEY) || {});
168
+ // resurface from session storage (or get re-persisted below). Sanitizing
169
+ // here too flushes poisoned values persisted by a pre-sanitization SDK.
170
+ var storedTrafficSources = (0, sanitize_1.sanitizeTrafficSources)(_this.redactStoredTrafficSources((0, storage_1.session)().get(constants_1.SESSION_TRAFFIC_SOURCE_KEY) || {}));
167
171
  var mergedClickIds = {};
168
172
  for (var _i = 0, CLICK_ID_PARAMS_2 = constants_2.CLICK_ID_PARAMS; _i < CLICK_ID_PARAMS_2.length; _i++) {
169
173
  var p = CLICK_ID_PARAMS_2[_i];
@@ -211,13 +215,14 @@ var EventFactory = /** @class */ (function () {
211
215
  try {
212
216
  urlObj = new URL(globalThis.location.href);
213
217
  _this.redactQueryParams(urlObj);
218
+ _this.normalizeUrlPath(urlObj);
214
219
  }
215
220
  catch (_a) { }
216
221
  if ((0, validators_1.isUndefined)(pageProps.url)) {
217
222
  pageProps.url = urlObj ? urlObj.href : globalThis.location.href;
218
223
  }
219
224
  if ((0, validators_1.isUndefined)(pageProps.path)) {
220
- pageProps.path = globalThis.location.pathname;
225
+ pageProps.path = urlObj ? urlObj.pathname : globalThis.location.pathname;
221
226
  }
222
227
  if ((0, validators_1.isUndefined)(pageProps.hash)) {
223
228
  pageProps.hash = globalThis.location.hash;
@@ -312,6 +317,16 @@ var EventFactory = /** @class */ (function () {
312
317
  EventFactory.prototype.isExcludedQueryParam = function (key) {
313
318
  return this.excludedQueryParams.has(key.toLowerCase());
314
319
  };
320
+ /**
321
+ * Normalize URL paths for analytics aggregation by stripping trailing slashes
322
+ * from non-root paths. Query strings and hash fragments are preserved by
323
+ * mutating only the URL pathname.
324
+ */
325
+ EventFactory.prototype.normalizeUrlPath = function (url) {
326
+ if (url.pathname !== "/") {
327
+ url.pathname = url.pathname.replace(/\/+$/, "");
328
+ }
329
+ };
315
330
  /**
316
331
  * Strip excluded (sensitive) query parameters from a URL in place. Only the
317
332
  * query string is touched; the path and hash/fragment are left as-is.
@@ -329,9 +344,9 @@ var EventFactory = /** @class */ (function () {
329
344
  keysToDelete.forEach(function (key) { return url.searchParams.delete(key); });
330
345
  };
331
346
  /**
332
- * Return the given absolute URL with excluded query parameters removed. The
333
- * input is returned unchanged when it is empty or cannot be parsed (e.g. an
334
- * empty referrer).
347
+ * Return the given absolute URL with excluded query parameters removed and
348
+ * trailing slashes stripped from non-root paths. The input is returned
349
+ * unchanged when it is empty or cannot be parsed (e.g. an empty referrer).
335
350
  */
336
351
  EventFactory.prototype.redactUrl = function (href) {
337
352
  if (!href)
@@ -339,6 +354,7 @@ var EventFactory = /** @class */ (function () {
339
354
  try {
340
355
  var url = new URL(href);
341
356
  this.redactQueryParams(url);
357
+ this.normalizeUrlPath(url);
342
358
  return url.href;
343
359
  }
344
360
  catch (_a) {
@@ -582,8 +598,9 @@ var EventFactory = /** @class */ (function () {
582
598
  EventFactory.prototype.create = function (event, address, userId) {
583
599
  return __awaiter(this, void 0, void 0, function () {
584
600
  var formoEvent, _a, chainId;
585
- return __generator(this, function (_b) {
586
- switch (_b.label) {
601
+ var _b, _c;
602
+ return __generator(this, function (_d) {
603
+ switch (_d.label) {
587
604
  case 0:
588
605
  formoEvent = {};
589
606
  _a = event.type;
@@ -601,39 +618,39 @@ var EventFactory = /** @class */ (function () {
601
618
  return [3 /*break*/, 17];
602
619
  case 1: return [4 /*yield*/, this.generatePageEvent(event.category, event.name, event.properties, event.context)];
603
620
  case 2:
604
- formoEvent = _b.sent();
621
+ formoEvent = _d.sent();
605
622
  return [3 /*break*/, 19];
606
623
  case 3: return [4 /*yield*/, this.generateDetectWalletEvent(event.providerName, event.rdns, event.properties, event.context)];
607
624
  case 4:
608
- formoEvent = _b.sent();
625
+ formoEvent = _d.sent();
609
626
  return [3 /*break*/, 19];
610
627
  case 5: return [4 /*yield*/, this.generateIdentifyEvent(event.providerName, event.rdns, event.address, event.userId, event.properties, event.context)];
611
628
  case 6:
612
- formoEvent = _b.sent();
629
+ formoEvent = _d.sent();
613
630
  return [3 /*break*/, 19];
614
631
  case 7: return [4 /*yield*/, this.generateChainChangedEvent(event.chainId, event.address, event.properties, event.context)];
615
632
  case 8:
616
- formoEvent = _b.sent();
633
+ formoEvent = _d.sent();
617
634
  return [3 /*break*/, 19];
618
635
  case 9: return [4 /*yield*/, this.generateConnectEvent(event.chainId, event.address, event.properties, event.context)];
619
636
  case 10:
620
- formoEvent = _b.sent();
637
+ formoEvent = _d.sent();
621
638
  return [3 /*break*/, 19];
622
639
  case 11: return [4 /*yield*/, this.generateDisconnectEvent(event.chainId, event.address, event.properties, event.context)];
623
640
  case 12:
624
- formoEvent = _b.sent();
641
+ formoEvent = _d.sent();
625
642
  return [3 /*break*/, 19];
626
643
  case 13: return [4 /*yield*/, this.generateSignatureEvent(event.status, event.chainId, event.address, event.message, event.properties, event.context)];
627
644
  case 14:
628
- formoEvent = _b.sent();
645
+ formoEvent = _d.sent();
629
646
  return [3 /*break*/, 19];
630
647
  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)];
631
648
  case 16:
632
- formoEvent = _b.sent();
649
+ formoEvent = _d.sent();
633
650
  return [3 /*break*/, 19];
634
651
  case 17: return [4 /*yield*/, this.generateTrackEvent(event.event, event.properties, event.context)];
635
652
  case 18:
636
- formoEvent = _b.sent();
653
+ formoEvent = _d.sent();
637
654
  return [3 /*break*/, 19];
638
655
  case 19:
639
656
  // Set address if not already set by the specific event generator
@@ -644,7 +661,17 @@ var EventFactory = /** @class */ (function () {
644
661
  chainId = 'chainId' in event ? event.chainId : undefined;
645
662
  formoEvent.address = this.validateEventAddress(address, chainId);
646
663
  }
647
- formoEvent.user_id = userId || null;
664
+ // An identify event asserts an explicit identity in its own payload (e.g. a
665
+ // Privy DID for each wallet being clustered). Keep that payload user_id
666
+ // rather than overwriting it with the active-session user id - otherwise a
667
+ // clustering identify that intentionally leaves the active user unchanged
668
+ // (setActive:false) would be stripped of its DID, defeating server-side
669
+ // wallet clustering. Fall back to the active-session user id when the
670
+ // identify payload carries none; all other events use the session user id.
671
+ formoEvent.user_id =
672
+ event.type === "identify"
673
+ ? (_c = (_b = formoEvent.user_id) !== null && _b !== void 0 ? _b : userId) !== null && _c !== void 0 ? _c : null
674
+ : userId || null;
648
675
  return [2 /*return*/, formoEvent];
649
676
  }
650
677
  });
@@ -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
@@ -9,7 +9,7 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
9
9
  return to.concat(ar || Array.prototype.slice.call(from));
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.PAGE_PROPERTIES_EXCLUDED_FIELDS = exports.DEFAULT_EXCLUDED_QUERY_PARAMS = exports.CLICK_ID_PARAMS = exports.VERSION = exports.CHANNEL = void 0;
12
+ exports.PAGE_PROPERTIES_EXCLUDED_FIELDS = exports.DEFAULT_REFERRAL_PARAMS = exports.DEFAULT_EXCLUDED_QUERY_PARAMS = exports.CLICK_ID_PARAMS = exports.VERSION = exports.CHANNEL = void 0;
13
13
  var CHANNEL = "web";
14
14
  exports.CHANNEL = CHANNEL;
15
15
  var VERSION = "0";
@@ -47,24 +47,36 @@ var DEFAULT_EXCLUDED_QUERY_PARAMS = [
47
47
  "privy_oauth_provider",
48
48
  ];
49
49
  exports.DEFAULT_EXCLUDED_QUERY_PARAMS = DEFAULT_EXCLUDED_QUERY_PARAMS;
50
+ /**
51
+ * Default query parameter names checked (in order) for a referral code on the
52
+ * landing-page URL. The first parameter present supplies the `ref` traffic
53
+ * source. Consumers can override this list via `referral.queryParams`.
54
+ *
55
+ * Keep in sync with the ReferralOptions.queryParams @default in
56
+ * src/types/base.ts.
57
+ */
58
+ var DEFAULT_REFERRAL_PARAMS = [
59
+ 'ref',
60
+ 'referral',
61
+ 'refcode',
62
+ 'af',
63
+ 'referrer',
64
+ ];
65
+ exports.DEFAULT_REFERRAL_PARAMS = DEFAULT_REFERRAL_PARAMS;
50
66
  /**
51
67
  * Fields that should be excluded from page event properties parsing
52
68
  * These are either:
53
69
  * - Already captured in event context (UTM params, referral params, click IDs)
54
70
  * - Semantic event properties that should not be overridden by URL params
55
71
  */
56
- var PAGE_PROPERTIES_EXCLUDED_FIELDS = new Set(__spreadArray(__spreadArray([
72
+ var PAGE_PROPERTIES_EXCLUDED_FIELDS = new Set(__spreadArray(__spreadArray(__spreadArray([
57
73
  // Context fields (already captured in event context)
58
74
  'utm_source',
59
75
  'utm_medium',
60
76
  'utm_campaign',
61
77
  'utm_term',
62
- 'utm_content',
63
- 'ref',
64
- 'referral',
65
- 'refcode',
66
- 'referrer'
67
- ], CLICK_ID_PARAMS, true), [
78
+ 'utm_content'
79
+ ], DEFAULT_REFERRAL_PARAMS, true), CLICK_ID_PARAMS, true), [
68
80
  // Semantic event properties (should not be overridden by URL params)
69
81
  'category',
70
82
  'name',
@@ -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,94 @@
1
+ "use strict";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.sanitizeTrafficSources = exports.sanitizeUtm = exports.sanitizeRef = exports.sanitizeClickId = void 0;
15
+ var constants_1 = require("./constants");
16
+ /**
17
+ * Traffic-source value sanitization.
18
+ *
19
+ * Vulnerability scanners (e.g. Acunetix) crawl customer sites injecting XSS
20
+ * probes such as `javascript:domxssExecutionSink(1,"'\"><xsstag>()locxss")`
21
+ * or `<script>alert(1)</script>` into every query parameter. Without
22
+ * validation those payloads are captured verbatim as utm_* / click-id / ref
23
+ * values, persisted as sticky session traffic sources, and pollute the
24
+ * customer's attribution reporting. Each field class gets the tightest rule
25
+ * its legitimate values allow (verified against production data):
26
+ *
27
+ * - Click IDs are opaque platform-generated tokens (base64url-ish); every
28
+ * legitimate production value matches the strict token pattern.
29
+ * - Referral codes are short tokens; >99.5% of production values match the
30
+ * strict pattern and none of the remainder are legitimate (scanner
31
+ * payloads, mangled encodings, URLs glued to codes).
32
+ * - UTM values are free-form (spaces, unicode, `+` are legitimate), so they
33
+ * only reject markup/quote characters, dangerous URL schemes, control and
34
+ * zero-width characters, and absurd lengths.
35
+ *
36
+ * Invalid values are dropped to "" — the same representation as "parameter
37
+ * absent" — rather than repaired, so a poisoned value can never be persisted
38
+ * or reported.
39
+ */
40
+ var CLICK_ID_PATTERN = /^[A-Za-z0-9._-]{1,255}$/;
41
+ var REF_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
42
+ var UTM_MAX_LENGTH = 255;
43
+ // Markup/quote/backslash characters plus C0/C1 control characters and
44
+ // zero-width / bidi / BOM / replacement characters (mangled-encoding
45
+ // markers). Explicit ranges instead of \p{C} to avoid the `u`-flag
46
+ // property-escape requirement.
47
+ var UTM_FORBIDDEN_CHARS = /[<>"'`\\\u0000-\u001f\u007f-\u009f\u200b-\u200f\u2028-\u202e\u2060\ufeff\ufffd]/;
48
+ // Values smuggling an executable/URL scheme, e.g. `javascript:alert(1)`.
49
+ var FORBIDDEN_SCHEME_PREFIX = /^\s*(javascript|data|vbscript):/i;
50
+ var sanitizeClickId = function (value) {
51
+ return CLICK_ID_PATTERN.test(value) ? value : "";
52
+ };
53
+ exports.sanitizeClickId = sanitizeClickId;
54
+ var sanitizeRef = function (value) {
55
+ return REF_PATTERN.test(value) ? value : "";
56
+ };
57
+ exports.sanitizeRef = sanitizeRef;
58
+ var sanitizeUtm = function (value) {
59
+ return value.length <= UTM_MAX_LENGTH &&
60
+ !UTM_FORBIDDEN_CHARS.test(value) &&
61
+ !FORBIDDEN_SCHEME_PREFIX.test(value)
62
+ ? value
63
+ : "";
64
+ };
65
+ exports.sanitizeUtm = sanitizeUtm;
66
+ var CLICK_ID_KEYS = new Set(constants_1.CLICK_ID_PARAMS);
67
+ /**
68
+ * Sanitize every traffic-source field of a (possibly sparse) traffic-source
69
+ * object. `referrer` is left untouched: it is a browser-set URL already
70
+ * handled by redactUrl, not an attacker-controlled query parameter. Unknown
71
+ * keys fall through to the UTM rule, the most permissive one.
72
+ */
73
+ var sanitizeTrafficSources = function (trafficSources) {
74
+ var result = __assign({}, trafficSources);
75
+ for (var _i = 0, _a = Object.keys(result); _i < _a.length; _i++) {
76
+ var key = _a[_i];
77
+ var value = result[key];
78
+ if (typeof value !== "string" || value === "" || key === "referrer") {
79
+ continue;
80
+ }
81
+ if (CLICK_ID_KEYS.has(key)) {
82
+ result[key] = sanitizeClickId(value);
83
+ }
84
+ else if (key === "ref") {
85
+ result[key] = sanitizeRef(value);
86
+ }
87
+ else {
88
+ result[key] = sanitizeUtm(value);
89
+ }
90
+ }
91
+ return result;
92
+ };
93
+ exports.sanitizeTrafficSources = sanitizeTrafficSources;
94
+ //# 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
- * This module exports the property extraction utility and related types.
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
@@ -2,11 +2,16 @@
2
2
  /**
3
3
  * Privy integration module
4
4
  *
5
- * Provides utilities for enriching wallet profiles with Privy user data.
6
- * This module exports the property extraction utility and related types.
5
+ * Provides utilities for enriching wallet profiles with Privy user data:
6
+ * `parsePrivyProperties` (low-level parsing) and `identifyPrivyUser` (identify
7
+ * every linked wallet under the user's DID). The same behavior is also
8
+ * available as `formo.identify(user, { privy: true })`.
9
+ *
10
+ * This module is React-free so it can be used from the `core` entry.
7
11
  */
8
12
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.parsePrivyProperties = void 0;
13
+ exports.identifyPrivyUser = exports.parsePrivyProperties = void 0;
10
14
  var utils_1 = require("./utils");
11
15
  Object.defineProperty(exports, "parsePrivyProperties", { enumerable: true, get: function () { return utils_1.parsePrivyProperties; } });
16
+ Object.defineProperty(exports, "identifyPrivyUser", { enumerable: true, get: function () { return utils_1.identifyPrivyUser; } });
12
17
  //# 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
- /** Account creation timestamp */
55
- createdAt?: Date;
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