@interfere/react 9.0.2 → 10.0.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 (187) hide show
  1. package/README.md +4 -4
  2. package/dist/api.d.mts +25 -0
  3. package/dist/api.d.mts.map +1 -0
  4. package/dist/api.mjs +68 -0
  5. package/dist/api.mjs.map +1 -0
  6. package/dist/error-boundary.d.mts +11 -4
  7. package/dist/error-boundary.d.mts.map +1 -1
  8. package/dist/error-boundary.mjs +6 -3
  9. package/dist/error-boundary.mjs.map +1 -1
  10. package/dist/internal/browser-context.d.mts +6 -0
  11. package/dist/internal/browser-context.d.mts.map +1 -0
  12. package/dist/internal/browser-context.mjs +59 -0
  13. package/dist/internal/browser-context.mjs.map +1 -0
  14. package/dist/internal/capture-boundary.d.mts +5 -1
  15. package/dist/internal/capture-boundary.d.mts.map +1 -1
  16. package/dist/internal/capture-boundary.mjs +9 -5
  17. package/dist/internal/capture-boundary.mjs.map +1 -1
  18. package/dist/internal/capture.d.mts +16 -5
  19. package/dist/internal/capture.d.mts.map +1 -1
  20. package/dist/internal/capture.mjs +20 -16
  21. package/dist/internal/capture.mjs.map +1 -1
  22. package/dist/internal/config.d.mts +20 -4
  23. package/dist/internal/config.d.mts.map +1 -1
  24. package/dist/internal/config.mjs +12 -12
  25. package/dist/internal/config.mjs.map +1 -1
  26. package/dist/internal/consent.d.mts.map +1 -1
  27. package/dist/internal/consent.mjs +3 -1
  28. package/dist/internal/consent.mjs.map +1 -1
  29. package/dist/internal/console-patch.d.mts +19 -0
  30. package/dist/internal/console-patch.d.mts.map +1 -0
  31. package/dist/internal/console-patch.mjs +62 -0
  32. package/dist/internal/console-patch.mjs.map +1 -0
  33. package/dist/internal/dom/actionable.d.mts +27 -0
  34. package/dist/internal/dom/actionable.d.mts.map +1 -0
  35. package/dist/internal/dom/actionable.mjs +62 -0
  36. package/dist/internal/dom/actionable.mjs.map +1 -0
  37. package/dist/internal/kernel-registry.d.mts +8 -0
  38. package/dist/internal/kernel-registry.d.mts.map +1 -0
  39. package/dist/internal/kernel-registry.mjs +31 -0
  40. package/dist/internal/kernel-registry.mjs.map +1 -0
  41. package/dist/internal/kernel.d.mts +267 -0
  42. package/dist/internal/kernel.d.mts.map +1 -0
  43. package/dist/internal/kernel.mjs +322 -0
  44. package/dist/internal/kernel.mjs.map +1 -0
  45. package/dist/internal/otel/exporter.d.mts +93 -0
  46. package/dist/internal/otel/exporter.d.mts.map +1 -0
  47. package/dist/internal/otel/exporter.mjs +212 -0
  48. package/dist/internal/otel/exporter.mjs.map +1 -0
  49. package/dist/internal/otel/index.d.mts +6 -0
  50. package/dist/internal/otel/index.mjs +6 -0
  51. package/dist/internal/otel/instrumentations.d.mts +42 -0
  52. package/dist/internal/otel/instrumentations.d.mts.map +1 -0
  53. package/dist/internal/otel/instrumentations.mjs +150 -0
  54. package/dist/internal/otel/instrumentations.mjs.map +1 -0
  55. package/dist/internal/otel/page-scope-context-manager.d.mts +32 -0
  56. package/dist/internal/otel/page-scope-context-manager.d.mts.map +1 -0
  57. package/dist/internal/otel/page-scope-context-manager.mjs +36 -0
  58. package/dist/internal/otel/page-scope-context-manager.mjs.map +1 -0
  59. package/dist/internal/otel/propagation.d.mts +21 -0
  60. package/dist/internal/otel/propagation.d.mts.map +1 -0
  61. package/dist/internal/otel/propagation.mjs +40 -0
  62. package/dist/internal/otel/propagation.mjs.map +1 -0
  63. package/dist/internal/otel/provider.d.mts +107 -0
  64. package/dist/internal/otel/provider.d.mts.map +1 -0
  65. package/dist/internal/otel/provider.mjs +151 -0
  66. package/dist/internal/otel/provider.mjs.map +1 -0
  67. package/dist/internal/otel/web-vitals.d.mts +35 -0
  68. package/dist/internal/otel/web-vitals.d.mts.map +1 -0
  69. package/dist/internal/otel/web-vitals.mjs +162 -0
  70. package/dist/internal/otel/web-vitals.mjs.map +1 -0
  71. package/dist/internal/page-lifecycle.d.mts +21 -0
  72. package/dist/internal/page-lifecycle.d.mts.map +1 -0
  73. package/dist/internal/page-lifecycle.mjs +33 -0
  74. package/dist/internal/page-lifecycle.mjs.map +1 -0
  75. package/dist/internal/plugin-runtime.d.mts +0 -2
  76. package/dist/internal/plugin-runtime.d.mts.map +1 -1
  77. package/dist/internal/plugin-runtime.mjs +1 -7
  78. package/dist/internal/plugin-runtime.mjs.map +1 -1
  79. package/dist/internal/react-context.d.mts +45 -0
  80. package/dist/internal/react-context.d.mts.map +1 -0
  81. package/dist/internal/react-context.mjs +34 -0
  82. package/dist/internal/react-context.mjs.map +1 -0
  83. package/dist/internal/sw.d.mts +22 -2
  84. package/dist/internal/sw.d.mts.map +1 -1
  85. package/dist/internal/sw.mjs +30 -3
  86. package/dist/internal/sw.mjs.map +1 -1
  87. package/dist/internal/version.d.mts +3 -1
  88. package/dist/internal/version.d.mts.map +1 -1
  89. package/dist/internal/version.mjs +4 -2
  90. package/dist/internal/version.mjs.map +1 -1
  91. package/dist/internal/wrapper-singleton.d.mts +47 -0
  92. package/dist/internal/wrapper-singleton.d.mts.map +1 -0
  93. package/dist/internal/wrapper-singleton.mjs +73 -0
  94. package/dist/internal/wrapper-singleton.mjs.map +1 -0
  95. package/dist/package.mjs +1 -1
  96. package/dist/plugins/errors.d.mts.map +1 -1
  97. package/dist/plugins/errors.mjs +18 -25
  98. package/dist/plugins/errors.mjs.map +1 -1
  99. package/dist/plugins/lib/loader.d.mts +1 -2
  100. package/dist/plugins/lib/loader.d.mts.map +1 -1
  101. package/dist/plugins/lib/loader.mjs +2 -11
  102. package/dist/plugins/lib/loader.mjs.map +1 -1
  103. package/dist/plugins/lib/types.d.mts +3 -2
  104. package/dist/plugins/lib/types.d.mts.map +1 -1
  105. package/dist/plugins/logs.d.mts +13 -0
  106. package/dist/plugins/logs.d.mts.map +1 -0
  107. package/dist/plugins/logs.mjs +53 -0
  108. package/dist/plugins/logs.mjs.map +1 -0
  109. package/dist/plugins/rage-clicks.d.mts.map +1 -1
  110. package/dist/plugins/rage-clicks.mjs +12 -10
  111. package/dist/plugins/rage-clicks.mjs.map +1 -1
  112. package/dist/plugins/replay.d.mts.map +1 -1
  113. package/dist/plugins/replay.mjs +58 -19
  114. package/dist/plugins/replay.mjs.map +1 -1
  115. package/dist/provider.d.mts +11 -20
  116. package/dist/provider.d.mts.map +1 -1
  117. package/dist/provider.mjs +13 -14
  118. package/dist/provider.mjs.map +1 -1
  119. package/dist/react-error-handler.d.mts +21 -5
  120. package/dist/react-error-handler.d.mts.map +1 -1
  121. package/dist/react-error-handler.mjs +15 -7
  122. package/dist/react-error-handler.mjs.map +1 -1
  123. package/dist/sw.d.mts +2 -0
  124. package/dist/sw.mjs +2 -0
  125. package/dist/tracking/api.d.mts +41 -15
  126. package/dist/tracking/api.d.mts.map +1 -1
  127. package/dist/tracking/api.mjs +122 -104
  128. package/dist/tracking/api.mjs.map +1 -1
  129. package/dist/tracking/device.d.mts +30 -7
  130. package/dist/tracking/device.d.mts.map +1 -1
  131. package/dist/tracking/device.mjs +70 -46
  132. package/dist/tracking/device.mjs.map +1 -1
  133. package/dist/tracking/geo.d.mts +11 -3
  134. package/dist/tracking/geo.d.mts.map +1 -1
  135. package/dist/tracking/geo.mjs +33 -29
  136. package/dist/tracking/geo.mjs.map +1 -1
  137. package/dist/tracking/session.d.mts +3 -1
  138. package/dist/tracking/session.d.mts.map +1 -1
  139. package/dist/tracking/session.mjs.map +1 -1
  140. package/dist/util/bot.d.mts +10 -0
  141. package/dist/util/bot.d.mts.map +1 -0
  142. package/dist/util/bot.mjs +14 -0
  143. package/dist/util/bot.mjs.map +1 -0
  144. package/dist/util/global.d.mts +10 -0
  145. package/dist/util/global.d.mts.map +1 -0
  146. package/dist/util/global.mjs +12 -0
  147. package/dist/util/global.mjs.map +1 -0
  148. package/dist/util/log.d.mts.map +1 -1
  149. package/dist/util/log.mjs +8 -1
  150. package/dist/util/log.mjs.map +1 -1
  151. package/dist/util/stringify.d.mts +9 -0
  152. package/dist/util/stringify.d.mts.map +1 -0
  153. package/dist/util/stringify.mjs +16 -0
  154. package/dist/util/stringify.mjs.map +1 -0
  155. package/package.json +73 -20
  156. package/dist/internal/client.d.mts +0 -48
  157. package/dist/internal/client.d.mts.map +0 -1
  158. package/dist/internal/client.mjs +0 -146
  159. package/dist/internal/client.mjs.map +0 -1
  160. package/dist/internal/context.d.mts +0 -6
  161. package/dist/internal/context.d.mts.map +0 -1
  162. package/dist/internal/context.mjs +0 -32
  163. package/dist/internal/context.mjs.map +0 -1
  164. package/dist/internal/envelope.d.mts +0 -15
  165. package/dist/internal/envelope.d.mts.map +0 -1
  166. package/dist/internal/envelope.mjs +0 -24
  167. package/dist/internal/envelope.mjs.map +0 -1
  168. package/dist/internal/errors.d.mts +0 -4
  169. package/dist/internal/errors.d.mts.map +0 -1
  170. package/dist/internal/errors.mjs +0 -4
  171. package/dist/internal/errors.mjs.map +0 -1
  172. package/dist/plugins/device.d.mts +0 -6
  173. package/dist/plugins/device.d.mts.map +0 -1
  174. package/dist/plugins/device.mjs +0 -13
  175. package/dist/plugins/device.mjs.map +0 -1
  176. package/dist/plugins/pages.d.mts +0 -6
  177. package/dist/plugins/pages.d.mts.map +0 -1
  178. package/dist/plugins/pages.mjs +0 -102
  179. package/dist/plugins/pages.mjs.map +0 -1
  180. package/dist/transport/http.d.mts +0 -25
  181. package/dist/transport/http.d.mts.map +0 -1
  182. package/dist/transport/http.mjs +0 -80
  183. package/dist/transport/http.mjs.map +0 -1
  184. package/dist/transport/queue.d.mts +0 -34
  185. package/dist/transport/queue.d.mts.map +0 -1
  186. package/dist/transport/queue.mjs +0 -100
  187. package/dist/transport/queue.mjs.map +0 -1
@@ -1,21 +1,47 @@
1
- import { IngestTarget } from "../transport/http.mjs";
1
+ import { IngestTarget } from "../internal/config.mjs";
2
+ import { DeviceManager } from "./device.mjs";
3
+ import { GeoDetector } from "./geo.mjs";
4
+ import { SessionId } from "@interfere/types/data/session";
2
5
  import { IdentifyParams } from "@interfere/types/sdk/identify";
3
6
 
4
7
  //#region src/tracking/api.d.ts
5
- declare function bootstrap(sessionTarget: IngestTarget): void;
6
- declare const device: {
8
+ interface SessionTrackerOptions {
9
+ device?: DeviceManager;
10
+ fetcher?: typeof globalThis.fetch;
11
+ geo?: GeoDetector;
12
+ target: IngestTarget;
13
+ }
14
+ declare class SessionTracker {
15
+ private readonly target;
16
+ private readonly fetcher;
17
+ private readonly device;
18
+ private readonly geo;
19
+ private mgr;
20
+ private currentIdentity;
21
+ private identifiedSessionId;
22
+ private syncedSessionId;
23
+ private syncAttemptMs;
24
+ private generation;
25
+ constructor(opts: SessionTrackerOptions);
26
+ start(): void;
27
+ sessionId(): SessionId | null;
28
+ windowId(): string | null;
7
29
  getDeviceId(): string | null;
8
30
  getFpHash(): string | null;
9
- };
10
- declare const session: {
11
- getId(): string | null;
12
- getWindowId(): string | null;
13
- };
14
- declare const identity: {
15
- get(): IdentifyParams | null;
16
- set(params: IdentifyParams): Promise<void>;
17
- clear(): void;
18
- };
19
- declare function teardown(): void;
31
+ /**
32
+ * Identity headers (session / window / device). Layered onto the
33
+ * session/identify POSTs alongside the target's static headers
34
+ * (content-type + auth + force-enable).
35
+ */
36
+ headers(): Record<string, string>;
37
+ getIdentity(): IdentifyParams | null;
38
+ identify(params: IdentifyParams): Promise<void>;
39
+ clearIdentity(): void;
40
+ dispose(): void;
41
+ private requestHeaders;
42
+ private ensureSynced;
43
+ private syncSession;
44
+ private onRotate;
45
+ }
20
46
  //#endregion
21
- export { bootstrap, device, identity, session, teardown };
47
+ export { SessionTracker, SessionTrackerOptions };
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.mts","names":[],"sources":["../../src/tracking/api.ts"],"mappings":";;;;iBAyFgB,SAAA,CAAU,aAAA,EAAe,YAAA;AAAA,cAY5B,MAAA;EAQZ,WAAA;EAAA,SAAA;AAAA;AAAA,cAEY,OAAA;EAYZ,KAAA;EAAA,WAAA;AAAA;AAAA,cAEY,QAAA;SACJ,cAAA;cAIW,cAAA,GAAiB,OAAA;;;iBAgDrB,QAAA,CAAA"}
1
+ {"version":3,"file":"api.d.mts","names":[],"sources":["../../src/tracking/api.ts"],"mappings":";;;;;;;UAaiB,qBAAA;EACf,MAAA,GAAS,aAAA;EACT,OAAA,UAAiB,UAAA,CAAW,KAAA;EAC5B,GAAA,GAAM,WAAA;EACN,MAAA,EAAQ,YAAA;AAAA;AAAA,cAGG,cAAA;EAAA,iBACM,MAAA;EAAA,iBACA,OAAA;EAAA,iBACA,MAAA;EAAA,iBACA,GAAA;EAAA,QAET,GAAA;EAAA,QACA,eAAA;EAAA,QACA,mBAAA;EAAA,QACA,eAAA;EAAA,QACA,aAAA;EAAA,QACA,UAAA;cAEI,IAAA,EAAM,qBAAA;EAOlB,KAAA,CAAA;EAaA,SAAA,CAAA,GAAa,SAAA;EAQb,QAAA,CAAA;EAIA,WAAA,CAAA;EAIA,SAAA,CAAA;EAjDyB;;;;;EA0DzB,OAAA,CAAA,GAAW,MAAA;EAiBX,WAAA,CAAA,GAAe,cAAA;EAIT,QAAA,CAAS,MAAA,EAAQ,cAAA,GAAiB,OAAA;EAiDxC,aAAA,CAAA;EAKA,OAAA,CAAA;EAAA,QAQQ,cAAA;EAAA,QAOA,YAAA;EAAA,QAcA,WAAA;EAAA,QA0BM,QAAA;AAAA"}
@@ -1,107 +1,86 @@
1
1
  import { createLogger } from "../util/log.mjs";
2
- import { getDeviceId, getFpHash, initDevice, whenDeviceReady, whenFingerprintReady } from "./device.mjs";
3
- import { buildHeaders } from "../transport/http.mjs";
4
- import { detectCountryCode, resetGeo } from "./geo.mjs";
2
+ import { DeviceManager } from "./device.mjs";
3
+ import { GeoDetector } from "./geo.mjs";
5
4
  import { SessionManager } from "./session.mjs";
6
5
  //#region src/tracking/api.ts
7
6
  const log = createLogger("tracking");
8
- let mgr = null;
9
- let target = null;
10
- let currentIdentity = null;
11
- let identifiedSessionId = null;
12
- let syncedSessionId = null;
13
- let syncAttemptMs = 0;
14
- let generation = 0;
15
7
  const SYNC_COOLDOWN_MS = 5e3;
16
- function syncSession(sessionId, deviceId, fpHash) {
17
- if (!target) return;
18
- syncAttemptMs = Date.now();
19
- syncedSessionId = sessionId;
20
- fetch(target.url, {
21
- method: "POST",
22
- headers: buildHeaders(target.headers),
23
- body: JSON.stringify({
24
- sessionId,
25
- deviceId,
26
- fpHash
27
- }),
28
- keepalive: true,
29
- signal: AbortSignal.timeout(1e4)
30
- }).then((res) => {
31
- if (!res.ok) syncedSessionId = null;
32
- }).catch(() => {
33
- syncedSessionId = null;
34
- log.warn("session sync failed, will retry");
35
- });
36
- }
37
- function ensureSynced(sessionId) {
38
- if (syncedSessionId === sessionId) return;
39
- if (Date.now() - syncAttemptMs < SYNC_COOLDOWN_MS) return;
40
- syncSession(sessionId, getDeviceId(), getFpHash());
41
- }
42
- async function onRotate(sessionId) {
8
+ var SessionTracker = class {
9
+ target;
10
+ fetcher;
11
+ device;
12
+ geo;
13
+ mgr = null;
14
+ currentIdentity = null;
15
+ identifiedSessionId = null;
43
16
  syncedSessionId = null;
44
- syncAttemptMs = Date.now();
45
- if (!target) return;
46
- const gen = generation;
47
- const [deviceId, fpHash] = await Promise.all([whenDeviceReady(), whenFingerprintReady()]);
48
- if (gen !== generation) return;
49
- log.debug("POST session %s (device=%s fp=%s)", sessionId, deviceId ?? "pending", fpHash ?? "none");
50
- syncSession(sessionId, deviceId, fpHash);
51
- }
52
- function bootstrap(sessionTarget) {
53
- target = sessionTarget;
54
- initDevice();
55
- detectCountryCode();
56
- mgr = new SessionManager((id) => {
57
- onRotate(id).catch(() => {});
58
- });
59
- }
60
- const device = {
17
+ syncAttemptMs = 0;
18
+ generation = 0;
19
+ constructor(opts) {
20
+ this.target = opts.target;
21
+ this.fetcher = opts.fetcher ?? globalThis.fetch.bind(globalThis);
22
+ this.device = opts.device ?? new DeviceManager();
23
+ this.geo = opts.geo ?? new GeoDetector();
24
+ }
25
+ start() {
26
+ this.device.init();
27
+ this.geo.detect();
28
+ this.mgr = new SessionManager((id) => {
29
+ this.onRotate(id).catch(() => {});
30
+ });
31
+ }
32
+ sessionId() {
33
+ const id = this.mgr?.getSessionId() ?? null;
34
+ if (id) this.ensureSynced(id);
35
+ return id;
36
+ }
37
+ windowId() {
38
+ return this.mgr?.getWindowId() ?? null;
39
+ }
61
40
  getDeviceId() {
62
- return getDeviceId();
63
- },
41
+ return this.device.getDeviceId();
42
+ }
64
43
  getFpHash() {
65
- return getFpHash();
44
+ return this.device.getFpHash();
66
45
  }
67
- };
68
- const session = {
69
- getId() {
70
- const id = mgr?.getSessionId() ?? null;
71
- if (id) ensureSynced(id);
72
- return id;
73
- },
74
- getWindowId() {
75
- return mgr?.getWindowId() ?? null;
46
+ /**
47
+ * Identity headers (session / window / device). Layered onto the
48
+ * session/identify POSTs alongside the target's static headers
49
+ * (content-type + auth + force-enable).
50
+ */
51
+ headers() {
52
+ const h = {};
53
+ const sid = this.mgr?.getSessionId() ?? null;
54
+ if (sid) h["x-interfere-session"] = sid;
55
+ const wid = this.mgr?.getWindowId() ?? null;
56
+ if (wid) h["x-interfere-window"] = wid;
57
+ const did = this.device.getDeviceId();
58
+ if (did) h["x-interfere-device"] = did;
59
+ return h;
76
60
  }
77
- };
78
- const identity = {
79
- get() {
80
- return currentIdentity;
81
- },
82
- async set(params) {
83
- if (!(mgr && target)) return;
84
- const sessionId = mgr.getSessionId();
85
- if (identifiedSessionId === sessionId) {
61
+ getIdentity() {
62
+ return this.currentIdentity;
63
+ }
64
+ async identify(params) {
65
+ if (!this.mgr) return;
66
+ const sessionId = this.mgr.getSessionId();
67
+ if (this.identifiedSessionId === sessionId) {
86
68
  log.debug("skipped, already identified for session %s", sessionId);
87
69
  return;
88
70
  }
89
- currentIdentity = params;
90
- identifiedSessionId = sessionId;
91
- const gen = generation;
92
- const [deviceId, fpHash, country] = await Promise.all([
93
- whenDeviceReady(),
94
- whenFingerprintReady(),
95
- detectCountryCode()
96
- ]);
97
- if (gen !== generation || !target) {
98
- identifiedSessionId = null;
71
+ this.currentIdentity = params;
72
+ this.identifiedSessionId = sessionId;
73
+ const gen = this.generation;
74
+ const [fpHash, country] = await Promise.all([this.device.whenFingerprintReady(), this.geo.detect()]);
75
+ const deviceId = this.device.getDeviceId();
76
+ if (gen !== this.generation) {
77
+ this.identifiedSessionId = null;
99
78
  return;
100
79
  }
101
- log.info("PUT session %s → user %s", sessionId, params.identifier);
102
- fetch(target.url, {
103
- method: "PUT",
104
- headers: buildHeaders(target.headers),
80
+ log.info("POST session %s → user %s", sessionId, params.identifier);
81
+ this.fetcher(`${this.target.url}/identify`, {
82
+ method: "POST",
83
+ headers: this.requestHeaders(),
105
84
  body: JSON.stringify({
106
85
  sessionId,
107
86
  deviceId,
@@ -112,23 +91,62 @@ const identity = {
112
91
  keepalive: true,
113
92
  signal: AbortSignal.timeout(1e4)
114
93
  }).catch(() => {
115
- identifiedSessionId = null;
94
+ this.identifiedSessionId = null;
116
95
  log.warn("identify failed for session %s", sessionId);
117
96
  });
118
- },
119
- clear() {
120
- currentIdentity = null;
121
- identifiedSessionId = null;
97
+ }
98
+ clearIdentity() {
99
+ this.currentIdentity = null;
100
+ this.identifiedSessionId = null;
101
+ }
102
+ dispose() {
103
+ this.generation += 1;
104
+ this.clearIdentity();
105
+ this.syncedSessionId = null;
106
+ this.syncAttemptMs = 0;
107
+ this.mgr = null;
108
+ }
109
+ requestHeaders() {
110
+ return {
111
+ ...Object.fromEntries(this.target.headers.entries()),
112
+ ...this.headers()
113
+ };
114
+ }
115
+ ensureSynced(sessionId) {
116
+ if (this.syncedSessionId === sessionId) return;
117
+ if (Date.now() - this.syncAttemptMs < SYNC_COOLDOWN_MS) return;
118
+ this.syncSession(sessionId, this.device.getDeviceId(), this.device.getFpHash());
119
+ }
120
+ syncSession(sessionId, deviceId, fpHash) {
121
+ this.syncAttemptMs = Date.now();
122
+ this.syncedSessionId = sessionId;
123
+ this.fetcher(this.target.url, {
124
+ method: "POST",
125
+ headers: this.requestHeaders(),
126
+ body: JSON.stringify({
127
+ sessionId,
128
+ deviceId,
129
+ fpHash
130
+ }),
131
+ keepalive: true,
132
+ signal: AbortSignal.timeout(1e4)
133
+ }).then((res) => {
134
+ if (!res.ok) this.syncedSessionId = null;
135
+ }).catch(() => {
136
+ this.syncedSessionId = null;
137
+ log.warn("session sync failed, will retry");
138
+ });
139
+ }
140
+ async onRotate(sessionId) {
141
+ this.syncedSessionId = null;
142
+ this.syncAttemptMs = Date.now();
143
+ const gen = this.generation;
144
+ const fpHash = await this.device.whenFingerprintReady();
145
+ const deviceId = this.device.getDeviceId();
146
+ if (gen !== this.generation) return;
147
+ log.debug("POST session %s (device=%s fp=%s)", sessionId, deviceId ?? "pending", fpHash ?? "none");
148
+ this.syncSession(sessionId, deviceId, fpHash);
122
149
  }
123
150
  };
124
- function teardown() {
125
- generation += 1;
126
- identity.clear();
127
- resetGeo();
128
- syncedSessionId = null;
129
- syncAttemptMs = 0;
130
- mgr = null;
131
- target = null;
132
- }
133
151
  //#endregion
134
- export { bootstrap, device, identity, session, teardown };
152
+ export { SessionTracker };
@@ -1 +1 @@
1
- {"version":3,"file":"api.mjs","names":[],"sources":["../../src/tracking/api.ts"],"sourcesContent":["import type { IdentifyParams } from \"@interfere/types/sdk/identify\";\n\nimport { buildHeaders, type IngestTarget } from \"../transport/http.js\";\nimport { createLogger } from \"../util/log.js\";\nimport {\n getDeviceId,\n getFpHash,\n initDevice,\n whenDeviceReady,\n whenFingerprintReady,\n} from \"./device.js\";\nimport { detectCountryCode, resetGeo } from \"./geo.js\";\nimport { SessionManager } from \"./session.js\";\n\nconst log = createLogger(\"tracking\");\n\nlet mgr: SessionManager | null = null;\nlet target: IngestTarget | null = null;\nlet currentIdentity: IdentifyParams | null = null;\nlet identifiedSessionId: string | null = null;\n\nlet syncedSessionId: string | null = null;\nlet syncAttemptMs = 0;\nlet generation = 0;\nconst SYNC_COOLDOWN_MS = 5000;\n\nfunction syncSession(\n sessionId: string,\n deviceId: string | null,\n fpHash: string | null\n): void {\n if (!target) {\n return;\n }\n\n syncAttemptMs = Date.now();\n syncedSessionId = sessionId;\n\n fetch(target.url, {\n method: \"POST\",\n headers: buildHeaders(target.headers),\n body: JSON.stringify({ sessionId, deviceId, fpHash }),\n keepalive: true,\n signal: AbortSignal.timeout(10_000),\n })\n .then((res) => {\n if (!res.ok) {\n syncedSessionId = null;\n }\n })\n .catch(() => {\n syncedSessionId = null;\n log.warn(\"session sync failed, will retry\");\n });\n}\n\nfunction ensureSynced(sessionId: string): void {\n if (syncedSessionId === sessionId) {\n return;\n }\n if (Date.now() - syncAttemptMs < SYNC_COOLDOWN_MS) {\n return;\n }\n syncSession(sessionId, getDeviceId(), getFpHash());\n}\n\nasync function onRotate(sessionId: string): Promise<void> {\n syncedSessionId = null;\n syncAttemptMs = Date.now();\n if (!target) {\n return;\n }\n const gen = generation;\n const [deviceId, fpHash] = await Promise.all([\n whenDeviceReady(),\n whenFingerprintReady(),\n ]);\n if (gen !== generation) {\n return;\n }\n log.debug(\n \"POST session %s (device=%s fp=%s)\",\n sessionId,\n deviceId ?? \"pending\",\n fpHash ?? \"none\"\n );\n syncSession(sessionId, deviceId, fpHash);\n}\n\nexport function bootstrap(sessionTarget: IngestTarget): void {\n target = sessionTarget;\n initDevice();\n detectCountryCode();\n\n mgr = new SessionManager((id) => {\n onRotate(id).catch(() => {\n /* best-effort */\n });\n });\n}\n\nexport const device = {\n getDeviceId(): string | null {\n return getDeviceId();\n },\n\n getFpHash(): string | null {\n return getFpHash();\n },\n};\n\nexport const session = {\n getId(): string | null {\n const id = mgr?.getSessionId() ?? null;\n if (id) {\n ensureSynced(id);\n }\n return id;\n },\n\n getWindowId(): string | null {\n return mgr?.getWindowId() ?? null;\n },\n};\n\nexport const identity = {\n get(): IdentifyParams | null {\n return currentIdentity;\n },\n\n async set(params: IdentifyParams): Promise<void> {\n if (!(mgr && target)) {\n return;\n }\n const sessionId = mgr.getSessionId();\n if (identifiedSessionId === sessionId) {\n log.debug(\"skipped, already identified for session %s\", sessionId);\n return;\n }\n\n currentIdentity = params;\n identifiedSessionId = sessionId;\n\n const gen = generation;\n const [deviceId, fpHash, country] = await Promise.all([\n whenDeviceReady(),\n whenFingerprintReady(),\n detectCountryCode(),\n ]);\n if (gen !== generation || !target) {\n identifiedSessionId = null;\n return;\n }\n log.info(\"PUT session %s → user %s\", sessionId, params.identifier);\n fetch(target.url, {\n method: \"PUT\",\n headers: buildHeaders(target.headers),\n body: JSON.stringify({\n sessionId,\n deviceId,\n fpHash,\n ...params,\n ...(country && { country }),\n }),\n keepalive: true,\n signal: AbortSignal.timeout(10_000),\n }).catch(() => {\n identifiedSessionId = null;\n log.warn(\"identify failed for session %s\", sessionId);\n });\n },\n\n clear(): void {\n currentIdentity = null;\n identifiedSessionId = null;\n },\n};\n\nexport function teardown(): void {\n generation += 1;\n identity.clear();\n resetGeo();\n syncedSessionId = null;\n syncAttemptMs = 0;\n mgr = null;\n target = null;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"api.mjs","names":[],"sources":["../../src/tracking/api.ts"],"sourcesContent":["import type { SessionId } from \"@interfere/types/data/session\";\nimport type { IdentifyParams } from \"@interfere/types/sdk/identify\";\n\nimport type { IngestTarget } from \"../internal/config.js\";\nimport { createLogger } from \"../util/log.js\";\nimport { DeviceManager } from \"./device.js\";\nimport { GeoDetector } from \"./geo.js\";\nimport { SessionManager } from \"./session.js\";\n\nconst log = createLogger(\"tracking\");\n\nconst SYNC_COOLDOWN_MS = 5000;\n\nexport interface SessionTrackerOptions {\n device?: DeviceManager;\n fetcher?: typeof globalThis.fetch;\n geo?: GeoDetector;\n target: IngestTarget;\n}\n\nexport class SessionTracker {\n private readonly target: IngestTarget;\n private readonly fetcher: typeof globalThis.fetch;\n private readonly device: DeviceManager;\n private readonly geo: GeoDetector;\n\n private mgr: SessionManager | null = null;\n private currentIdentity: IdentifyParams | null = null;\n private identifiedSessionId: string | null = null;\n private syncedSessionId: string | null = null;\n private syncAttemptMs = 0;\n private generation = 0;\n\n constructor(opts: SessionTrackerOptions) {\n this.target = opts.target;\n this.fetcher = opts.fetcher ?? globalThis.fetch.bind(globalThis);\n this.device = opts.device ?? new DeviceManager();\n this.geo = opts.geo ?? new GeoDetector();\n }\n\n start(): void {\n this.device.init();\n // Best-effort warm-up; `GeoDetector.detect()` swallows its own errors and\n // resolves to `null`, so we don't need to attach a `.catch`. If `identify`\n // runs before the geo result lands, the country field is just omitted.\n this.geo.detect();\n this.mgr = new SessionManager((id) => {\n this.onRotate(id).catch(() => {\n /* best-effort */\n });\n });\n }\n\n sessionId(): SessionId | null {\n const id = this.mgr?.getSessionId() ?? null;\n if (id) {\n this.ensureSynced(id);\n }\n return id;\n }\n\n windowId(): string | null {\n return this.mgr?.getWindowId() ?? null;\n }\n\n getDeviceId(): string | null {\n return this.device.getDeviceId();\n }\n\n getFpHash(): string | null {\n return this.device.getFpHash();\n }\n\n /**\n * Identity headers (session / window / device). Layered onto the\n * session/identify POSTs alongside the target's static headers\n * (content-type + auth + force-enable).\n */\n headers(): Record<string, string> {\n const h: Record<string, string> = {};\n const sid = this.mgr?.getSessionId() ?? null;\n if (sid) {\n h[\"x-interfere-session\"] = sid;\n }\n const wid = this.mgr?.getWindowId() ?? null;\n if (wid) {\n h[\"x-interfere-window\"] = wid;\n }\n const did = this.device.getDeviceId();\n if (did) {\n h[\"x-interfere-device\"] = did;\n }\n return h;\n }\n\n getIdentity(): IdentifyParams | null {\n return this.currentIdentity;\n }\n\n async identify(params: IdentifyParams): Promise<void> {\n if (!this.mgr) {\n return;\n }\n const sessionId = this.mgr.getSessionId();\n if (this.identifiedSessionId === sessionId) {\n log.debug(\"skipped, already identified for session %s\", sessionId);\n return;\n }\n\n this.currentIdentity = params;\n this.identifiedSessionId = sessionId;\n\n const gen = this.generation;\n const [fpHash, country] = await Promise.all([\n this.device.whenFingerprintReady(),\n this.geo.detect(),\n ]);\n const deviceId = this.device.getDeviceId();\n if (gen !== this.generation) {\n this.identifiedSessionId = null;\n return;\n }\n // Append the path to the existing target. `new URL(\"/identify\", base)`\n // is wrong on both axes: in proxy mode `target.url` is relative\n // (`/api/interfere/v1/session`) so the URL constructor throws\n // synchronously — `currentIdentity` is set but no fetch fires; in\n // pub-token mode the path-absolute \"/identify\" replaces the target's\n // path, hitting `/identify` at the origin instead of the collector's\n // `/v1/session/identify` route. Plain string concatenation gets both.\n log.info(\"POST session %s → user %s\", sessionId, params.identifier);\n this.fetcher(`${this.target.url}/identify`, {\n method: \"POST\",\n headers: this.requestHeaders(),\n body: JSON.stringify({\n sessionId,\n deviceId,\n fpHash,\n ...params,\n ...(country && { country }),\n }),\n keepalive: true,\n signal: AbortSignal.timeout(10_000),\n }).catch(() => {\n this.identifiedSessionId = null;\n log.warn(\"identify failed for session %s\", sessionId);\n });\n }\n\n clearIdentity(): void {\n this.currentIdentity = null;\n this.identifiedSessionId = null;\n }\n\n dispose(): void {\n this.generation += 1;\n this.clearIdentity();\n this.syncedSessionId = null;\n this.syncAttemptMs = 0;\n this.mgr = null;\n }\n\n private requestHeaders(): Record<string, string> {\n return {\n ...Object.fromEntries(this.target.headers.entries()),\n ...this.headers(),\n };\n }\n\n private ensureSynced(sessionId: string): void {\n if (this.syncedSessionId === sessionId) {\n return;\n }\n if (Date.now() - this.syncAttemptMs < SYNC_COOLDOWN_MS) {\n return;\n }\n this.syncSession(\n sessionId,\n this.device.getDeviceId(),\n this.device.getFpHash()\n );\n }\n\n private syncSession(\n sessionId: string,\n deviceId: string | null,\n fpHash: string | null\n ): void {\n this.syncAttemptMs = Date.now();\n this.syncedSessionId = sessionId;\n\n this.fetcher(this.target.url, {\n method: \"POST\",\n headers: this.requestHeaders(),\n body: JSON.stringify({ sessionId, deviceId, fpHash }),\n keepalive: true,\n signal: AbortSignal.timeout(10_000),\n })\n .then((res) => {\n if (!res.ok) {\n this.syncedSessionId = null;\n }\n })\n .catch(() => {\n this.syncedSessionId = null;\n log.warn(\"session sync failed, will retry\");\n });\n }\n\n private async onRotate(sessionId: string): Promise<void> {\n this.syncedSessionId = null;\n this.syncAttemptMs = Date.now();\n const gen = this.generation;\n const fpHash = await this.device.whenFingerprintReady();\n const deviceId = this.device.getDeviceId();\n if (gen !== this.generation) {\n return;\n }\n log.debug(\n \"POST session %s (device=%s fp=%s)\",\n sessionId,\n deviceId ?? \"pending\",\n fpHash ?? \"none\"\n );\n this.syncSession(sessionId, deviceId, fpHash);\n }\n}\n"],"mappings":"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"}
@@ -1,9 +1,32 @@
1
1
  //#region src/tracking/device.d.ts
2
- declare function initDevice(): void;
3
- declare function getDeviceId(): string | null;
4
- declare function getFpHash(): string | null;
5
- declare function whenDeviceReady(): Promise<string | null>;
6
- declare function whenFingerprintReady(): Promise<string | null>;
7
- declare function resetDevice(): void;
2
+ /**
3
+ * Producer of an opaque, stable per-browser fingerprint hash. The default
4
+ * dynamic-imports the FingerprintJS OSS library and self-defers via
5
+ * `requestIdleCallback` so the ~1.5s of synchronous font / canvas / WebGL
6
+ * probing the library performs on a cold load doesn't land on the
7
+ * hydration tick. Tests inject a fixed-value provider that resolves
8
+ * immediately — they pay nothing for the production deferral.
9
+ */
10
+ type FingerprintProvider = () => Promise<string | null>;
11
+ /**
12
+ * Resolves on the next browser idle frame, capped at 5s. Falls back to
13
+ * `setTimeout(0)` (next macrotask) on hosts without
14
+ * `requestIdleCallback`. Exported only for the unit test that pins the
15
+ * "default provider gates fingerprint work via RIC" contract — not part
16
+ * of the package's public surface.
17
+ */
18
+ declare function whenIdle(): Promise<void>;
19
+ declare class DeviceManager {
20
+ private deviceId;
21
+ private fpHash;
22
+ private fpPending;
23
+ private readonly fingerprintProvider;
24
+ constructor(fingerprintProvider?: FingerprintProvider);
25
+ init(): void;
26
+ private startFingerprint;
27
+ getDeviceId(): string | null;
28
+ getFpHash(): string | null;
29
+ whenFingerprintReady(): Promise<string | null>;
30
+ }
8
31
  //#endregion
9
- export { getDeviceId, getFpHash, initDevice, resetDevice, whenDeviceReady, whenFingerprintReady };
32
+ export { DeviceManager, FingerprintProvider, whenIdle };
@@ -1 +1 @@
1
- {"version":3,"file":"device.d.mts","names":[],"sources":["../../src/tracking/device.ts"],"mappings":";iBA+CgB,UAAA,CAAA;AAAA,iBAiDA,WAAA,CAAA;AAAA,iBAIA,SAAA,CAAA;AAAA,iBAIA,eAAA,CAAA,GAAmB,OAAA;AAAA,iBAOnB,oBAAA,CAAA,GAAwB,OAAA;AAAA,iBAOxB,WAAA,CAAA"}
1
+ {"version":3,"file":"device.d.mts","names":[],"sources":["../../src/tracking/device.ts"],"mappings":";;AAgBA;;;;;AAWA;;KAXY,mBAAA,SAA4B,OAAA;;;AAkFxC;;;;;iBAvEgB,QAAA,CAAA,GAAY,OAAA;AAAA,cAuEf,aAAA;EAAA,QACH,QAAA;EAAA,QACA,MAAA;EAAA,QACA,SAAA;EAAA,iBACS,mBAAA;cAEL,mBAAA,GAAsB,mBAAA;EAKlC,IAAA,CAAA;EAAA,QAkCQ,gBAAA;EAkBR,WAAA,CAAA;EAIA,SAAA,CAAA;EAIA,oBAAA,CAAA,GAAwB,OAAA;AAAA"}
@@ -4,9 +4,37 @@ const log = createLogger("device");
4
4
  const LS_KEY = "interfere:device_id";
5
5
  const COOKIE_NAME = "interfere_did";
6
6
  const COOKIE_MAX_AGE_DAYS = 400;
7
- let deviceId = null;
8
- let fpHash = null;
9
- let fpPending = null;
7
+ const FP_IDLE_TIMEOUT_MS = 5e3;
8
+ /**
9
+ * Resolves on the next browser idle frame, capped at 5s. Falls back to
10
+ * `setTimeout(0)` (next macrotask) on hosts without
11
+ * `requestIdleCallback`. Exported only for the unit test that pins the
12
+ * "default provider gates fingerprint work via RIC" contract — not part
13
+ * of the package's public surface.
14
+ */
15
+ function whenIdle() {
16
+ return new Promise((resolve) => {
17
+ if (typeof globalThis.requestIdleCallback === "function") {
18
+ globalThis.requestIdleCallback(() => resolve(), { timeout: FP_IDLE_TIMEOUT_MS });
19
+ return;
20
+ }
21
+ setTimeout(resolve, 0);
22
+ });
23
+ }
24
+ const defaultFingerprintProvider = async () => {
25
+ await whenIdle();
26
+ try {
27
+ const result = await (await (await import("@fingerprintjs/fingerprintjs")).load()).get();
28
+ if (!result.visitorId) {
29
+ log.error("Fingerprinting returned an empty visitor id; visitor identity will be cookie-only");
30
+ return null;
31
+ }
32
+ return result.visitorId;
33
+ } catch (err) {
34
+ log.error("fingerprint init failed; visitor identity will be cookie-only", err);
35
+ return null;
36
+ }
37
+ };
10
38
  function tryLocalStorage() {
11
39
  try {
12
40
  const s = globalThis.localStorage;
@@ -31,50 +59,46 @@ function setCookie(name, value) {
31
59
  function generateId() {
32
60
  return crypto.randomUUID();
33
61
  }
34
- function initDevice() {
35
- if (deviceId) return;
36
- const ls = tryLocalStorage();
37
- const fromLs = ls?.getItem(LS_KEY) ?? null;
38
- const fromCookie = getCookie(COOKIE_NAME);
39
- deviceId = fromLs ?? fromCookie ?? generateId();
40
- if (!fromLs && ls) ls.setItem(LS_KEY, deviceId);
41
- if (!fromCookie) setCookie(COOKIE_NAME, deviceId);
42
- if (fromLs && !fromCookie) setCookie(COOKIE_NAME, deviceId);
43
- if (fromCookie && !fromLs && ls) ls.setItem(LS_KEY, deviceId);
44
- log.debug("device %s (ls=%s cookie=%s)", deviceId, !!fromLs, !!fromCookie);
45
- initFpHash();
46
- }
47
- function initFpHash() {
48
- if (fpHash || fpPending) return;
49
- fpPending = (async () => {
50
- try {
51
- fpHash = (await (await (await import("@fingerprintjs/fingerprintjs")).load()).get()).visitorId;
52
- log.debug("fpHash %s", fpHash);
53
- return fpHash;
54
- } catch {
55
- log.warn("fp hash failed");
56
- return null;
57
- }
58
- })();
59
- }
60
- function getDeviceId() {
61
- return deviceId;
62
- }
63
- function getFpHash() {
64
- return fpHash;
65
- }
66
- function whenDeviceReady() {
67
- if (deviceId) return Promise.resolve(deviceId);
68
- return Promise.resolve(null);
69
- }
70
- function whenFingerprintReady() {
71
- if (fpHash) return Promise.resolve(fpHash);
72
- return fpPending ?? Promise.resolve(null);
73
- }
74
- function resetDevice() {
62
+ var DeviceManager = class {
75
63
  deviceId = null;
76
64
  fpHash = null;
77
65
  fpPending = null;
78
- }
66
+ fingerprintProvider;
67
+ constructor(fingerprintProvider) {
68
+ this.fingerprintProvider = fingerprintProvider ?? defaultFingerprintProvider;
69
+ }
70
+ init() {
71
+ if (this.deviceId) return;
72
+ const ls = tryLocalStorage();
73
+ const fromLs = ls?.getItem(LS_KEY) ?? null;
74
+ const fromCookie = getCookie(COOKIE_NAME);
75
+ this.deviceId = fromLs ?? fromCookie ?? generateId();
76
+ if (!fromLs && ls) ls.setItem(LS_KEY, this.deviceId);
77
+ if (!fromCookie) setCookie(COOKIE_NAME, this.deviceId);
78
+ if (fromLs && !fromCookie) setCookie(COOKIE_NAME, this.deviceId);
79
+ if (fromCookie && !fromLs && ls) ls.setItem(LS_KEY, this.deviceId);
80
+ log.debug("device %s (ls=%s cookie=%s)", this.deviceId, !!fromLs, !!fromCookie);
81
+ this.startFingerprint();
82
+ }
83
+ startFingerprint() {
84
+ if (this.fpHash || this.fpPending) return;
85
+ this.fpPending = (async () => {
86
+ const hash = await this.fingerprintProvider();
87
+ this.fpHash = hash;
88
+ if (hash) log.debug("fpHash %s", hash);
89
+ return hash;
90
+ })();
91
+ }
92
+ getDeviceId() {
93
+ return this.deviceId;
94
+ }
95
+ getFpHash() {
96
+ return this.fpHash;
97
+ }
98
+ whenFingerprintReady() {
99
+ if (this.fpHash) return Promise.resolve(this.fpHash);
100
+ return this.fpPending ?? Promise.resolve(null);
101
+ }
102
+ };
79
103
  //#endregion
80
- export { getDeviceId, getFpHash, initDevice, resetDevice, whenDeviceReady, whenFingerprintReady };
104
+ export { DeviceManager, whenIdle };
@@ -1 +1 @@
1
- {"version":3,"file":"device.mjs","names":[],"sources":["../../src/tracking/device.ts"],"sourcesContent":["import { createLogger } from \"../util/log.js\";\n\nconst log = createLogger(\"device\");\n\nconst LS_KEY = \"interfere:device_id\";\nconst COOKIE_NAME = \"interfere_did\";\nconst COOKIE_MAX_AGE_DAYS = 400;\n\nlet deviceId: string | null = null;\nlet fpHash: string | null = null;\nlet fpPending: Promise<string | null> | null = null;\n\nfunction tryLocalStorage(): Storage | null {\n try {\n const s = globalThis.localStorage;\n const key = \"__interfere_device_probe__\";\n s.setItem(key, \"1\");\n s.removeItem(key);\n return s;\n } catch {\n return null;\n }\n}\n\nfunction getCookie(name: string): string | null {\n if (typeof document === \"undefined\") {\n return null;\n }\n const match = document.cookie\n .split(\"; \")\n .find((c) => c.startsWith(`${name}=`));\n return match ? decodeURIComponent(match.split(\"=\")[1] ?? \"\") : null;\n}\n\nfunction setCookie(name: string, value: string): void {\n if (typeof document === \"undefined\") {\n return;\n }\n const maxAge = COOKIE_MAX_AGE_DAYS * 24 * 60 * 60;\n // biome-ignore lint/suspicious/noDocumentCookie: Cookie Store API is async and not universally supported; synchronous access is required here\n document.cookie = `${name}=${encodeURIComponent(value)};path=/;max-age=${maxAge};SameSite=Lax`;\n}\n\nfunction generateId(): string {\n return crypto.randomUUID();\n}\n\nexport function initDevice(): void {\n if (deviceId) {\n return;\n }\n\n const ls = tryLocalStorage();\n const fromLs = ls?.getItem(LS_KEY) ?? null;\n const fromCookie = getCookie(COOKIE_NAME);\n\n deviceId = fromLs ?? fromCookie ?? generateId();\n\n if (!fromLs && ls) {\n ls.setItem(LS_KEY, deviceId);\n }\n if (!fromCookie) {\n setCookie(COOKIE_NAME, deviceId);\n }\n if (fromLs && !fromCookie) {\n setCookie(COOKIE_NAME, deviceId);\n }\n if (fromCookie && !fromLs && ls) {\n ls.setItem(LS_KEY, deviceId);\n }\n\n log.debug(\"device %s (ls=%s cookie=%s)\", deviceId, !!fromLs, !!fromCookie);\n\n initFpHash();\n}\n\nfunction initFpHash(): void {\n if (fpHash || fpPending) {\n return;\n }\n\n fpPending = (async () => {\n try {\n const FingerprintJS = await import(\"@fingerprintjs/fingerprintjs\");\n const fp = await FingerprintJS.load();\n const result = await fp.get();\n fpHash = result.visitorId;\n log.debug(\"fpHash %s\", fpHash);\n return fpHash;\n } catch {\n log.warn(\"fp hash failed\");\n return null;\n }\n })();\n}\n\nexport function getDeviceId(): string | null {\n return deviceId;\n}\n\nexport function getFpHash(): string | null {\n return fpHash;\n}\n\nexport function whenDeviceReady(): Promise<string | null> {\n if (deviceId) {\n return Promise.resolve(deviceId);\n }\n return Promise.resolve(null);\n}\n\nexport function whenFingerprintReady(): Promise<string | null> {\n if (fpHash) {\n return Promise.resolve(fpHash);\n }\n return fpPending ?? Promise.resolve(null);\n}\n\nexport function resetDevice(): void {\n deviceId = null;\n fpHash = null;\n fpPending = null;\n}\n"],"mappings":";;AAEA,MAAM,MAAM,aAAa,SAAS;AAElC,MAAM,SAAS;AACf,MAAM,cAAc;AACpB,MAAM,sBAAsB;AAE5B,IAAI,WAA0B;AAC9B,IAAI,SAAwB;AAC5B,IAAI,YAA2C;AAE/C,SAAS,kBAAkC;AACzC,KAAI;EACF,MAAM,IAAI,WAAW;EACrB,MAAM,MAAM;AACZ,IAAE,QAAQ,KAAK,IAAI;AACnB,IAAE,WAAW,IAAI;AACjB,SAAO;SACD;AACN,SAAO;;;AAIX,SAAS,UAAU,MAA6B;AAC9C,KAAI,OAAO,aAAa,YACtB,QAAO;CAET,MAAM,QAAQ,SAAS,OACpB,MAAM,KAAK,CACX,MAAM,MAAM,EAAE,WAAW,GAAG,KAAK,GAAG,CAAC;AACxC,QAAO,QAAQ,mBAAmB,MAAM,MAAM,IAAI,CAAC,MAAM,GAAG,GAAG;;AAGjE,SAAS,UAAU,MAAc,OAAqB;AACpD,KAAI,OAAO,aAAa,YACtB;CAEF,MAAM,SAAS,sBAAsB,KAAK,KAAK;AAE/C,UAAS,SAAS,GAAG,KAAK,GAAG,mBAAmB,MAAM,CAAC,kBAAkB,OAAO;;AAGlF,SAAS,aAAqB;AAC5B,QAAO,OAAO,YAAY;;AAG5B,SAAgB,aAAmB;AACjC,KAAI,SACF;CAGF,MAAM,KAAK,iBAAiB;CAC5B,MAAM,SAAS,IAAI,QAAQ,OAAO,IAAI;CACtC,MAAM,aAAa,UAAU,YAAY;AAEzC,YAAW,UAAU,cAAc,YAAY;AAE/C,KAAI,CAAC,UAAU,GACb,IAAG,QAAQ,QAAQ,SAAS;AAE9B,KAAI,CAAC,WACH,WAAU,aAAa,SAAS;AAElC,KAAI,UAAU,CAAC,WACb,WAAU,aAAa,SAAS;AAElC,KAAI,cAAc,CAAC,UAAU,GAC3B,IAAG,QAAQ,QAAQ,SAAS;AAG9B,KAAI,MAAM,+BAA+B,UAAU,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;AAE1E,aAAY;;AAGd,SAAS,aAAmB;AAC1B,KAAI,UAAU,UACZ;AAGF,cAAa,YAAY;AACvB,MAAI;AAIF,aAAS,OADY,OADJ,MADW,OAAO,iCACJ,MAAM,EACb,KAAK,EACb;AAChB,OAAI,MAAM,aAAa,OAAO;AAC9B,UAAO;UACD;AACN,OAAI,KAAK,iBAAiB;AAC1B,UAAO;;KAEP;;AAGN,SAAgB,cAA6B;AAC3C,QAAO;;AAGT,SAAgB,YAA2B;AACzC,QAAO;;AAGT,SAAgB,kBAA0C;AACxD,KAAI,SACF,QAAO,QAAQ,QAAQ,SAAS;AAElC,QAAO,QAAQ,QAAQ,KAAK;;AAG9B,SAAgB,uBAA+C;AAC7D,KAAI,OACF,QAAO,QAAQ,QAAQ,OAAO;AAEhC,QAAO,aAAa,QAAQ,QAAQ,KAAK;;AAG3C,SAAgB,cAAoB;AAClC,YAAW;AACX,UAAS;AACT,aAAY"}
1
+ {"version":3,"file":"device.mjs","names":[],"sources":["../../src/tracking/device.ts"],"sourcesContent":["import { createLogger } from \"../util/log.js\";\n\nconst log = createLogger(\"device\");\n\nconst LS_KEY = \"interfere:device_id\";\nconst COOKIE_NAME = \"interfere_did\";\nconst COOKIE_MAX_AGE_DAYS = 400;\n\n/**\n * Producer of an opaque, stable per-browser fingerprint hash. The default\n * dynamic-imports the FingerprintJS OSS library and self-defers via\n * `requestIdleCallback` so the ~1.5s of synchronous font / canvas / WebGL\n * probing the library performs on a cold load doesn't land on the\n * hydration tick. Tests inject a fixed-value provider that resolves\n * immediately — they pay nothing for the production deferral.\n */\nexport type FingerprintProvider = () => Promise<string | null>;\n\nconst FP_IDLE_TIMEOUT_MS = 5000;\n\n/**\n * Resolves on the next browser idle frame, capped at 5s. Falls back to\n * `setTimeout(0)` (next macrotask) on hosts without\n * `requestIdleCallback`. Exported only for the unit test that pins the\n * \"default provider gates fingerprint work via RIC\" contract — not part\n * of the package's public surface.\n */\nexport function whenIdle(): Promise<void> {\n return new Promise((resolve) => {\n if (typeof globalThis.requestIdleCallback === \"function\") {\n globalThis.requestIdleCallback(() => resolve(), {\n timeout: FP_IDLE_TIMEOUT_MS,\n });\n return;\n }\n setTimeout(resolve, 0);\n });\n}\n\nconst defaultFingerprintProvider: FingerprintProvider = async () => {\n await whenIdle();\n try {\n const FingerprintJS = await import(\"@fingerprintjs/fingerprintjs\");\n const fp = await FingerprintJS.load();\n const result = await fp.get();\n if (!result.visitorId) {\n log.error(\n \"Fingerprinting returned an empty visitor id; visitor identity will be cookie-only\"\n );\n\n return null;\n }\n return result.visitorId;\n } catch (err) {\n log.error(\n \"fingerprint init failed; visitor identity will be cookie-only\",\n err\n );\n\n return null;\n }\n};\n\nfunction tryLocalStorage(): Storage | null {\n try {\n const s = globalThis.localStorage;\n const key = \"__interfere_device_probe__\";\n s.setItem(key, \"1\");\n s.removeItem(key);\n return s;\n } catch {\n return null;\n }\n}\n\nfunction getCookie(name: string): string | null {\n if (typeof document === \"undefined\") {\n return null;\n }\n const match = document.cookie\n .split(\"; \")\n .find((c) => c.startsWith(`${name}=`));\n return match ? decodeURIComponent(match.split(\"=\")[1] ?? \"\") : null;\n}\n\nfunction setCookie(name: string, value: string): void {\n if (typeof document === \"undefined\") {\n return;\n }\n const maxAge = COOKIE_MAX_AGE_DAYS * 24 * 60 * 60;\n // biome-ignore lint/suspicious/noDocumentCookie: Cookie Store API is async and not universally supported; synchronous access is required here\n document.cookie = `${name}=${encodeURIComponent(value)};path=/;max-age=${maxAge};SameSite=Lax`;\n}\n\nfunction generateId(): string {\n return crypto.randomUUID();\n}\n\nexport class DeviceManager {\n private deviceId: string | null = null;\n private fpHash: string | null = null;\n private fpPending: Promise<string | null> | null = null;\n private readonly fingerprintProvider: FingerprintProvider;\n\n constructor(fingerprintProvider?: FingerprintProvider) {\n this.fingerprintProvider =\n fingerprintProvider ?? defaultFingerprintProvider;\n }\n\n init(): void {\n if (this.deviceId) {\n return;\n }\n\n const ls = tryLocalStorage();\n const fromLs = ls?.getItem(LS_KEY) ?? null;\n const fromCookie = getCookie(COOKIE_NAME);\n\n this.deviceId = fromLs ?? fromCookie ?? generateId();\n\n if (!fromLs && ls) {\n ls.setItem(LS_KEY, this.deviceId);\n }\n if (!fromCookie) {\n setCookie(COOKIE_NAME, this.deviceId);\n }\n if (fromLs && !fromCookie) {\n setCookie(COOKIE_NAME, this.deviceId);\n }\n if (fromCookie && !fromLs && ls) {\n ls.setItem(LS_KEY, this.deviceId);\n }\n\n log.debug(\n \"device %s (ls=%s cookie=%s)\",\n this.deviceId,\n !!fromLs,\n !!fromCookie\n );\n\n this.startFingerprint();\n }\n\n private startFingerprint(): void {\n if (this.fpHash || this.fpPending) {\n return;\n }\n\n this.fpPending = (async () => {\n const hash = await this.fingerprintProvider();\n this.fpHash = hash;\n if (hash) {\n log.debug(\"fpHash %s\", hash);\n }\n // No log on null — the provider is responsible for surfacing its own\n // failure mode (the default provider logs at error level; custom\n // providers can choose). Logging here too would double-count.\n return hash;\n })();\n }\n\n getDeviceId(): string | null {\n return this.deviceId;\n }\n\n getFpHash(): string | null {\n return this.fpHash;\n }\n\n whenFingerprintReady(): Promise<string | null> {\n if (this.fpHash) {\n return Promise.resolve(this.fpHash);\n }\n return this.fpPending ?? Promise.resolve(null);\n }\n}\n"],"mappings":";;AAEA,MAAM,MAAM,aAAa,SAAS;AAElC,MAAM,SAAS;AACf,MAAM,cAAc;AACpB,MAAM,sBAAsB;AAY5B,MAAM,qBAAqB;;;;;;;;AAS3B,SAAgB,WAA0B;CACxC,OAAO,IAAI,SAAS,YAAY;EAC9B,IAAI,OAAO,WAAW,wBAAwB,YAAY;GACxD,WAAW,0BAA0B,SAAS,EAAE,EAC9C,SAAS,oBACV,CAAC;GACF;;EAEF,WAAW,SAAS,EAAE;GACtB;;AAGJ,MAAM,6BAAkD,YAAY;CAClE,MAAM,UAAU;CAChB,IAAI;EAGF,MAAM,SAAS,OAAM,OADJ,MADW,OAAO,iCACJ,MAAM,EACb,KAAK;EAC7B,IAAI,CAAC,OAAO,WAAW;GACrB,IAAI,MACF,oFACD;GAED,OAAO;;EAET,OAAO,OAAO;UACP,KAAK;EACZ,IAAI,MACF,iEACA,IACD;EAED,OAAO;;;AAIX,SAAS,kBAAkC;CACzC,IAAI;EACF,MAAM,IAAI,WAAW;EACrB,MAAM,MAAM;EACZ,EAAE,QAAQ,KAAK,IAAI;EACnB,EAAE,WAAW,IAAI;EACjB,OAAO;SACD;EACN,OAAO;;;AAIX,SAAS,UAAU,MAA6B;CAC9C,IAAI,OAAO,aAAa,aACtB,OAAO;CAET,MAAM,QAAQ,SAAS,OACpB,MAAM,KAAK,CACX,MAAM,MAAM,EAAE,WAAW,GAAG,KAAK,GAAG,CAAC;CACxC,OAAO,QAAQ,mBAAmB,MAAM,MAAM,IAAI,CAAC,MAAM,GAAG,GAAG;;AAGjE,SAAS,UAAU,MAAc,OAAqB;CACpD,IAAI,OAAO,aAAa,aACtB;CAEF,MAAM,SAAS,sBAAsB,KAAK,KAAK;CAE/C,SAAS,SAAS,GAAG,KAAK,GAAG,mBAAmB,MAAM,CAAC,kBAAkB,OAAO;;AAGlF,SAAS,aAAqB;CAC5B,OAAO,OAAO,YAAY;;AAG5B,IAAa,gBAAb,MAA2B;CACzB,WAAkC;CAClC,SAAgC;CAChC,YAAmD;CACnD;CAEA,YAAY,qBAA2C;EACrD,KAAK,sBACH,uBAAuB;;CAG3B,OAAa;EACX,IAAI,KAAK,UACP;EAGF,MAAM,KAAK,iBAAiB;EAC5B,MAAM,SAAS,IAAI,QAAQ,OAAO,IAAI;EACtC,MAAM,aAAa,UAAU,YAAY;EAEzC,KAAK,WAAW,UAAU,cAAc,YAAY;EAEpD,IAAI,CAAC,UAAU,IACb,GAAG,QAAQ,QAAQ,KAAK,SAAS;EAEnC,IAAI,CAAC,YACH,UAAU,aAAa,KAAK,SAAS;EAEvC,IAAI,UAAU,CAAC,YACb,UAAU,aAAa,KAAK,SAAS;EAEvC,IAAI,cAAc,CAAC,UAAU,IAC3B,GAAG,QAAQ,QAAQ,KAAK,SAAS;EAGnC,IAAI,MACF,+BACA,KAAK,UACL,CAAC,CAAC,QACF,CAAC,CAAC,WACH;EAED,KAAK,kBAAkB;;CAGzB,mBAAiC;EAC/B,IAAI,KAAK,UAAU,KAAK,WACtB;EAGF,KAAK,aAAa,YAAY;GAC5B,MAAM,OAAO,MAAM,KAAK,qBAAqB;GAC7C,KAAK,SAAS;GACd,IAAI,MACF,IAAI,MAAM,aAAa,KAAK;GAK9B,OAAO;MACL;;CAGN,cAA6B;EAC3B,OAAO,KAAK;;CAGd,YAA2B;EACzB,OAAO,KAAK;;CAGd,uBAA+C;EAC7C,IAAI,KAAK,QACP,OAAO,QAAQ,QAAQ,KAAK,OAAO;EAErC,OAAO,KAAK,aAAa,QAAQ,QAAQ,KAAK"}
@@ -1,5 +1,13 @@
1
1
  //#region src/tracking/geo.d.ts
2
- declare function detectCountryCode(): Promise<string | null>;
3
- declare function resetGeo(): void;
2
+ declare class GeoDetector {
3
+ private cached;
4
+ private pending;
5
+ private failed;
6
+ private readonly fetcher;
7
+ constructor(fetcher?: typeof globalThis.fetch);
8
+ detect(): Promise<string | null>;
9
+ getCountry(): string | null;
10
+ private fetchCountryCode;
11
+ }
4
12
  //#endregion
5
- export { detectCountryCode, resetGeo };
13
+ export { GeoDetector };
@@ -1 +1 @@
1
- {"version":3,"file":"geo.d.mts","names":[],"sources":["../../src/tracking/geo.ts"],"mappings":";iBAwCgB,iBAAA,CAAA,GAAqB,OAAA;AAAA,iBAuBrB,QAAA,CAAA"}
1
+ {"version":3,"file":"geo.d.mts","names":[],"sources":["../../src/tracking/geo.ts"],"mappings":";cAea,WAAA;EAAA,QACH,MAAA;EAAA,QACA,OAAA;EAAA,QACA,MAAA;EAAA,iBACS,OAAA;cAEL,OAAA,UAAiB,UAAA,CAAW,KAAA;EAIxC,MAAA,CAAA,GAAU,OAAA;EAuBV,UAAA,CAAA;EAAA,QAIc,gBAAA;AAAA"}