@formo/analytics 1.33.1 → 1.34.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 (45) 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 +188 -94
  4. package/dist/cjs/src/event/EventFactory.d.ts +1 -1
  5. package/dist/cjs/src/event/EventFactory.js +32 -16
  6. package/dist/cjs/src/event/sanitize.d.ts +13 -0
  7. package/dist/cjs/src/event/sanitize.js +94 -0
  8. package/dist/cjs/src/privy/index.d.ts +8 -2
  9. package/dist/cjs/src/privy/index.js +8 -2
  10. package/dist/cjs/src/privy/types.d.ts +25 -2
  11. package/dist/cjs/src/privy/utils.d.ts +100 -0
  12. package/dist/cjs/src/privy/utils.js +375 -16
  13. package/dist/cjs/src/session/index.d.ts +73 -6
  14. package/dist/cjs/src/session/index.js +309 -12
  15. package/dist/cjs/src/solana/SolanaManager.d.ts +1 -1
  16. package/dist/cjs/src/solana/SolanaManager.js +1 -1
  17. package/dist/cjs/src/solana/storeTypes.d.ts +1 -1
  18. package/dist/cjs/src/solana/storeTypes.js +1 -1
  19. package/dist/cjs/src/solana/types.d.ts +2 -2
  20. package/dist/cjs/src/types/base.d.ts +17 -1
  21. package/dist/cjs/src/version.d.ts +1 -1
  22. package/dist/cjs/src/version.js +1 -1
  23. package/dist/esm/src/FormoAnalytics.d.ts +65 -19
  24. package/dist/esm/src/FormoAnalytics.js +188 -94
  25. package/dist/esm/src/event/EventFactory.d.ts +1 -1
  26. package/dist/esm/src/event/EventFactory.js +32 -16
  27. package/dist/esm/src/event/sanitize.d.ts +13 -0
  28. package/dist/esm/src/event/sanitize.js +88 -0
  29. package/dist/esm/src/privy/index.d.ts +8 -2
  30. package/dist/esm/src/privy/index.js +8 -2
  31. package/dist/esm/src/privy/types.d.ts +25 -2
  32. package/dist/esm/src/privy/utils.d.ts +100 -0
  33. package/dist/esm/src/privy/utils.js +374 -16
  34. package/dist/esm/src/session/index.d.ts +73 -6
  35. package/dist/esm/src/session/index.js +309 -12
  36. package/dist/esm/src/solana/SolanaManager.d.ts +1 -1
  37. package/dist/esm/src/solana/SolanaManager.js +1 -1
  38. package/dist/esm/src/solana/storeTypes.d.ts +1 -1
  39. package/dist/esm/src/solana/storeTypes.js +1 -1
  40. package/dist/esm/src/solana/types.d.ts +2 -2
  41. package/dist/esm/src/types/base.d.ts +17 -1
  42. package/dist/esm/src/version.d.ts +1 -1
  43. package/dist/esm/src/version.js +1 -1
  44. package/dist/index.umd.min.js +1 -1
  45. package/package.json +7 -7
@@ -21,6 +21,172 @@ exports.FormoAnalyticsSession = exports.SESSION_WALLET_IDENTIFIED_KEY = exports.
21
21
  var storage_1 = require("../storage");
22
22
  var cookiePolicy_1 = require("../storage/cookiePolicy");
23
23
  var logger_1 = require("../logger");
24
+ /**
25
+ * Serialize a value so that equal values always produce the same string,
26
+ * regardless of object key insertion order.
27
+ *
28
+ * `JSON.stringify` preserves insertion order, so `{a:1,b:2}` and `{b:2,a:1}`
29
+ * would hash differently and re-emit an identify that carries identical
30
+ * properties. Object keys are therefore sorted; arrays keep their order, since
31
+ * order is meaningful there.
32
+ *
33
+ * **This function must never throw.** It runs inside `identify()` *after* the
34
+ * active address/user have been updated, so a throw would leave the SDK with
35
+ * mutated identity state and no emitted event. `JSON.stringify` throws on
36
+ * circular references and on `BigInt` - both realistic here, since a web3 app
37
+ * can easily pass a token balance (`123n`) or a wallet object with a back
38
+ * reference in `properties`. Every such case is therefore handled explicitly
39
+ * and degrades to a stable marker instead of an exception.
40
+ *
41
+ * Totality here is only about *dedup*: it guarantees the fingerprint step can't
42
+ * be what loses an identify. It does **not** make such values sendable - the
43
+ * event queue still serializes with native `JSON.stringify`, so a `BigInt` or
44
+ * circular value in `properties` fails downstream exactly as it does for
45
+ * `track()`. That is a separate, pre-existing SDK-wide limitation.
46
+ *
47
+ * It mirrors `JSON.stringify`'s own value semantics so the fingerprint tracks
48
+ * **what actually goes on the wire**, not what happens to be in the object:
49
+ *
50
+ * - `undefined`, functions, and symbols are *omitted* as object properties and
51
+ * become `null` as array elements, exactly as JSON does. Two property sets
52
+ * that serialize identically must fingerprint identically, or dedup emits a
53
+ * second identify carrying a byte-identical payload.
54
+ * - `null` stays `null`, so a real value → `null` update is never mistaken for
55
+ * an omitted key.
56
+ * - `NaN`/`Infinity` become `null`, because that is what is sent.
57
+ * - `toJSON()` is honored, so `Date` and `URL` reflect their serialized form
58
+ * rather than their (often empty) enumerable keys.
59
+ * - `Map`/`Set` have no enumerable own properties and no `toJSON`, so they send
60
+ * as `{}` - and therefore canonicalize as `{}`.
61
+ *
62
+ * The exceptions are the two things JSON *cannot* represent: `BigInt` and
63
+ * circular references both make `JSON.stringify` throw. They get distinct
64
+ * markers instead, because this function must never throw - it runs inside
65
+ * `identify()` *after* the active address/user have been updated, so a throw
66
+ * would leave the SDK with mutated identity state and no emitted event.
67
+ *
68
+ * Totality here is only about *dedup*: it guarantees the fingerprint step can't
69
+ * be what loses an identify. It does **not** make such values sendable - the
70
+ * event queue still serializes with native `JSON.stringify`, so a `BigInt` or
71
+ * circular value in `properties` fails downstream exactly as it does for
72
+ * `track()`. That is a separate, pre-existing SDK-wide limitation.
73
+ *
74
+ * Returns `undefined` when JSON would omit the value entirely.
75
+ */
76
+ function stableStringify(value, seen) {
77
+ if (seen === void 0) { seen = new Set(); }
78
+ // JSON omits these as object properties; array elements are mapped to "null"
79
+ // by the caller below.
80
+ if (value === undefined)
81
+ return undefined;
82
+ if (typeof value === "function")
83
+ return undefined;
84
+ if (typeof value === "symbol")
85
+ return undefined;
86
+ if (value === null)
87
+ return "null";
88
+ // JSON.stringify throws on BigInt, so it has no wire form to mirror.
89
+ if (typeof value === "bigint")
90
+ return "bigint:".concat(value.toString());
91
+ if (typeof value === "number") {
92
+ // NaN and Infinity are sent as null.
93
+ return Number.isFinite(value) ? JSON.stringify(value) : "null";
94
+ }
95
+ if (typeof value !== "object") {
96
+ // Remaining primitives: string, boolean.
97
+ return JSON.stringify(value);
98
+ }
99
+ // Cycle guard: a repeated reference within the current path is replaced by a
100
+ // marker rather than recursing forever. JSON.stringify would throw here.
101
+ if (seen.has(value))
102
+ return '"[circular]"';
103
+ seen.add(value);
104
+ try {
105
+ // JSON calls toJSON() before inspecting the value, so do it first. This is
106
+ // what makes an invalid Date canonicalize as null (Date.prototype.toJSON
107
+ // returns null rather than throwing) and a URL reflect its href.
108
+ var maybeToJSON = value.toJSON;
109
+ if (typeof maybeToJSON === "function") {
110
+ return stableStringify(maybeToJSON.call(value), seen);
111
+ }
112
+ if (Array.isArray(value)) {
113
+ return "[".concat(value
114
+ .map(function (item) { var _a; return (_a = stableStringify(item, seen)) !== null && _a !== void 0 ? _a : "null"; })
115
+ .join(","), "]");
116
+ }
117
+ // Everything else, Map and Set included, serializes from its enumerable own
118
+ // properties. Keys are sorted so insertion order can't change the result.
119
+ var record = value;
120
+ var parts = [];
121
+ for (var _i = 0, _a = Object.keys(record).sort(); _i < _a.length; _i++) {
122
+ var key = _a[_i];
123
+ var encoded = stableStringify(record[key], seen);
124
+ if (encoded === undefined)
125
+ continue; // JSON omits this property
126
+ parts.push("".concat(JSON.stringify(key), ":").concat(encoded));
127
+ }
128
+ return "{".concat(parts.join(","), "}");
129
+ }
130
+ finally {
131
+ // Only guard against cycles, not repeated siblings: the same object used
132
+ // twice in one payload must serialize the same both times.
133
+ seen.delete(value);
134
+ }
135
+ }
136
+ /**
137
+ * Short, stable fingerprint of an identify's properties (two FNV-1a lanes,
138
+ * base36).
139
+ *
140
+ * Folded into the dedup key so that re-identifying a known wallet with *changed*
141
+ * properties re-emits, while an unchanged repeat still dedupes. Kept short
142
+ * because every key is stored in a size-bounded cookie.
143
+ *
144
+ * Two independent lanes (different offset bases, combined into ~64 bits) rather
145
+ * than one: a single 32-bit lane collides readily - `{"x":"xefn1fnkq0"}` and
146
+ * `{"x":"filot3n704"}` both hash to `1mgjpo5` - and a collision here silently
147
+ * suppresses a legitimately changed profile for the rest of the session. At 64
148
+ * bits that is no longer a practical concern, for six more characters per key.
149
+ */
150
+ function fingerprintProperties(properties) {
151
+ var _a;
152
+ if (!properties)
153
+ return undefined;
154
+ var serialized;
155
+ try {
156
+ // Canonicalize first, then decide emptiness from the result. Anything that
157
+ // serializes to `{}` - a literal `{}`, or an object whose every value JSON
158
+ // omits - carries no wire payload, so it keeps the legacy no-hash key shape
159
+ // rather than getting a hash of "{}". Doing this inside the guard matters:
160
+ // reading properties can run user code (a Proxy with a throwing ownKeys
161
+ // trap), and this whole function must be total.
162
+ var canonical = stableStringify(properties);
163
+ if (canonical === undefined || canonical === "{}")
164
+ return undefined;
165
+ serialized = canonical;
166
+ }
167
+ catch (error) {
168
+ // stableStringify handles every value type it knows about, but reading a
169
+ // property can still run arbitrary user code (a throwing getter, an exotic
170
+ // Proxy). Identity state has already been updated by the time we get here,
171
+ // so failing closed to a constant is the safe outcome: dedup degrades to
172
+ // the pre-fingerprint behavior (identify once per session for this wallet)
173
+ // instead of the whole identify being swallowed by the catch in identify().
174
+ (_a = logger_1.logger.warn) === null || _a === void 0 ? void 0 : _a.call(logger_1.logger, "Session: failed to fingerprint identify properties", error);
175
+ return "nohash";
176
+ }
177
+ var lane1 = 0x811c9dc5;
178
+ var lane2 = 0x01000193;
179
+ for (var i = 0; i < serialized.length; i++) {
180
+ var code = serialized.charCodeAt(i);
181
+ lane1 ^= code;
182
+ lane1 = Math.imul(lane1, 0x01000193);
183
+ // A second lane with a different seed and multiplier, fed the position as
184
+ // well as the character, so the two lanes don't move together.
185
+ lane2 ^= code + i;
186
+ lane2 = Math.imul(lane2, 0x85ebca6b);
187
+ }
188
+ return "".concat((lane1 >>> 0).toString(36)).concat((lane2 >>> 0).toString(36));
189
+ }
24
190
  /**
25
191
  * Cookie keys for session tracking
26
192
  * NOTE: These values must match the original constants in constants/base.ts
@@ -38,20 +204,130 @@ exports.SESSION_WALLET_IDENTIFIED_KEY = "wallet-identified";
38
204
  * Session data expires at end of day (86400 seconds).
39
205
  */
40
206
  var MAX_SESSION_ENTRIES = 20;
207
+ /**
208
+ * Byte budget for the identified-wallet cookie, measured on the value as the
209
+ * browser actually stores it.
210
+ *
211
+ * A Privy user can identify far more than 20 wallets in one session (an 8+
212
+ * wallet user is the motivating case), so a fixed entry count would evict
213
+ * `(wallet, userId)` keys and let a later sync re-emit them. Instead we bound
214
+ * the store by serialized size and evict oldest only when it would overflow the
215
+ * cookie - so every identity that fits is retained.
216
+ *
217
+ * The budget must be applied to the **encoded** length. Key components are
218
+ * already percent-encoded, and `CookieStorage.set()` then encodes the whole
219
+ * joined value again, so `%3A` becomes `%253A` and each `,` separator becomes
220
+ * `%2C`. Measuring the raw string underestimates what is written: 37 realistic
221
+ * DID-bearing keys measure 3500 raw but 3956 encoded, and a non-ASCII external
222
+ * user id inflates far more than that. Overflowing makes the browser reject the
223
+ * write outright, so nothing is persisted and every identify re-emits for the
224
+ * rest of the session - the exact failure the store exists to prevent.
225
+ */
226
+ var MAX_COOKIE_BYTES = 4096;
227
+ /** Reserve for the cookie name plus path/expires/SameSite/Secure attributes. */
228
+ var COOKIE_OVERHEAD_RESERVE = 512;
229
+ var MAX_IDENTIFIED_ENCODED_BYTES = MAX_COOKIE_BYTES - COOKIE_OVERHEAD_RESERVE;
230
+ /** Length of a cookie value as written, i.e. after CookieStorage encodes it. */
231
+ function encodedCookieLength(value) {
232
+ return encodeURIComponent(value).length;
233
+ }
41
234
  var FormoAnalyticsSession = /** @class */ (function () {
42
235
  function FormoAnalyticsSession() {
43
236
  }
44
237
  /**
45
- * Generate a unique key for wallet identification tracking
46
- * Combines address and RDNS to track specific wallet-address combinations
238
+ * Generate a unique key for wallet identification tracking.
239
+ *
240
+ * Combines address, RDNS, and (optionally) the external user ID and a
241
+ * fingerprint of the identify's properties, so the key identifies a specific
242
+ * wallet-user-profile combination rather than just an address.
243
+ *
244
+ * Folding the user ID in means the same wallet identified first anonymously
245
+ * and later with a user ID (e.g. after a Privy login attaches a DID) produces
246
+ * two distinct keys, so the second identify is not deduped.
247
+ *
248
+ * Folding the properties hash in means a *changed* profile re-emits. This
249
+ * matters for account linking: a Privy user who links a Google account keeps
250
+ * the same wallets and the same DID, so without the hash every already-seen
251
+ * wallet would dedupe and the new `google` property would never reach Formo
252
+ * until the session expired. An identify repeated with identical properties
253
+ * still dedupes, so this does not turn a re-render into an event.
254
+ *
255
+ * Key shapes, by component count - each is unambiguous, so they cannot
256
+ * collide with one another:
257
+ *
258
+ * | Components | Shape | When |
259
+ * | --- | --- | --- |
260
+ * | 1 | `address` | no rdns, no userId, no properties |
261
+ * | 2 | `address:rdns` | rdns only |
262
+ * | 3 | `address:rdns:userId` | userId, no properties |
263
+ * | 4 | `address:rdns:userId:hash` | properties present |
264
+ *
265
+ * Shapes 1 and 2 are unchanged from before user IDs and property hashes
266
+ * existed, so keys already stored in browsers still match (backward
267
+ * compatible). An identify that carries properties moves to shape 4, so the
268
+ * first identify after an upgrade re-emits once per wallet - a one-off, and
269
+ * the correct outcome, since those properties were never recorded under the
270
+ * new key.
47
271
  *
48
272
  * @param address The wallet address
49
273
  * @param rdns The reverse domain name of the wallet provider
274
+ * @param userId Optional external user ID (e.g. a Privy DID)
275
+ * @param properties Optional identify properties, fingerprinted into the key
50
276
  * @returns A unique identification key
51
277
  */
52
- FormoAnalyticsSession.prototype.generateIdentificationKey = function (address, rdns) {
53
- // If rdns is missing, use address-only key as fallback for empty identifies
54
- return rdns ? "".concat(address, ":").concat(rdns) : address;
278
+ FormoAnalyticsSession.prototype.generateIdentificationKey = function (address, rdns, userId, properties) {
279
+ return this.buildIdentificationKey(address, rdns, userId, properties).key;
280
+ };
281
+ /**
282
+ * Build the dedup key plus the **identity prefix** it belongs to.
283
+ *
284
+ * The identity prefix is the key with the properties hash stripped -
285
+ * `address:rdns:userId` - i.e. *which wallet-user this is*, independent of
286
+ * *what profile it last had*. `markWalletIdentified` uses it to drop that
287
+ * identity's previous state before storing the new one, which matters twice:
288
+ *
289
+ * - **Reversion.** Keeping every state seen would make dedup mean "have I
290
+ * ever seen this exact profile", so a profile that goes A → B → A (link
291
+ * then unlink an account) would find the old A key and emit nothing. Dedup
292
+ * should mean "is this the same as this wallet's *last* identify", so only
293
+ * the current state is retained.
294
+ * - **Growth.** Otherwise each profile change adds a key per wallet, and an
295
+ * 8-wallet user linking a few accounts would push the cookie into eviction.
296
+ * Superseding keeps it at one entry per wallet-user.
297
+ *
298
+ * The prefix is only defined for keys that have a userId and/or a properties
299
+ * hash (3+ components). The legacy 1- and 2-component shapes are the whole
300
+ * identity already, so there is nothing to supersede.
301
+ */
302
+ FormoAnalyticsSession.prototype.buildIdentificationKey = function (address, rdns, userId, properties) {
303
+ // Percent-encode each component before joining. The identified-wallet list
304
+ // is persisted comma-joined in a cookie and later split on commas, so a raw
305
+ // comma in an arbitrary external userId would corrupt the key and defeat
306
+ // dedup (the same identify would re-emit on every call). Encoding also keeps
307
+ // the ":" separator unambiguous. Addresses and RDNS contain no reserved
308
+ // characters, so their encoded form is unchanged - existing stored keys
309
+ // still match (backward compatible).
310
+ // An identify with no properties keeps the pre-hash key shape, so the
311
+ // common `identify({ address })` call is unaffected.
312
+ var propertiesHash = fingerprintProperties(properties);
313
+ var parts = [encodeURIComponent(address)];
314
+ if (userId || propertiesHash) {
315
+ // Once any later slot is set, always emit the intervening slots (even when
316
+ // empty) so the tuple has a fixed shape. Otherwise a userId that happens to
317
+ // equal a provider RDNS (e.g. "io.metamask") would produce the same key as
318
+ // an anonymous `address:rdns` identify and be wrongly deduped. userId and
319
+ // hash keys are new, so this shape has no backward-compat cost.
320
+ parts.push(encodeURIComponent(rdns || ""));
321
+ parts.push(encodeURIComponent(userId || ""));
322
+ var identityPrefix = parts.join(":");
323
+ if (propertiesHash)
324
+ parts.push(propertiesHash);
325
+ return { key: parts.join(":"), identityPrefix: identityPrefix };
326
+ }
327
+ if (rdns) {
328
+ parts.push(encodeURIComponent(rdns));
329
+ }
330
+ return { key: parts.join(":") };
55
331
  };
56
332
  /**
57
333
  * Check if a wallet provider has been detected in this session
@@ -90,8 +366,8 @@ var FormoAnalyticsSession = /** @class */ (function () {
90
366
  * @param rdns The reverse domain name of the wallet provider
91
367
  * @returns true if this wallet-address pair has been identified
92
368
  */
93
- FormoAnalyticsSession.prototype.isWalletIdentified = function (address, rdns) {
94
- var identifiedKey = this.generateIdentificationKey(address, rdns);
369
+ FormoAnalyticsSession.prototype.isWalletIdentified = function (address, rdns, userId, properties) {
370
+ var identifiedKey = this.generateIdentificationKey(address, rdns, userId, properties);
95
371
  var cookieValue = (0, storage_1.cookie)().get(exports.SESSION_WALLET_IDENTIFIED_KEY);
96
372
  var identifiedWallets = (cookieValue === null || cookieValue === void 0 ? void 0 : cookieValue.split(",")) || [];
97
373
  var isIdentified = identifiedWallets.includes(identifiedKey);
@@ -109,17 +385,38 @@ var FormoAnalyticsSession = /** @class */ (function () {
109
385
  * @param address The wallet address
110
386
  * @param rdns The reverse domain name of the wallet provider
111
387
  */
112
- FormoAnalyticsSession.prototype.markWalletIdentified = function (address, rdns) {
388
+ FormoAnalyticsSession.prototype.markWalletIdentified = function (address, rdns, userId, properties) {
113
389
  var _a;
114
- var identifiedKey = this.generateIdentificationKey(address, rdns);
390
+ var _b = this.buildIdentificationKey(address, rdns, userId, properties), identifiedKey = _b.key, identityPrefix = _b.identityPrefix;
115
391
  var identifiedWallets = ((_a = (0, storage_1.cookie)().get(exports.SESSION_WALLET_IDENTIFIED_KEY)) === null || _a === void 0 ? void 0 : _a.split(",")) || [];
116
392
  var alreadyExists = identifiedWallets.includes(identifiedKey);
117
393
  if (!alreadyExists) {
118
- identifiedWallets.push(identifiedKey);
119
- if (identifiedWallets.length > MAX_SESSION_ENTRIES) {
120
- identifiedWallets.splice(0, identifiedWallets.length - MAX_SESSION_ENTRIES);
394
+ // Supersede this wallet-user's previous profile state rather than
395
+ // accumulating one key per state. Without this, a profile that reverts to
396
+ // an earlier value (link then unlink an account) would match the stale key
397
+ // and emit nothing, and every profile change would grow the cookie.
398
+ if (identityPrefix) {
399
+ identifiedWallets = identifiedWallets.filter(function (entry) {
400
+ return entry !== identityPrefix &&
401
+ !entry.startsWith("".concat(identityPrefix, ":"));
402
+ });
121
403
  }
404
+ identifiedWallets.push(identifiedKey);
405
+ // Bound the stored list by serialized size (not a fixed entry count) so a
406
+ // many-wallet Privy user's identities all persist, evicting oldest only if
407
+ // the value would overflow the cookie.
408
+ //
409
+ // `shift()` drops the oldest from the front while the new key was pushed
410
+ // to the back, and the loop stops at one entry, so the just-added key can
411
+ // never be evicted. A single key is bounded by its components (address +
412
+ // rdns + external user id + a 13-char hash) and cannot on its own approach
413
+ // the budget, so there is no oversized-single-entry case to handle.
122
414
  var newValue = identifiedWallets.join(",");
415
+ while (identifiedWallets.length > 1 &&
416
+ encodedCookieLength(newValue) > MAX_IDENTIFIED_ENCODED_BYTES) {
417
+ identifiedWallets.shift();
418
+ newValue = identifiedWallets.join(",");
419
+ }
123
420
  (0, storage_1.cookie)().set(exports.SESSION_WALLET_IDENTIFIED_KEY, newValue, __assign({
124
421
  // Expires by the end of the day
125
422
  expires: new Date(Date.now() + 86400 * 1000).toUTCString(), path: "/" }, (0, cookiePolicy_1.getIdentityCookieSecurity)()));
@@ -29,7 +29,7 @@ export declare class SolanaManager {
29
29
  *
30
30
  * @example
31
31
  * ```tsx
32
- * import { createClient } from '@solana-foundation/framework-kit';
32
+ * import { createClient, autoDiscover } from '@solana/client';
33
33
  *
34
34
  * const client = createClient({ endpoint: '...', walletConnectors: autoDiscover() });
35
35
  * formo.solana.setStore(client.store);
@@ -40,7 +40,7 @@ var SolanaManager = /** @class */ (function () {
40
40
  *
41
41
  * @example
42
42
  * ```tsx
43
- * import { createClient } from '@solana-foundation/framework-kit';
43
+ * import { createClient, autoDiscover } from '@solana/client';
44
44
  *
45
45
  * const client = createClient({ endpoint: '...', walletConnectors: autoDiscover() });
46
46
  * formo.solana.setStore(client.store);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Type definitions for framework-kit's zustand store integration.
3
3
  *
4
- * These types mirror the state shape of @solana-foundation/framework-kit's
4
+ * These types mirror the state shape of framework-kit's
5
5
  * vanilla zustand store, allowing the SDK to subscribe to wallet and
6
6
  * transaction state changes without wrapping any wallet methods.
7
7
  *
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Type definitions for framework-kit's zustand store integration.
4
4
  *
5
- * These types mirror the state shape of @solana-foundation/framework-kit's
5
+ * These types mirror the state shape of framework-kit's
6
6
  * vanilla zustand store, allowing the SDK to subscribe to wallet and
7
7
  * transaction state changes without wrapping any wallet methods.
8
8
  *
@@ -54,11 +54,11 @@ export interface SolanaOptions {
54
54
  * When provided, wallet connect/disconnect and transaction events are tracked
55
55
  * automatically by subscribing to zustand store state changes.
56
56
  *
57
- * This is the recommended approach for apps using @solana-foundation/framework-kit.
57
+ * This is the recommended approach for apps using framework-kit.
58
58
  *
59
59
  * @example
60
60
  * ```tsx
61
- * import { createClient } from '@solana-foundation/framework-kit';
61
+ * import { createClient, autoDiscover } from '@solana/client';
62
62
  * const client = createClient({ endpoint, walletConnectors: autoDiscover() });
63
63
  * const formo = await Formo.init(writeKey, { solana: { store: client.store } });
64
64
  * ```
@@ -2,6 +2,7 @@ import { LogLevel } from "../logger";
2
2
  import { IFormoEventContext, IFormoEventProperties, SignatureStatus, TransactionStatus } from "./events";
3
3
  import { EIP1193Provider } from "./provider";
4
4
  import { SolanaOptions } from "../solana/types";
5
+ import type { PrivyUser } from "../privy/types";
5
6
  export type Nullable<T> = T | null;
6
7
  export type ChainID = number;
7
8
  export type Address = string;
@@ -51,6 +52,21 @@ export interface IFormoAnalytics {
51
52
  function_name?: string;
52
53
  function_args?: Record<string, unknown>;
53
54
  }, properties?: IFormoEventProperties, context?: IFormoEventContext, callback?: (...args: unknown[]) => void): Promise<void>;
55
+ /**
56
+ * Privy form. Pass the `usePrivy()` user directly and the SDK identifies
57
+ * every wallet linked to that Privy account under the user's DID in a single
58
+ * call, forwarding each wallet's metadata. Only the active wallet (explicit
59
+ * `activeAddress`, else the already-connected wallet, else Privy's
60
+ * `user.wallet`) takes over event attribution - the rest are recorded purely
61
+ * for identity clustering.
62
+ *
63
+ * Recognized by shape: a Privy user has a string `id` and no `address`,
64
+ * while an address-keyed identify always has an `address`.
65
+ */
66
+ identify(user: PrivyUser, options?: {
67
+ activeAddress?: string;
68
+ properties?: IFormoEventProperties;
69
+ }): Promise<void>;
54
70
  identify(params: {
55
71
  address: Address;
56
72
  providerName?: string;
@@ -75,7 +91,7 @@ export interface TrackingOptions {
75
91
  /**
76
92
  * IANA timezone names to opt out of tracking entirely. When the visitor's
77
93
  * resolved timezone (via `Intl.DateTimeFormat().resolvedOptions().timeZone`)
78
- * matches one of these, no events are enqueued or sent including `identify`
94
+ * matches one of these, no events are enqueued or sent - including `identify`
79
95
  * and `connect`. Matched case-insensitively against the full timezone string.
80
96
  *
81
97
  * Note: this is client-side, timezone-derived geolocation. It is best-effort
@@ -1,2 +1,2 @@
1
- export declare const version = "1.30.1";
1
+ export declare const version = "1.34.1";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -3,5 +3,5 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.version = void 0;
4
4
  // This file is auto-generated by scripts/update-version.js during npm version
5
5
  // Do not edit manually - it will be overwritten
6
- exports.version = '1.30.1';
6
+ exports.version = '1.34.1';
7
7
  //# sourceMappingURL=version.js.map
@@ -1,6 +1,7 @@
1
1
  import { EIP6963ProviderDetail } from "mipd";
2
2
  import { Address, ChainID, Config, EIP1193Provider, IFormoAnalytics, IFormoEventContext, IFormoEventProperties, Options, SignatureStatus, TransactionStatus } from "./types";
3
3
  import { SolanaManager } from "./solana/SolanaManager";
4
+ import type { PrivyUser } from "./privy";
4
5
  export declare class FormoAnalytics implements IFormoAnalytics {
5
6
  readonly writeKey: string;
6
7
  options: Options;
@@ -189,23 +190,50 @@ export declare class FormoAnalytics implements IFormoAnalytics {
189
190
  * // Basic identify
190
191
  * formo.identify({ address: '0x...', userId: 'user123' });
191
192
  *
192
- * // With Privy user
193
- * import { parsePrivyProperties } from '@formo/analytics';
193
+ * // Privy: pass the usePrivy() user to identify every linked wallet under
194
+ * // the user's DID in one call. Attribution stays on the already-connected
195
+ * // wallet when there is one, else Privy's primary (user.wallet); pass
196
+ * // `activeAddress` to pin a specific wallet.
194
197
  * const { user } = usePrivy();
195
- * if (user) {
196
- * const { properties, wallets } = parsePrivyProperties(user);
197
- * for (const wallet of wallets) {
198
- * formo.identify({ address: wallet.address, userId: user.id }, properties);
199
- * }
200
- * }
198
+ * if (user) formo.identify(user);
201
199
  * ```
202
200
  */
201
+ identify(user: PrivyUser, options?: {
202
+ activeAddress?: string;
203
+ properties?: IFormoEventProperties;
204
+ }): Promise<void>;
203
205
  identify(params?: {
204
206
  address: Address;
205
207
  providerName?: string;
206
208
  userId?: string;
207
209
  rdns?: string;
208
210
  }, properties?: IFormoEventProperties, context?: IFormoEventContext, callback?: (...args: unknown[]) => void): Promise<void>;
211
+ /**
212
+ * Reconcile currentChainId with a newly-activated Privy wallet's chain
213
+ * namespace. identify() sets currentAddress but never touches the chain id
214
+ * (that comes from connect()/chain()/wagmi), so activating e.g. a Solana
215
+ * wallet while an EVM chain id is current would leave the address paired with
216
+ * a mismatched chain in events, excludeChains, and the active-wallet cookie.
217
+ *
218
+ * We can't infer the wallet's specific chain id from Privy's chainType, so on
219
+ * a namespace mismatch we clear the chain id rather than assert a wrong one; a
220
+ * real wallet connect will set the correct chain. Same-namespace activations
221
+ * (and wallets whose namespace can't be determined) leave the chain id alone.
222
+ *
223
+ * Privy doesn't always supply `chainType`: a `smart_wallet` entry is
224
+ * `{ type, address, smartWalletType }`, and `cross_app` wallets are bare
225
+ * `{ address }`. A `0x`-prefixed 20-byte address is unambiguously EVM though,
226
+ * so fall back to the address shape - otherwise activating an EVM smart
227
+ * wallet while a Solana chain id is current would leave the address paired
228
+ * with the wrong chain, and an `excludeChains` gate could drop the identify
229
+ * after it was already dedup-marked.
230
+ *
231
+ * @internal Not part of the public IFormoAnalytics contract - invoked by
232
+ * `identifyPrivyUser` (via a structural cast) before it emits, so both the
233
+ * `identify(user,{privy:true})` and direct `identifyPrivyUser()` paths
234
+ * reconcile the chain.
235
+ */
236
+ syncPrivyActiveChain(chainType?: string, address?: string): void;
209
237
  /**
210
238
  * Emits a detect wallet event with current wallet provider info.
211
239
  * @param {string} params.providerName
@@ -286,20 +314,38 @@ export declare class FormoAnalytics implements IFormoAnalytics {
286
314
  * Visitor-level tracking suppression.
287
315
  *
288
316
  * Returns true when the SDK must not persist any identity/session/chain
289
- * state or send any events for this visitor i.e. an explicit opt-out or a
317
+ * state or send any events for this visitor - i.e. an explicit opt-out or a
290
318
  * jurisdiction/timezone exclusion. Public entry points that write state
291
319
  * before reaching the `shouldTrack()` event gate (identify/connect/detect)
292
320
  * check this first so suppressed visitors leave no cookies or session state.
293
321
  * @returns {boolean} True if all tracking and persistence must be suppressed
322
+ * @internal Also read by `identifyPrivyUser` (via a structural cast) so the
323
+ * Privy sync skips chain reconciliation and emission for suppressed visitors.
324
+ */
325
+ isTrackingSuppressed(): boolean;
326
+ /**
327
+ * Whether the current chain id is in `tracking.excludeChains`.
328
+ *
329
+ * Split out from `shouldTrack()` so `identify()` can check it *before*
330
+ * mutating identity state. `trackEvent()` drops an excluded event silently
331
+ * and returns void, but `identify()` marks the wallet as identified first, so
332
+ * without this guard an identify on an excluded chain is dedup-marked and
333
+ * then discarded, and the wallet never re-emits for the rest of the session
334
+ * even after switching to an allowed chain. On the Privy path that loses the
335
+ * user's whole cluster at once rather than a single wallet.
336
+ *
337
+ * Mirrors the chain rule in `shouldTrack()`: only applies when `tracking` is
338
+ * an options object with `excludeChains` set, and only once a chain id is
339
+ * known.
294
340
  */
295
- private isTrackingSuppressed;
341
+ private isCurrentChainExcluded;
296
342
  /**
297
- * Whether the current environment is excluded from tracking the visitor's
343
+ * Whether the current environment is excluded from tracking - the visitor's
298
344
  * timezone, the current hostname, or the current pathname matches a
299
345
  * configured exclusion.
300
346
  *
301
347
  * Timezone is visitor/session-level (stable for the session); host/path are
302
- * current-page-level and transient if a SPA navigates to an allowed path,
348
+ * current-page-level and transient - if a SPA navigates to an allowed path,
303
349
  * tracking resumes for future actions. Used as the "do not write identity or
304
350
  * send events" gate at every entry point that would persist state before the
305
351
  * `shouldTrack()` event gate.
@@ -308,19 +354,19 @@ export declare class FormoAnalytics implements IFormoAnalytics {
308
354
  private isCurrentEnvironmentExcluded;
309
355
  /**
310
356
  * Whether the current hostname matches a configured `tracking.excludeHosts`
311
- * entry (exact match). Current-page-level see isCurrentEnvironmentExcluded.
357
+ * entry (exact match). Current-page-level - see isCurrentEnvironmentExcluded.
312
358
  * @returns {boolean} True if the current hostname is excluded
313
359
  */
314
360
  private isHostExcluded;
315
361
  /**
316
362
  * Whether the current pathname matches a configured `tracking.excludePaths`
317
- * entry (exact match). Current-page-level see isCurrentEnvironmentExcluded.
363
+ * entry (exact match). Current-page-level - see isCurrentEnvironmentExcluded.
318
364
  * @returns {boolean} True if the current pathname is excluded
319
365
  */
320
366
  private isPathExcluded;
321
367
  /**
322
- * Whether the current call is in a visitor-level suppression state opt-out
323
- * or excluded timezone for which any persisted identity cookie should be
368
+ * Whether the current call is in a visitor-level suppression state - opt-out
369
+ * or excluded timezone - for which any persisted identity cookie should be
324
370
  * actively purged (not merely skipped). Host/path exclusions are
325
371
  * deliberately excluded here: they are transient current-page states, so a
326
372
  * cookie legitimately written on an allowed page must survive a visit to an
@@ -331,7 +377,7 @@ export declare class FormoAnalytics implements IFormoAnalytics {
331
377
  /**
332
378
  * Whether the visitor's browser-resolved timezone matches a configured
333
379
  * `tracking.excludeTimezones` entry (case-insensitive). Client-side and
334
- * best-effort see TrackingOptions.excludeTimezones.
380
+ * best-effort - see TrackingOptions.excludeTimezones.
335
381
  * @returns {boolean} True if the current timezone is excluded
336
382
  */
337
383
  private isTimezoneExcluded;
@@ -445,7 +491,7 @@ export declare class FormoAnalytics implements IFormoAnalytics {
445
491
  * WITHOUT emitting an event.
446
492
  *
447
493
  * Integrations (e.g. the wagmi handler) must call this on every
448
- * connect / chain-change / disconnect even when the corresponding
494
+ * connect / chain-change / disconnect - even when the corresponding
449
495
  * autocapture event is disabled. Otherwise `currentChainId` stays
450
496
  * stale/undefined and `shouldTrack()`'s `tracking.excludeChains`
451
497
  * check (which keys off `currentChainId`, not the event payload) can
@@ -467,7 +513,7 @@ export declare class FormoAnalytics implements IFormoAnalytics {
467
513
  /**
468
514
  * Persist (or clear) the current wallet snapshot in a cookie so that the
469
515
  * SDK can repopulate `currentAddress`/`currentChainId` at init on the next
470
- * page load closing the gap between page-show and wagmi/EIP-1193
516
+ * page load - closing the gap between page-show and wagmi/EIP-1193
471
517
  * reconnection during which track()/page() events would otherwise ship
472
518
  * with an empty address.
473
519
  */