@vectoral-labs/browser 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,458 @@
1
+ // src/env.ts
2
+ var safe = (fn, fallback) => {
3
+ try {
4
+ return fn();
5
+ } catch {
6
+ return fallback;
7
+ }
8
+ };
9
+ var nav = () => globalThis.navigator ?? {};
10
+ var win = () => globalThis;
11
+ var doc = () => globalThis.document;
12
+ var hasDom = () => typeof globalThis === "object" && doc() !== void 0;
13
+ var screenInfo = () => safe(
14
+ () => {
15
+ const s = globalThis.screen;
16
+ return {
17
+ width: s?.width ?? 0,
18
+ height: s?.height ?? 0,
19
+ colorDepth: s?.colorDepth ?? 0
20
+ };
21
+ },
22
+ { width: 0, height: 0, colorDepth: 0 }
23
+ );
24
+ var timezone = () => safe(() => Intl.DateTimeFormat().resolvedOptions().timeZone ?? null, null);
25
+ var webglInfo = () => safe(() => {
26
+ const d = doc();
27
+ if (!d) return null;
28
+ const c = d.createElement("canvas");
29
+ const gl = c.getContext("webgl") ?? c.getContext("experimental-webgl");
30
+ if (!gl) return null;
31
+ const dbg = gl.getExtension("WEBGL_debug_renderer_info");
32
+ if (!dbg) return { vendor: null, renderer: null };
33
+ return {
34
+ vendor: String(gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL) ?? ""),
35
+ renderer: String(gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) ?? "")
36
+ };
37
+ }, null);
38
+ var canvasSample = () => safe(() => {
39
+ const d = doc();
40
+ if (!d) return null;
41
+ const c = d.createElement("canvas");
42
+ c.width = 240;
43
+ c.height = 60;
44
+ const ctx = c.getContext("2d");
45
+ if (!ctx) return null;
46
+ ctx.textBaseline = "top";
47
+ ctx.font = "14px 'Arial'";
48
+ ctx.fillStyle = "#f60";
49
+ ctx.fillRect(125, 1, 62, 20);
50
+ ctx.fillStyle = "#069";
51
+ ctx.fillText("Vectoral \u26A1 sensor", 2, 15);
52
+ ctx.fillStyle = "rgba(102,204,0,0.7)";
53
+ ctx.fillText("Vectoral \u26A1 sensor", 4, 17);
54
+ return c.toDataURL();
55
+ }, null);
56
+
57
+ // src/device.ts
58
+ var FINGERPRINT_VERSION = 1;
59
+ function collectComponents() {
60
+ const n = safe(nav, {});
61
+ const s = screenInfo();
62
+ const gl = webglInfo();
63
+ const canvas = canvasSample();
64
+ const probe = (fn) => safe(fn, null) ?? null;
65
+ return {
66
+ platform: probe(() => n.userAgentData?.platform ?? n.platform),
67
+ // Brand NAMES only — versions are dropped so a browser update does not
68
+ // change the fingerprint.
69
+ brands: probe(
70
+ () => n.userAgentData?.brands?.map((b) => b.brand).filter((b) => !/not.a.brand/i.test(b)).sort().join(",")
71
+ ),
72
+ mobile: probe(
73
+ () => n.userAgentData?.mobile === void 0 ? null : String(n.userAgentData.mobile)
74
+ ),
75
+ language: probe(() => n.language),
76
+ languages: probe(() => n.languages?.join(",")),
77
+ timezone: timezone(),
78
+ screen: probe(() => s.width > 0 ? `${s.width}x${s.height}x${s.colorDepth}` : null),
79
+ concurrency: probe(
80
+ () => n.hardwareConcurrency ? String(n.hardwareConcurrency) : null
81
+ ),
82
+ memory: probe(() => n.deviceMemory ? String(n.deviceMemory) : null),
83
+ touch: probe(
84
+ () => n.maxTouchPoints === void 0 ? null : String(n.maxTouchPoints)
85
+ ),
86
+ webglVendor: gl?.vendor ?? null,
87
+ webglRenderer: gl?.renderer ?? null,
88
+ canvas
89
+ };
90
+ }
91
+ async function deviceFingerprint(opts) {
92
+ if (!opts?.siteKey) {
93
+ throw new Error("deviceFingerprint: `siteKey` is required (it is the salt)");
94
+ }
95
+ if (!hasDom()) {
96
+ throw new Error("deviceFingerprint: no DOM \u2014 this runs in the browser only");
97
+ }
98
+ const components = collectComponents();
99
+ const entries = Object.entries(components);
100
+ const present = entries.filter(([, v]) => v !== null && v !== "").length;
101
+ const material = [
102
+ `v${FINGERPRINT_VERSION}`,
103
+ opts.siteKey,
104
+ ...entries.map(([k, v]) => `${k}=${v ?? ""}`)
105
+ ].join("|");
106
+ const { digest, strong } = await hashHex(material);
107
+ return {
108
+ fingerprint: `fp_${digest.slice(0, 32)}`,
109
+ strong,
110
+ coverage: { present, total: entries.length },
111
+ components
112
+ };
113
+ }
114
+ async function hashHex(input) {
115
+ const subtle = safe(
116
+ () => globalThis.crypto?.subtle,
117
+ void 0
118
+ );
119
+ if (subtle) {
120
+ try {
121
+ const bytes = new TextEncoder().encode(input);
122
+ const buf = await subtle.digest("SHA-256", bytes);
123
+ const hex = Array.from(
124
+ new Uint8Array(buf),
125
+ (b) => b.toString(16).padStart(2, "0")
126
+ ).join("");
127
+ return { digest: hex, strong: true };
128
+ } catch {
129
+ }
130
+ }
131
+ return { digest: fnv1a128(input), strong: false };
132
+ }
133
+ function fnv1a128(input) {
134
+ const PRIME = 16777619;
135
+ const bases = [2166136261, 16777619, 2654435769, 2246822507];
136
+ return bases.map((base) => {
137
+ let h = base >>> 0;
138
+ for (let i = 0; i < input.length; i++) {
139
+ h ^= input.charCodeAt(i) & 255;
140
+ h = Math.imul(h, PRIME) >>> 0;
141
+ h ^= input.charCodeAt(i) >>> 8;
142
+ h = Math.imul(h, PRIME) >>> 0;
143
+ }
144
+ return h.toString(16).padStart(8, "0");
145
+ }).join("");
146
+ }
147
+
148
+ // src/automation.ts
149
+ var AUTOMATION_GLOBALS = [
150
+ "__playwright",
151
+ "__puppeteer",
152
+ "__pw_manual",
153
+ "_phantom",
154
+ "__nightmare",
155
+ "callPhantom",
156
+ "domAutomation",
157
+ "domAutomationController",
158
+ "_Selenium_IDE_Recorder",
159
+ "__selenium_unwrapped",
160
+ "__webdriver_evaluate",
161
+ "__driver_evaluate"
162
+ ];
163
+ var tells = () => {
164
+ const n = safe(nav, {});
165
+ const w = safe(win, {});
166
+ const ua = safe(() => n.userAgent ?? "", "");
167
+ const s = screenInfo();
168
+ const gl = webglInfo();
169
+ return [
170
+ {
171
+ name: "webdriver_flag",
172
+ weight: 0.85,
173
+ test: () => n.webdriver === true
174
+ },
175
+ {
176
+ name: "automation_globals",
177
+ weight: 0.85,
178
+ test: () => AUTOMATION_GLOBALS.some((k) => k in w)
179
+ },
180
+ {
181
+ name: "chromedriver_cdc",
182
+ weight: 0.85,
183
+ test: () => safe(() => {
184
+ const d = doc();
185
+ if (!d) return false;
186
+ return Object.keys(d).some((k) => k.startsWith("cdc_") || k.startsWith("$cdc_"));
187
+ }, false)
188
+ },
189
+ {
190
+ name: "headless_user_agent",
191
+ weight: 0.8,
192
+ test: () => /headless/i.test(ua)
193
+ },
194
+ {
195
+ name: "empty_languages",
196
+ weight: 0.55,
197
+ inconsistency: true,
198
+ test: () => Array.isArray(n.languages) && n.languages.length === 0
199
+ },
200
+ {
201
+ name: "zero_outer_window",
202
+ weight: 0.5,
203
+ inconsistency: true,
204
+ test: () => safe(() => {
205
+ const outer = w;
206
+ return outer.outerWidth === 0 || outer.outerHeight === 0;
207
+ }, false)
208
+ },
209
+ {
210
+ name: "native_code_patched",
211
+ weight: 0.45,
212
+ inconsistency: true,
213
+ // Stealth plugins replace native functions with JS shims. A genuine
214
+ // built-in stringifies to "[native code]"; a shim does not.
215
+ test: () => safe(() => {
216
+ const fns = [
217
+ n.permissions?.query,
218
+ w.Function?.prototype?.toString
219
+ ];
220
+ return fns.some(
221
+ (f) => typeof f === "function" && !/\[native code\]/.test(String(f))
222
+ );
223
+ }, false)
224
+ },
225
+ {
226
+ name: "webgl_software_renderer",
227
+ weight: 0.45,
228
+ inconsistency: true,
229
+ test: () => /swiftshader|llvmpipe|software|mesa offscreen/i.test(gl?.renderer ?? "")
230
+ },
231
+ {
232
+ name: "chrome_object_missing",
233
+ weight: 0.4,
234
+ inconsistency: true,
235
+ test: () => /chrome/i.test(ua) && !("chrome" in w)
236
+ },
237
+ {
238
+ name: "zero_screen",
239
+ weight: 0.4,
240
+ inconsistency: true,
241
+ test: () => s.width === 0 || s.height === 0
242
+ },
243
+ {
244
+ name: "platform_ua_mismatch",
245
+ weight: 0.35,
246
+ inconsistency: true,
247
+ test: () => {
248
+ const platform = n.userAgentData?.platform ?? n.platform ?? "";
249
+ if (!platform || !ua) return false;
250
+ const claimed = /windows/i.test(ua) ? "win" : /mac os|macintosh/i.test(ua) ? "mac" : /linux|android/i.test(ua) ? "linux" : null;
251
+ if (!claimed) return false;
252
+ const actual = platform.toLowerCase();
253
+ if (claimed === "win") return !/win/.test(actual);
254
+ if (claimed === "mac") return !/mac/.test(actual);
255
+ return !/linux|android|arm/.test(actual);
256
+ }
257
+ }
258
+ ];
259
+ };
260
+ var noisyOr = (weights) => weights.length === 0 ? 0 : 1 - weights.reduce((acc, w) => acc * (1 - w), 1);
261
+ var round2 = (n) => Math.round(n * 100) / 100;
262
+ function detectAutomation(opts = {}) {
263
+ const threshold = opts.threshold ?? 0.6;
264
+ const evaluated = tells().map((t) => ({ ...t, fired: safe(t.test, false) }));
265
+ const fired = evaluated.filter((t) => t.fired).sort((a, b) => b.weight - a.weight);
266
+ const score = noisyOr(fired.map((t) => t.weight));
267
+ const anomaly = noisyOr(fired.filter((t) => t.inconsistency).map((t) => t.weight));
268
+ const signals = {};
269
+ for (const t of evaluated) signals[t.name] = t.fired;
270
+ return {
271
+ score: round2(score),
272
+ automated: score >= threshold,
273
+ reasons: fired.map((t) => t.name),
274
+ signals,
275
+ fingerprintAnomaly: round2(anomaly)
276
+ };
277
+ }
278
+
279
+ // src/form.ts
280
+ var newField = () => ({
281
+ pasted: false,
282
+ keystrokes: 0,
283
+ corrections: 0,
284
+ focusMs: 0,
285
+ focusedAt: null
286
+ });
287
+ var CORRECTION_KEYS = /* @__PURE__ */ new Set(["Backspace", "Delete", "Undo"]);
288
+ var NON_TYPING_KEYS = /* @__PURE__ */ new Set([
289
+ "Shift",
290
+ "Control",
291
+ "Alt",
292
+ "Meta",
293
+ "CapsLock",
294
+ "Tab",
295
+ "Escape",
296
+ "ArrowUp",
297
+ "ArrowDown",
298
+ "ArrowLeft",
299
+ "ArrowRight",
300
+ "Home",
301
+ "End",
302
+ "PageUp",
303
+ "PageDown"
304
+ ]);
305
+ var FormTelemetry = class {
306
+ fields = /* @__PURE__ */ new Map();
307
+ now;
308
+ allowed;
309
+ constructor(opts = {}) {
310
+ this.now = opts.now ?? (() => {
311
+ const p = globalThis.performance;
312
+ return p ? p.now() : 0;
313
+ });
314
+ this.allowed = opts.fields ? new Set(opts.fields) : null;
315
+ }
316
+ state(field) {
317
+ if (this.allowed && !this.allowed.has(field)) return null;
318
+ let s = this.fields.get(field);
319
+ if (!s) {
320
+ s = newField();
321
+ this.fields.set(field, s);
322
+ }
323
+ return s;
324
+ }
325
+ focus(field) {
326
+ const s = this.state(field);
327
+ if (s && s.focusedAt === null) s.focusedAt = this.now();
328
+ }
329
+ blur(field) {
330
+ const s = this.state(field);
331
+ if (!s || s.focusedAt === null) return;
332
+ s.focusMs += this.now() - s.focusedAt;
333
+ s.focusedAt = null;
334
+ }
335
+ keystroke(field, key) {
336
+ const s = this.state(field);
337
+ if (!s) return;
338
+ if (CORRECTION_KEYS.has(key)) {
339
+ s.corrections += 1;
340
+ return;
341
+ }
342
+ if (NON_TYPING_KEYS.has(key)) return;
343
+ s.keystrokes += 1;
344
+ }
345
+ paste(field) {
346
+ const s = this.state(field);
347
+ if (s) s.pasted = true;
348
+ }
349
+ /**
350
+ * Current telemetry. Call at submit. Fields still focused have their
351
+ * in-progress focus time included without ending the focus, so calling this
352
+ * twice is safe.
353
+ */
354
+ snapshot() {
355
+ const t = this.now();
356
+ const fields = {};
357
+ for (const [name, s] of this.fields) {
358
+ const live = s.focusedAt === null ? 0 : t - s.focusedAt;
359
+ fields[name] = {
360
+ pasted: s.pasted,
361
+ keystrokes: s.keystrokes,
362
+ corrections: s.corrections,
363
+ focus_ms: Math.round(s.focusMs + live)
364
+ };
365
+ }
366
+ return { load_to_submit_ms: Math.round(t), fields };
367
+ }
368
+ };
369
+ var CONTROL_TYPES = /* @__PURE__ */ new Set(["submit", "button", "reset", "image"]);
370
+ function trackForm(form, opts = {}) {
371
+ const telemetry = new FormTelemetry(opts);
372
+ const nameOf = (e) => {
373
+ const t = e.target;
374
+ if (!t) return null;
375
+ if (typeof t.type === "string" && CONTROL_TYPES.has(t.type)) return null;
376
+ return t.name || t.id || t.type || null;
377
+ };
378
+ const onFocus = (e) => {
379
+ const n = nameOf(e);
380
+ if (n) telemetry.focus(n);
381
+ };
382
+ const onBlur = (e) => {
383
+ const n = nameOf(e);
384
+ if (n) telemetry.blur(n);
385
+ };
386
+ const onKeyDown = (e) => {
387
+ const n = nameOf(e);
388
+ if (n) telemetry.keystroke(n, e.key ?? "");
389
+ };
390
+ const onPaste = (e) => {
391
+ const n = nameOf(e);
392
+ if (n) telemetry.paste(n);
393
+ };
394
+ const onInput = (e) => {
395
+ if (e.inputType === "insertFromPaste") {
396
+ const n = nameOf(e);
397
+ if (n) telemetry.paste(n);
398
+ }
399
+ };
400
+ const opt = { capture: true, passive: true };
401
+ const bound = [
402
+ ["focusin", onFocus],
403
+ ["focusout", onBlur],
404
+ ["keydown", onKeyDown],
405
+ ["paste", onPaste],
406
+ ["input", onInput]
407
+ ];
408
+ for (const [type, fn] of bound) form.addEventListener(type, fn, opt);
409
+ let stopped = false;
410
+ return {
411
+ snapshot: () => telemetry.snapshot(),
412
+ stop: () => {
413
+ if (stopped) return;
414
+ stopped = true;
415
+ for (const [type, fn] of bound) form.removeEventListener(type, fn, opt);
416
+ }
417
+ };
418
+ }
419
+
420
+ // src/signals.ts
421
+ async function signupSignals(opts) {
422
+ const device = await deviceFingerprint({ siteKey: opts.siteKey });
423
+ const automation = detectAutomation(
424
+ opts.automationThreshold !== void 0 ? { threshold: opts.automationThreshold } : {}
425
+ );
426
+ const formSnapshot = opts.form?.snapshot();
427
+ const tz = timezone();
428
+ const webdriver = safe(() => nav().webdriver, void 0);
429
+ return {
430
+ device_fingerprint: device.fingerprint,
431
+ client: {
432
+ // A real `false` is a useful negative; omitting the field says nothing.
433
+ // So the field is included ONLY when the browser actually reported the
434
+ // property. The automation tell cannot answer this — it is
435
+ // `navigator.webdriver === true`, which reads `false` for "absent" and
436
+ // for "measured false" alike, so probe the property directly.
437
+ ...typeof webdriver === "boolean" ? { webdriver } : {},
438
+ fingerprint_anomaly: automation.fingerprintAnomaly,
439
+ ...formSnapshot ? { load_to_submit_ms: formSnapshot.load_to_submit_ms } : {},
440
+ ...tz ? { timezone: tz } : {}
441
+ },
442
+ ...formSnapshot ? { form: formSnapshot } : {},
443
+ diagnostics: {
444
+ automation_score: automation.score,
445
+ automation_reasons: automation.reasons,
446
+ fingerprint_strong: device.strong,
447
+ fingerprint_coverage: device.coverage
448
+ }
449
+ };
450
+ }
451
+ export {
452
+ FormTelemetry,
453
+ detectAutomation,
454
+ deviceFingerprint,
455
+ signupSignals,
456
+ trackForm
457
+ };
458
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/env.ts","../src/device.ts","../src/automation.ts","../src/form.ts","../src/signals.ts"],"sourcesContent":["// Environment probing. Every probe is wrapped: a fingerprint that throws in a\n// hardened browser is worse than one with a missing component.\n\nexport const safe = <T>(fn: () => T, fallback: T): T => {\n try {\n return fn();\n } catch {\n return fallback;\n }\n};\n\nexport interface UserAgentData {\n brands?: { brand: string; version: string }[];\n mobile?: boolean;\n platform?: string;\n}\n\ninterface NavigatorLike {\n userAgent?: string;\n userAgentData?: UserAgentData;\n platform?: string;\n language?: string;\n languages?: readonly string[];\n hardwareConcurrency?: number;\n deviceMemory?: number;\n maxTouchPoints?: number;\n webdriver?: boolean;\n plugins?: { length: number };\n cookieEnabled?: boolean;\n permissions?: unknown;\n}\n\nexport const nav = (): NavigatorLike =>\n (globalThis as { navigator?: NavigatorLike }).navigator ?? {};\n\nexport const win = (): Record<string, unknown> =>\n globalThis as unknown as Record<string, unknown>;\n\nexport const doc = (): Document | undefined =>\n (globalThis as { document?: Document }).document;\n\n/** Is there enough of a DOM to collect anything at all? */\nexport const hasDom = (): boolean =>\n typeof globalThis === \"object\" && doc() !== undefined;\n\nexport interface ScreenInfo {\n width: number;\n height: number;\n colorDepth: number;\n}\n\nexport const screenInfo = (): ScreenInfo =>\n safe(\n () => {\n const s = (globalThis as { screen?: Screen }).screen;\n return {\n width: s?.width ?? 0,\n height: s?.height ?? 0,\n colorDepth: s?.colorDepth ?? 0,\n };\n },\n { width: 0, height: 0, colorDepth: 0 },\n );\n\nexport const timezone = (): string | null =>\n safe(() => Intl.DateTimeFormat().resolvedOptions().timeZone ?? null, null);\n\nexport interface WebglInfo {\n vendor: string | null;\n renderer: string | null;\n}\n\nexport const webglInfo = (): WebglInfo | null =>\n safe(() => {\n const d = doc();\n if (!d) return null;\n const c = d.createElement(\"canvas\");\n const gl = (c.getContext(\"webgl\") ??\n c.getContext(\"experimental-webgl\")) as WebGLRenderingContext | null;\n if (!gl) return null;\n const dbg = gl.getExtension(\"WEBGL_debug_renderer_info\");\n if (!dbg) return { vendor: null, renderer: null };\n return {\n vendor: String(gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL) ?? \"\"),\n renderer: String(gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) ?? \"\"),\n };\n }, null);\n\n/**\n * A canvas rendering, as a data URL. The caller hashes it — we return the raw\n * string so the whole fingerprint goes through one salted digest rather than\n * leaking a second, unsalted one.\n */\nexport const canvasSample = (): string | null =>\n safe(() => {\n const d = doc();\n if (!d) return null;\n const c = d.createElement(\"canvas\");\n c.width = 240;\n c.height = 60;\n const ctx = c.getContext(\"2d\");\n if (!ctx) return null;\n ctx.textBaseline = \"top\";\n ctx.font = \"14px 'Arial'\";\n ctx.fillStyle = \"#f60\";\n ctx.fillRect(125, 1, 62, 20);\n ctx.fillStyle = \"#069\";\n ctx.fillText(\"Vectoral ⚡ sensor\", 2, 15);\n ctx.fillStyle = \"rgba(102,204,0,0.7)\";\n ctx.fillText(\"Vectoral ⚡ sensor\", 4, 17);\n return c.toDataURL();\n }, null);\n","// Device fingerprint — a stable, tenant-scoped identifier for this browser.\n//\n// SALT POLICY. This runs in the user's browser, so nothing it holds is secret.\n// We salt with the PUBLISHABLE site key, which buys scoping rather than\n// secrecy: the same device produces different values for different Vectoral\n// customers, so a fingerprint leaked from your logs cannot be joined against\n// another tenant's. Never put a secret salt here — shipping one to the browser\n// only tells you it was a secret. See docs/concepts/salts.md.\n\nimport {\n canvasSample,\n hasDom,\n nav,\n safe,\n screenInfo,\n timezone,\n webglInfo,\n} from \"./env.js\";\n\n/** Bumping this deliberately invalidates every previously issued value. */\nconst FINGERPRINT_VERSION = 1;\n\nexport interface DeviceFingerprintOptions {\n /** Your publishable site key (`pk_live_…`). Scopes the value to your tenant. */\n siteKey: string;\n}\n\nexport interface DeviceFingerprint {\n /** `fp_` + 32 hex chars. Send as `device_fingerprint`. */\n fingerprint: string;\n /**\n * False when SubtleCrypto was unavailable (an insecure context) and a\n * non-cryptographic fallback hash was used. The value is still stable and\n * still comparable — it is just cheaper to reverse, so do not treat it as a\n * privacy boundary.\n */\n strong: boolean;\n /**\n * How many of the components actually resolved, out of the total attempted.\n * A very low count means a hardened or headless browser, which is itself\n * worth forwarding.\n */\n coverage: { present: number; total: number };\n /** The raw components, for debugging. Never send these anywhere. */\n components: Record<string, string | null>;\n}\n\n/**\n * Components chosen for STABILITY as much as entropy. Deliberately excluded:\n *\n * - the full user-agent string and browser version: churns on every update,\n * which would reset the fingerprint of every legitimate user monthly while\n * costing an attacker nothing.\n * - window size and devicePixelRatio: change with zoom and window dragging.\n * - `navigator.plugins` contents: empty on modern browsers.\n */\nfunction collectComponents(): Record<string, string | null> {\n const n = safe(nav, {});\n const s = screenInfo();\n const gl = webglInfo();\n const canvas = canvasSample();\n // Each component is probed independently: a hardened browser that throws on\n // one property should cost us that component, not the whole fingerprint. The\n // KEY SET must stay fixed regardless, or `coverage.total` would move and two\n // browsers would hash different material for the same device.\n const probe = (fn: () => string | null | undefined): string | null =>\n safe(fn, null) ?? null;\n\n return {\n platform: probe(() => n.userAgentData?.platform ?? n.platform),\n // Brand NAMES only — versions are dropped so a browser update does not\n // change the fingerprint.\n brands: probe(() =>\n n.userAgentData?.brands\n ?.map((b) => b.brand)\n .filter((b) => !/not.a.brand/i.test(b))\n .sort()\n .join(\",\"),\n ),\n mobile: probe(() =>\n n.userAgentData?.mobile === undefined ? null : String(n.userAgentData.mobile),\n ),\n language: probe(() => n.language),\n languages: probe(() => n.languages?.join(\",\")),\n timezone: timezone(),\n screen: probe(() => (s.width > 0 ? `${s.width}x${s.height}x${s.colorDepth}` : null)),\n concurrency: probe(() =>\n n.hardwareConcurrency ? String(n.hardwareConcurrency) : null,\n ),\n memory: probe(() => (n.deviceMemory ? String(n.deviceMemory) : null)),\n touch: probe(() =>\n n.maxTouchPoints === undefined ? null : String(n.maxTouchPoints),\n ),\n webglVendor: gl?.vendor ?? null,\n webglRenderer: gl?.renderer ?? null,\n canvas,\n };\n}\n\n/**\n * Compute this browser's device fingerprint.\n *\n * Async because canvas and WebGL probing are, and because SubtleCrypto is.\n * Call it once per page and reuse the result; it does not change within a\n * session.\n *\n * Send the `fingerprint` to YOUR backend, which forwards it as\n * `device_fingerprint`. It is the single highest-value optional field on a\n * registration: one device across many signups is the strongest farm signal\n * that exists, and it is invisible to Vectoral without it.\n */\nexport async function deviceFingerprint(\n opts: DeviceFingerprintOptions,\n): Promise<DeviceFingerprint> {\n if (!opts?.siteKey) {\n throw new Error(\"deviceFingerprint: `siteKey` is required (it is the salt)\");\n }\n if (!hasDom()) {\n throw new Error(\"deviceFingerprint: no DOM — this runs in the browser only\");\n }\n const components = collectComponents();\n const entries = Object.entries(components);\n const present = entries.filter(([, v]) => v !== null && v !== \"\").length;\n\n const material = [\n `v${FINGERPRINT_VERSION}`,\n opts.siteKey,\n ...entries.map(([k, v]) => `${k}=${v ?? \"\"}`),\n ].join(\"|\");\n\n const { digest, strong } = await hashHex(material);\n return {\n fingerprint: `fp_${digest.slice(0, 32)}`,\n strong,\n coverage: { present, total: entries.length },\n components,\n };\n}\n\nasync function hashHex(input: string): Promise<{ digest: string; strong: boolean }> {\n const subtle = safe(\n () => (globalThis as { crypto?: { subtle?: SubtleCrypto } }).crypto?.subtle,\n undefined,\n );\n if (subtle) {\n try {\n const bytes = new TextEncoder().encode(input);\n const buf = await subtle.digest(\"SHA-256\", bytes);\n const hex = Array.from(new Uint8Array(buf), (b) =>\n b.toString(16).padStart(2, \"0\"),\n ).join(\"\");\n return { digest: hex, strong: true };\n } catch {\n // fall through to the non-crypto path\n }\n }\n return { digest: fnv1a128(input), strong: false };\n}\n\n/**\n * Fallback for insecure contexts: four FNV-1a passes with different offset\n * bases, concatenated to 32 hex chars. Not cryptographic — it exists so the\n * fingerprint keeps working on `http://` origins, not to resist analysis.\n */\nfunction fnv1a128(input: string): string {\n const PRIME = 16777619;\n const bases = [0x811c9dc5, 0x01000193, 0x9e3779b9, 0x85ebca6b];\n return bases\n .map((base) => {\n let h = base >>> 0;\n for (let i = 0; i < input.length; i++) {\n h ^= input.charCodeAt(i) & 0xff;\n h = Math.imul(h, PRIME) >>> 0;\n h ^= input.charCodeAt(i) >>> 8;\n h = Math.imul(h, PRIME) >>> 0;\n }\n return h.toString(16).padStart(8, \"0\");\n })\n .join(\"\");\n}\n","// Automation tells — is this browser being driven by a script?\n//\n// TRUST MODEL. This runs on the abuser's machine. Stealth frameworks\n// (puppeteer-extra-stealth, undetected-chromedriver) patch most of these tells,\n// and an attacker can strip the script or fake the posted score outright. Treat\n// the output as a soft signal that raises suspicion — strongest when several\n// independent tells fire together — never as a standalone block. The\n// spoof-resistant signals live server-side.\n\nimport { doc, nav, safe, screenInfo, webglInfo, win } from \"./env.js\";\n\nexport interface AutomationResult {\n /** Noisy-OR combination of the weights that fired, in [0,1]. */\n score: number;\n /** `score >= threshold`. */\n automated: boolean;\n /** Names of the tells that fired, strongest first. */\n reasons: string[];\n /** Every tell that was evaluated, whether it fired or not. */\n signals: Record<string, boolean>;\n /**\n * The environment-inconsistency subscore in [0,1] — the part of the evidence\n * that comes from the browser contradicting itself rather than from an\n * injected global. Send this as `client.fingerprint_anomaly`.\n */\n fingerprintAnomaly: number;\n}\n\nexport interface DetectAutomationOptions {\n /** Decision point for `automated`. Default 0.6. */\n threshold?: number;\n}\n\ninterface Tell {\n name: string;\n weight: number;\n /** True when the tell is an environment contradiction, not an injected hook. */\n inconsistency?: boolean;\n test: () => boolean;\n}\n\nconst AUTOMATION_GLOBALS = [\n \"__playwright\",\n \"__puppeteer\",\n \"__pw_manual\",\n \"_phantom\",\n \"__nightmare\",\n \"callPhantom\",\n \"domAutomation\",\n \"domAutomationController\",\n \"_Selenium_IDE_Recorder\",\n \"__selenium_unwrapped\",\n \"__webdriver_evaluate\",\n \"__driver_evaluate\",\n];\n\nconst tells = (): Tell[] => {\n // Every read here must be guarded: a hardened browser can install a throwing\n // getter on any of these, and a detector that dies on one is worth less than\n // one that reports the tells it could reach.\n const n = safe(nav, {});\n const w = safe(win, {} as Record<string, unknown>);\n const ua = safe(() => n.userAgent ?? \"\", \"\");\n const s = screenInfo();\n const gl = webglInfo();\n\n return [\n {\n name: \"webdriver_flag\",\n weight: 0.85,\n test: () => n.webdriver === true,\n },\n {\n name: \"automation_globals\",\n weight: 0.85,\n test: () => AUTOMATION_GLOBALS.some((k) => k in w),\n },\n {\n name: \"chromedriver_cdc\",\n weight: 0.85,\n test: () =>\n safe(() => {\n const d = doc();\n if (!d) return false;\n return Object.keys(d).some((k) => k.startsWith(\"cdc_\") || k.startsWith(\"$cdc_\"));\n }, false),\n },\n {\n name: \"headless_user_agent\",\n weight: 0.8,\n test: () => /headless/i.test(ua),\n },\n {\n name: \"empty_languages\",\n weight: 0.55,\n inconsistency: true,\n test: () => Array.isArray(n.languages) && n.languages.length === 0,\n },\n {\n name: \"zero_outer_window\",\n weight: 0.5,\n inconsistency: true,\n test: () =>\n safe(() => {\n const outer = w as { outerWidth?: number; outerHeight?: number };\n return outer.outerWidth === 0 || outer.outerHeight === 0;\n }, false),\n },\n {\n name: \"native_code_patched\",\n weight: 0.45,\n inconsistency: true,\n // Stealth plugins replace native functions with JS shims. A genuine\n // built-in stringifies to \"[native code]\"; a shim does not.\n test: () =>\n safe(() => {\n const fns: unknown[] = [\n (n as { permissions?: { query?: unknown } }).permissions?.query,\n (w as { Function?: { prototype?: { toString?: unknown } } }).Function\n ?.prototype?.toString,\n ];\n return fns.some(\n (f) => typeof f === \"function\" && !/\\[native code\\]/.test(String(f)),\n );\n }, false),\n },\n {\n name: \"webgl_software_renderer\",\n weight: 0.45,\n inconsistency: true,\n test: () => /swiftshader|llvmpipe|software|mesa offscreen/i.test(gl?.renderer ?? \"\"),\n },\n {\n name: \"chrome_object_missing\",\n weight: 0.4,\n inconsistency: true,\n test: () => /chrome/i.test(ua) && !(\"chrome\" in w),\n },\n {\n name: \"zero_screen\",\n weight: 0.4,\n inconsistency: true,\n test: () => s.width === 0 || s.height === 0,\n },\n {\n name: \"platform_ua_mismatch\",\n weight: 0.35,\n inconsistency: true,\n test: () => {\n const platform = n.userAgentData?.platform ?? n.platform ?? \"\";\n if (!platform || !ua) return false;\n const claimed = /windows/i.test(ua)\n ? \"win\"\n : /mac os|macintosh/i.test(ua)\n ? \"mac\"\n : /linux|android/i.test(ua)\n ? \"linux\"\n : null;\n if (!claimed) return false;\n const actual = platform.toLowerCase();\n if (claimed === \"win\") return !/win/.test(actual);\n if (claimed === \"mac\") return !/mac/.test(actual);\n return !/linux|android|arm/.test(actual);\n },\n },\n ];\n};\n\n/** `1 - Π(1 - w)`: one strong tell dominates; several weak ones still add up. */\nconst noisyOr = (weights: number[]): number =>\n weights.length === 0 ? 0 : 1 - weights.reduce((acc, w) => acc * (1 - w), 1);\n\nconst round2 = (n: number): number => Math.round(n * 100) / 100;\n\n/**\n * Evaluate every automation tell against the current browser. Synchronous and\n * cheap — safe to call on every protected action.\n */\nexport function detectAutomation(opts: DetectAutomationOptions = {}): AutomationResult {\n const threshold = opts.threshold ?? 0.6;\n const evaluated = tells().map((t) => ({ ...t, fired: safe(t.test, false) }));\n\n const fired = evaluated.filter((t) => t.fired).sort((a, b) => b.weight - a.weight);\n const score = noisyOr(fired.map((t) => t.weight));\n const anomaly = noisyOr(fired.filter((t) => t.inconsistency).map((t) => t.weight));\n\n const signals: Record<string, boolean> = {};\n for (const t of evaluated) signals[t.name] = t.fired;\n\n return {\n score: round2(score),\n automated: score >= threshold,\n reasons: fired.map((t) => t.name),\n signals,\n fingerprintAnomaly: round2(anomaly),\n };\n}\n","// Form-fill telemetry. `pasted` is the valuable bit: scripted fills paste,\n// humans type. `corrections` counts backspaces and edits — humans make them,\n// scripts do not.\n//\n// Omitting the block gates these checks off server-side entirely; it is never\n// read as a clean fill, so a partial integration is not penalised.\n\nexport interface FormFieldTelemetry {\n pasted: boolean;\n keystrokes: number;\n corrections: number;\n focus_ms: number;\n}\n\nexport interface FormTelemetrySnapshot {\n load_to_submit_ms: number;\n fields: Record<string, FormFieldTelemetry>;\n}\n\nexport interface FormTelemetryOptions {\n /** Clock, in ms since page load. Defaults to `performance.now()`. */\n now?: () => number;\n /**\n * Field names to report. When omitted, every field that received an event is\n * reported. Never include a field whose NAME would reveal something you would\n * not log — only names are sent, never values.\n */\n fields?: string[];\n}\n\ninterface FieldState {\n pasted: boolean;\n keystrokes: number;\n corrections: number;\n focusMs: number;\n focusedAt: number | null;\n}\n\nconst newField = (): FieldState => ({\n pasted: false,\n keystrokes: 0,\n corrections: 0,\n focusMs: 0,\n focusedAt: null,\n});\n\n/** Keys that edit rather than add. */\nconst CORRECTION_KEYS = new Set([\"Backspace\", \"Delete\", \"Undo\"]);\n\n/** Keys that produce no character and should not count as a keystroke. */\nconst NON_TYPING_KEYS = new Set([\n \"Shift\",\n \"Control\",\n \"Alt\",\n \"Meta\",\n \"CapsLock\",\n \"Tab\",\n \"Escape\",\n \"ArrowUp\",\n \"ArrowDown\",\n \"ArrowLeft\",\n \"ArrowRight\",\n \"Home\",\n \"End\",\n \"PageUp\",\n \"PageDown\",\n]);\n\n/**\n * Accumulates fill behaviour for one form. Transport-free and DOM-free so it\n * can be driven directly — `trackForm()` is the thin DOM wiring over it.\n */\nexport class FormTelemetry {\n private readonly fields = new Map<string, FieldState>();\n private readonly now: () => number;\n private readonly allowed: Set<string> | null;\n\n constructor(opts: FormTelemetryOptions = {}) {\n this.now =\n opts.now ??\n (() => {\n const p = (globalThis as { performance?: { now(): number } }).performance;\n return p ? p.now() : 0;\n });\n this.allowed = opts.fields ? new Set(opts.fields) : null;\n }\n\n private state(field: string): FieldState | null {\n if (this.allowed && !this.allowed.has(field)) return null;\n let s = this.fields.get(field);\n if (!s) {\n s = newField();\n this.fields.set(field, s);\n }\n return s;\n }\n\n focus(field: string): void {\n const s = this.state(field);\n if (s && s.focusedAt === null) s.focusedAt = this.now();\n }\n\n blur(field: string): void {\n const s = this.state(field);\n if (!s || s.focusedAt === null) return;\n s.focusMs += this.now() - s.focusedAt;\n s.focusedAt = null;\n }\n\n keystroke(field: string, key: string): void {\n const s = this.state(field);\n if (!s) return;\n if (CORRECTION_KEYS.has(key)) {\n s.corrections += 1;\n return;\n }\n if (NON_TYPING_KEYS.has(key)) return;\n s.keystrokes += 1;\n }\n\n paste(field: string): void {\n const s = this.state(field);\n if (s) s.pasted = true;\n }\n\n /**\n * Current telemetry. Call at submit. Fields still focused have their\n * in-progress focus time included without ending the focus, so calling this\n * twice is safe.\n */\n snapshot(): FormTelemetrySnapshot {\n const t = this.now();\n const fields: Record<string, FormFieldTelemetry> = {};\n for (const [name, s] of this.fields) {\n const live = s.focusedAt === null ? 0 : t - s.focusedAt;\n fields[name] = {\n pasted: s.pasted,\n keystrokes: s.keystrokes,\n corrections: s.corrections,\n focus_ms: Math.round(s.focusMs + live),\n };\n }\n return { load_to_submit_ms: Math.round(t), fields };\n }\n}\n\ninterface ListenerTarget {\n addEventListener(\n type: string,\n listener: (e: never) => void,\n options?: { capture?: boolean; passive?: boolean },\n ): void;\n removeEventListener(type: string, listener: (e: never) => void, options?: unknown): void;\n}\n\nexport interface FormTracker {\n /** Telemetry so far. Call at submit and send it as the `form` block. */\n snapshot(): FormTelemetrySnapshot;\n /** Detach listeners. Safe to call twice. */\n stop(): void;\n}\n\n/**\n * `type` values that make an element a control rather than a field.\n *\n * Controls take focus and receive key events like anything else on the form, so\n * without this they arrive as fill telemetry. The submit button is the one that\n * shows up in practice: it usually has no `name` and no `id`, so it fell through\n * the name → id → type chain and reported itself as a field literally called\n * \"submit\", with `keystrokes: 0` and `pasted: false` — a non-fill control\n * indistinguishable, in the payload, from a field a script had pasted into.\n *\n * Matched on `type` rather than `tagName` because that covers both spellings of\n * the same control: `<button>` reports `type === \"submit\"` by default, and\n * `<input type=\"submit\">` reports it explicitly. `image` is the third spelling\n * — `<input type=\"image\">` is a submit button drawn as a picture. An\n * `<input name=\"submit\">` is still a text field, reports `type === \"text\"`, and\n * is still tracked.\n */\nconst CONTROL_TYPES = new Set([\"submit\", \"button\", \"reset\", \"image\"]);\n\n/**\n * Attach fill tracking to a form.\n *\n * Only field NAMES and the counts above are recorded — never values. Listeners\n * are passive and capturing, so they never block typing.\n *\n * Buttons are not fields: submit, reset, plain and image buttons are skipped, so\n * they never appear in `fields`. For tighter control over what is reported, pass\n * an explicit `fields` allowlist.\n *\n * ```js\n * const tracker = trackForm(document.querySelector(\"#signup\"));\n * // at submit:\n * body.form = tracker.snapshot();\n * ```\n */\nexport function trackForm(\n form: ListenerTarget,\n opts: FormTelemetryOptions = {},\n): FormTracker {\n const telemetry = new FormTelemetry(opts);\n const nameOf = (e: { target?: unknown }): string | null => {\n const t = e.target as { name?: string; id?: string; type?: string } | null;\n if (!t) return null;\n // Before naming it: is this a field at all? Checked first so a control is\n // dropped however it is named — a submit button carrying a `name` is still\n // not something a human fills in.\n if (typeof t.type === \"string\" && CONTROL_TYPES.has(t.type)) return null;\n return t.name || t.id || t.type || null;\n };\n\n const onFocus = (e: never): void => {\n const n = nameOf(e);\n if (n) telemetry.focus(n);\n };\n const onBlur = (e: never): void => {\n const n = nameOf(e);\n if (n) telemetry.blur(n);\n };\n const onKeyDown = (e: never): void => {\n const n = nameOf(e);\n if (n) telemetry.keystroke(n, (e as { key?: string }).key ?? \"\");\n };\n const onPaste = (e: never): void => {\n const n = nameOf(e);\n if (n) telemetry.paste(n);\n };\n const onInput = (e: never): void => {\n // Covers paste via context menu and programmatic insertion that does not\n // raise a `paste` event.\n if ((e as { inputType?: string }).inputType === \"insertFromPaste\") {\n const n = nameOf(e);\n if (n) telemetry.paste(n);\n }\n };\n\n const opt = { capture: true, passive: true };\n const bound: [string, (e: never) => void][] = [\n [\"focusin\", onFocus],\n [\"focusout\", onBlur],\n [\"keydown\", onKeyDown],\n [\"paste\", onPaste],\n [\"input\", onInput],\n ];\n for (const [type, fn] of bound) form.addEventListener(type, fn, opt);\n\n let stopped = false;\n return {\n snapshot: () => telemetry.snapshot(),\n stop: () => {\n if (stopped) return;\n stopped = true;\n for (const [type, fn] of bound) form.removeEventListener(type, fn, opt);\n },\n };\n}\n","// The one call a signup page makes: collect everything the browser can offer,\n// shaped so your backend can forward it verbatim.\n\nimport { deviceFingerprint } from \"./device.js\";\nimport { detectAutomation } from \"./automation.js\";\nimport { nav, safe, timezone } from \"./env.js\";\nimport type { FormTracker, FormTelemetrySnapshot } from \"./form.js\";\n\nexport interface SignupSignalsOptions {\n /** Your publishable site key (`pk_live_…`). */\n siteKey: string;\n /** A tracker from `trackForm()`, if you wired one up. */\n form?: FormTracker;\n /** Threshold for `automation.automated`. Does not affect the score. */\n automationThreshold?: number;\n}\n\n/**\n * Exactly the shape `registrations.score()` accepts for these fields — post it\n * to your own endpoint and spread it into the request there.\n */\nexport interface SignupSignals {\n device_fingerprint: string;\n client: {\n /** Present only when the browser actually reported `navigator.webdriver`. */\n webdriver?: boolean;\n fingerprint_anomaly: number;\n load_to_submit_ms?: number;\n timezone?: string;\n };\n form?: FormTelemetrySnapshot;\n /** Diagnostics for your own logging. Not part of the wire contract. */\n diagnostics: {\n automation_score: number;\n automation_reasons: string[];\n fingerprint_strong: boolean;\n fingerprint_coverage: { present: number; total: number };\n };\n}\n\n/**\n * Collect every signup signal available in the browser.\n *\n * **Post this to your own backend, not to Vectoral.** The API key is a secret\n * and the IP must be read server-side. Your backend should also sanity-check\n * what arrives — clamp `fingerprint_anomaly` to [0,1] and ignore absent fields\n * — rather than trusting the client blindly.\n *\n * ```js\n * const tracker = trackForm(formEl);\n * // at submit:\n * const signals = await signupSignals({ siteKey: \"pk_live_…\", form: tracker });\n * await fetch(\"/api/signup\", { method: \"POST\", body: JSON.stringify({ email, signals }) });\n * ```\n */\nexport async function signupSignals(\n opts: SignupSignalsOptions,\n): Promise<SignupSignals> {\n // Written as two statements, not array-destructured from a literal. The\n // destructured form read as if the two ran concurrently; it never did —\n // array literals evaluate left to right, so the `await` completed before\n // `detectAutomation` was called. That was correct only because\n // `detectAutomation` is synchronous, and it would have silently become a\n // serialised round trip the day it gained an `await` of its own. Plain\n // statements say what actually happens; if these ever both become async,\n // `Promise.all` is the change to make and it will be obvious that it is one.\n const device = await deviceFingerprint({ siteKey: opts.siteKey });\n const automation = detectAutomation(\n opts.automationThreshold !== undefined\n ? { threshold: opts.automationThreshold }\n : {},\n );\n const formSnapshot = opts.form?.snapshot();\n const tz = timezone();\n const webdriver = safe(() => nav().webdriver, undefined);\n\n return {\n device_fingerprint: device.fingerprint,\n client: {\n // A real `false` is a useful negative; omitting the field says nothing.\n // So the field is included ONLY when the browser actually reported the\n // property. The automation tell cannot answer this — it is\n // `navigator.webdriver === true`, which reads `false` for \"absent\" and\n // for \"measured false\" alike, so probe the property directly.\n ...(typeof webdriver === \"boolean\" ? { webdriver } : {}),\n fingerprint_anomaly: automation.fingerprintAnomaly,\n ...(formSnapshot ? { load_to_submit_ms: formSnapshot.load_to_submit_ms } : {}),\n ...(tz ? { timezone: tz } : {}),\n },\n ...(formSnapshot ? { form: formSnapshot } : {}),\n diagnostics: {\n automation_score: automation.score,\n automation_reasons: automation.reasons,\n fingerprint_strong: device.strong,\n fingerprint_coverage: device.coverage,\n },\n };\n}\n"],"mappings":";AAGO,IAAM,OAAO,CAAI,IAAa,aAAmB;AACtD,MAAI;AACF,WAAO,GAAG;AAAA,EACZ,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAuBO,IAAM,MAAM,MAChB,WAA6C,aAAa,CAAC;AAEvD,IAAM,MAAM,MACjB;AAEK,IAAM,MAAM,MAChB,WAAuC;AAGnC,IAAM,SAAS,MACpB,OAAO,eAAe,YAAY,IAAI,MAAM;AAQvC,IAAM,aAAa,MACxB;AAAA,EACE,MAAM;AACJ,UAAM,IAAK,WAAmC;AAC9C,WAAO;AAAA,MACL,OAAO,GAAG,SAAS;AAAA,MACnB,QAAQ,GAAG,UAAU;AAAA,MACrB,YAAY,GAAG,cAAc;AAAA,IAC/B;AAAA,EACF;AAAA,EACA,EAAE,OAAO,GAAG,QAAQ,GAAG,YAAY,EAAE;AACvC;AAEK,IAAM,WAAW,MACtB,KAAK,MAAM,KAAK,eAAe,EAAE,gBAAgB,EAAE,YAAY,MAAM,IAAI;AAOpE,IAAM,YAAY,MACvB,KAAK,MAAM;AACT,QAAM,IAAI,IAAI;AACd,MAAI,CAAC,EAAG,QAAO;AACf,QAAM,IAAI,EAAE,cAAc,QAAQ;AAClC,QAAM,KAAM,EAAE,WAAW,OAAO,KAC9B,EAAE,WAAW,oBAAoB;AACnC,MAAI,CAAC,GAAI,QAAO;AAChB,QAAM,MAAM,GAAG,aAAa,2BAA2B;AACvD,MAAI,CAAC,IAAK,QAAO,EAAE,QAAQ,MAAM,UAAU,KAAK;AAChD,SAAO;AAAA,IACL,QAAQ,OAAO,GAAG,aAAa,IAAI,qBAAqB,KAAK,EAAE;AAAA,IAC/D,UAAU,OAAO,GAAG,aAAa,IAAI,uBAAuB,KAAK,EAAE;AAAA,EACrE;AACF,GAAG,IAAI;AAOF,IAAM,eAAe,MAC1B,KAAK,MAAM;AACT,QAAM,IAAI,IAAI;AACd,MAAI,CAAC,EAAG,QAAO;AACf,QAAM,IAAI,EAAE,cAAc,QAAQ;AAClC,IAAE,QAAQ;AACV,IAAE,SAAS;AACX,QAAM,MAAM,EAAE,WAAW,IAAI;AAC7B,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI,eAAe;AACnB,MAAI,OAAO;AACX,MAAI,YAAY;AAChB,MAAI,SAAS,KAAK,GAAG,IAAI,EAAE;AAC3B,MAAI,YAAY;AAChB,MAAI,SAAS,0BAAqB,GAAG,EAAE;AACvC,MAAI,YAAY;AAChB,MAAI,SAAS,0BAAqB,GAAG,EAAE;AACvC,SAAO,EAAE,UAAU;AACrB,GAAG,IAAI;;;AC3FT,IAAM,sBAAsB;AAoC5B,SAAS,oBAAmD;AAC1D,QAAM,IAAI,KAAK,KAAK,CAAC,CAAC;AACtB,QAAM,IAAI,WAAW;AACrB,QAAM,KAAK,UAAU;AACrB,QAAM,SAAS,aAAa;AAK5B,QAAM,QAAQ,CAAC,OACb,KAAK,IAAI,IAAI,KAAK;AAEpB,SAAO;AAAA,IACL,UAAU,MAAM,MAAM,EAAE,eAAe,YAAY,EAAE,QAAQ;AAAA;AAAA;AAAA,IAG7D,QAAQ;AAAA,MAAM,MACZ,EAAE,eAAe,QACb,IAAI,CAAC,MAAM,EAAE,KAAK,EACnB,OAAO,CAAC,MAAM,CAAC,eAAe,KAAK,CAAC,CAAC,EACrC,KAAK,EACL,KAAK,GAAG;AAAA,IACb;AAAA,IACA,QAAQ;AAAA,MAAM,MACZ,EAAE,eAAe,WAAW,SAAY,OAAO,OAAO,EAAE,cAAc,MAAM;AAAA,IAC9E;AAAA,IACA,UAAU,MAAM,MAAM,EAAE,QAAQ;AAAA,IAChC,WAAW,MAAM,MAAM,EAAE,WAAW,KAAK,GAAG,CAAC;AAAA,IAC7C,UAAU,SAAS;AAAA,IACnB,QAAQ,MAAM,MAAO,EAAE,QAAQ,IAAI,GAAG,EAAE,KAAK,IAAI,EAAE,MAAM,IAAI,EAAE,UAAU,KAAK,IAAK;AAAA,IACnF,aAAa;AAAA,MAAM,MACjB,EAAE,sBAAsB,OAAO,EAAE,mBAAmB,IAAI;AAAA,IAC1D;AAAA,IACA,QAAQ,MAAM,MAAO,EAAE,eAAe,OAAO,EAAE,YAAY,IAAI,IAAK;AAAA,IACpE,OAAO;AAAA,MAAM,MACX,EAAE,mBAAmB,SAAY,OAAO,OAAO,EAAE,cAAc;AAAA,IACjE;AAAA,IACA,aAAa,IAAI,UAAU;AAAA,IAC3B,eAAe,IAAI,YAAY;AAAA,IAC/B;AAAA,EACF;AACF;AAcA,eAAsB,kBACpB,MAC4B;AAC5B,MAAI,CAAC,MAAM,SAAS;AAClB,UAAM,IAAI,MAAM,2DAA2D;AAAA,EAC7E;AACA,MAAI,CAAC,OAAO,GAAG;AACb,UAAM,IAAI,MAAM,gEAA2D;AAAA,EAC7E;AACA,QAAM,aAAa,kBAAkB;AACrC,QAAM,UAAU,OAAO,QAAQ,UAAU;AACzC,QAAM,UAAU,QAAQ,OAAO,CAAC,CAAC,EAAE,CAAC,MAAM,MAAM,QAAQ,MAAM,EAAE,EAAE;AAElE,QAAM,WAAW;AAAA,IACf,IAAI,mBAAmB;AAAA,IACvB,KAAK;AAAA,IACL,GAAG,QAAQ,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,EAAE,EAAE;AAAA,EAC9C,EAAE,KAAK,GAAG;AAEV,QAAM,EAAE,QAAQ,OAAO,IAAI,MAAM,QAAQ,QAAQ;AACjD,SAAO;AAAA,IACL,aAAa,MAAM,OAAO,MAAM,GAAG,EAAE,CAAC;AAAA,IACtC;AAAA,IACA,UAAU,EAAE,SAAS,OAAO,QAAQ,OAAO;AAAA,IAC3C;AAAA,EACF;AACF;AAEA,eAAe,QAAQ,OAA6D;AAClF,QAAM,SAAS;AAAA,IACb,MAAO,WAAsD,QAAQ;AAAA,IACrE;AAAA,EACF;AACA,MAAI,QAAQ;AACV,QAAI;AACF,YAAM,QAAQ,IAAI,YAAY,EAAE,OAAO,KAAK;AAC5C,YAAM,MAAM,MAAM,OAAO,OAAO,WAAW,KAAK;AAChD,YAAM,MAAM,MAAM;AAAA,QAAK,IAAI,WAAW,GAAG;AAAA,QAAG,CAAC,MAC3C,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AAAA,MAChC,EAAE,KAAK,EAAE;AACT,aAAO,EAAE,QAAQ,KAAK,QAAQ,KAAK;AAAA,IACrC,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO,EAAE,QAAQ,SAAS,KAAK,GAAG,QAAQ,MAAM;AAClD;AAOA,SAAS,SAAS,OAAuB;AACvC,QAAM,QAAQ;AACd,QAAM,QAAQ,CAAC,YAAY,UAAY,YAAY,UAAU;AAC7D,SAAO,MACJ,IAAI,CAAC,SAAS;AACb,QAAI,IAAI,SAAS;AACjB,aAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,WAAK,MAAM,WAAW,CAAC,IAAI;AAC3B,UAAI,KAAK,KAAK,GAAG,KAAK,MAAM;AAC5B,WAAK,MAAM,WAAW,CAAC,MAAM;AAC7B,UAAI,KAAK,KAAK,GAAG,KAAK,MAAM;AAAA,IAC9B;AACA,WAAO,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AAAA,EACvC,CAAC,EACA,KAAK,EAAE;AACZ;;;AC1IA,IAAM,qBAAqB;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,IAAM,QAAQ,MAAc;AAI1B,QAAM,IAAI,KAAK,KAAK,CAAC,CAAC;AACtB,QAAM,IAAI,KAAK,KAAK,CAAC,CAA4B;AACjD,QAAM,KAAK,KAAK,MAAM,EAAE,aAAa,IAAI,EAAE;AAC3C,QAAM,IAAI,WAAW;AACrB,QAAM,KAAK,UAAU;AAErB,SAAO;AAAA,IACL;AAAA,MACE,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,MAAM,MAAM,EAAE,cAAc;AAAA,IAC9B;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,MAAM,MAAM,mBAAmB,KAAK,CAAC,MAAM,KAAK,CAAC;AAAA,IACnD;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,MAAM,MACJ,KAAK,MAAM;AACT,cAAM,IAAI,IAAI;AACd,YAAI,CAAC,EAAG,QAAO;AACf,eAAO,OAAO,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,EAAE,WAAW,MAAM,KAAK,EAAE,WAAW,OAAO,CAAC;AAAA,MACjF,GAAG,KAAK;AAAA,IACZ;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,MAAM,MAAM,YAAY,KAAK,EAAE;AAAA,IACjC;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,eAAe;AAAA,MACf,MAAM,MAAM,MAAM,QAAQ,EAAE,SAAS,KAAK,EAAE,UAAU,WAAW;AAAA,IACnE;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,eAAe;AAAA,MACf,MAAM,MACJ,KAAK,MAAM;AACT,cAAM,QAAQ;AACd,eAAO,MAAM,eAAe,KAAK,MAAM,gBAAgB;AAAA,MACzD,GAAG,KAAK;AAAA,IACZ;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,eAAe;AAAA;AAAA;AAAA,MAGf,MAAM,MACJ,KAAK,MAAM;AACT,cAAM,MAAiB;AAAA,UACpB,EAA4C,aAAa;AAAA,UACzD,EAA4D,UACzD,WAAW;AAAA,QACjB;AACA,eAAO,IAAI;AAAA,UACT,CAAC,MAAM,OAAO,MAAM,cAAc,CAAC,kBAAkB,KAAK,OAAO,CAAC,CAAC;AAAA,QACrE;AAAA,MACF,GAAG,KAAK;AAAA,IACZ;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,eAAe;AAAA,MACf,MAAM,MAAM,gDAAgD,KAAK,IAAI,YAAY,EAAE;AAAA,IACrF;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,eAAe;AAAA,MACf,MAAM,MAAM,UAAU,KAAK,EAAE,KAAK,EAAE,YAAY;AAAA,IAClD;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,eAAe;AAAA,MACf,MAAM,MAAM,EAAE,UAAU,KAAK,EAAE,WAAW;AAAA,IAC5C;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,eAAe;AAAA,MACf,MAAM,MAAM;AACV,cAAM,WAAW,EAAE,eAAe,YAAY,EAAE,YAAY;AAC5D,YAAI,CAAC,YAAY,CAAC,GAAI,QAAO;AAC7B,cAAM,UAAU,WAAW,KAAK,EAAE,IAC9B,QACA,oBAAoB,KAAK,EAAE,IACzB,QACA,iBAAiB,KAAK,EAAE,IACtB,UACA;AACR,YAAI,CAAC,QAAS,QAAO;AACrB,cAAM,SAAS,SAAS,YAAY;AACpC,YAAI,YAAY,MAAO,QAAO,CAAC,MAAM,KAAK,MAAM;AAChD,YAAI,YAAY,MAAO,QAAO,CAAC,MAAM,KAAK,MAAM;AAChD,eAAO,CAAC,oBAAoB,KAAK,MAAM;AAAA,MACzC;AAAA,IACF;AAAA,EACF;AACF;AAGA,IAAM,UAAU,CAAC,YACf,QAAQ,WAAW,IAAI,IAAI,IAAI,QAAQ,OAAO,CAAC,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC;AAE5E,IAAM,SAAS,CAAC,MAAsB,KAAK,MAAM,IAAI,GAAG,IAAI;AAMrD,SAAS,iBAAiB,OAAgC,CAAC,GAAqB;AACrF,QAAM,YAAY,KAAK,aAAa;AACpC,QAAM,YAAY,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,KAAK,EAAE,MAAM,KAAK,EAAE,EAAE;AAE3E,QAAM,QAAQ,UAAU,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM;AACjF,QAAM,QAAQ,QAAQ,MAAM,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC;AAChD,QAAM,UAAU,QAAQ,MAAM,OAAO,CAAC,MAAM,EAAE,aAAa,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC;AAEjF,QAAM,UAAmC,CAAC;AAC1C,aAAW,KAAK,UAAW,SAAQ,EAAE,IAAI,IAAI,EAAE;AAE/C,SAAO;AAAA,IACL,OAAO,OAAO,KAAK;AAAA,IACnB,WAAW,SAAS;AAAA,IACpB,SAAS,MAAM,IAAI,CAAC,MAAM,EAAE,IAAI;AAAA,IAChC;AAAA,IACA,oBAAoB,OAAO,OAAO;AAAA,EACpC;AACF;;;AC9JA,IAAM,WAAW,OAAmB;AAAA,EAClC,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,SAAS;AAAA,EACT,WAAW;AACb;AAGA,IAAM,kBAAkB,oBAAI,IAAI,CAAC,aAAa,UAAU,MAAM,CAAC;AAG/D,IAAM,kBAAkB,oBAAI,IAAI;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAMM,IAAM,gBAAN,MAAoB;AAAA,EACR,SAAS,oBAAI,IAAwB;AAAA,EACrC;AAAA,EACA;AAAA,EAEjB,YAAY,OAA6B,CAAC,GAAG;AAC3C,SAAK,MACH,KAAK,QACJ,MAAM;AACL,YAAM,IAAK,WAAmD;AAC9D,aAAO,IAAI,EAAE,IAAI,IAAI;AAAA,IACvB;AACF,SAAK,UAAU,KAAK,SAAS,IAAI,IAAI,KAAK,MAAM,IAAI;AAAA,EACtD;AAAA,EAEQ,MAAM,OAAkC;AAC9C,QAAI,KAAK,WAAW,CAAC,KAAK,QAAQ,IAAI,KAAK,EAAG,QAAO;AACrD,QAAI,IAAI,KAAK,OAAO,IAAI,KAAK;AAC7B,QAAI,CAAC,GAAG;AACN,UAAI,SAAS;AACb,WAAK,OAAO,IAAI,OAAO,CAAC;AAAA,IAC1B;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,OAAqB;AACzB,UAAM,IAAI,KAAK,MAAM,KAAK;AAC1B,QAAI,KAAK,EAAE,cAAc,KAAM,GAAE,YAAY,KAAK,IAAI;AAAA,EACxD;AAAA,EAEA,KAAK,OAAqB;AACxB,UAAM,IAAI,KAAK,MAAM,KAAK;AAC1B,QAAI,CAAC,KAAK,EAAE,cAAc,KAAM;AAChC,MAAE,WAAW,KAAK,IAAI,IAAI,EAAE;AAC5B,MAAE,YAAY;AAAA,EAChB;AAAA,EAEA,UAAU,OAAe,KAAmB;AAC1C,UAAM,IAAI,KAAK,MAAM,KAAK;AAC1B,QAAI,CAAC,EAAG;AACR,QAAI,gBAAgB,IAAI,GAAG,GAAG;AAC5B,QAAE,eAAe;AACjB;AAAA,IACF;AACA,QAAI,gBAAgB,IAAI,GAAG,EAAG;AAC9B,MAAE,cAAc;AAAA,EAClB;AAAA,EAEA,MAAM,OAAqB;AACzB,UAAM,IAAI,KAAK,MAAM,KAAK;AAC1B,QAAI,EAAG,GAAE,SAAS;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,WAAkC;AAChC,UAAM,IAAI,KAAK,IAAI;AACnB,UAAM,SAA6C,CAAC;AACpD,eAAW,CAAC,MAAM,CAAC,KAAK,KAAK,QAAQ;AACnC,YAAM,OAAO,EAAE,cAAc,OAAO,IAAI,IAAI,EAAE;AAC9C,aAAO,IAAI,IAAI;AAAA,QACb,QAAQ,EAAE;AAAA,QACV,YAAY,EAAE;AAAA,QACd,aAAa,EAAE;AAAA,QACf,UAAU,KAAK,MAAM,EAAE,UAAU,IAAI;AAAA,MACvC;AAAA,IACF;AACA,WAAO,EAAE,mBAAmB,KAAK,MAAM,CAAC,GAAG,OAAO;AAAA,EACpD;AACF;AAmCA,IAAM,gBAAgB,oBAAI,IAAI,CAAC,UAAU,UAAU,SAAS,OAAO,CAAC;AAkB7D,SAAS,UACd,MACA,OAA6B,CAAC,GACjB;AACb,QAAM,YAAY,IAAI,cAAc,IAAI;AACxC,QAAM,SAAS,CAAC,MAA2C;AACzD,UAAM,IAAI,EAAE;AACZ,QAAI,CAAC,EAAG,QAAO;AAIf,QAAI,OAAO,EAAE,SAAS,YAAY,cAAc,IAAI,EAAE,IAAI,EAAG,QAAO;AACpE,WAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ;AAAA,EACrC;AAEA,QAAM,UAAU,CAAC,MAAmB;AAClC,UAAM,IAAI,OAAO,CAAC;AAClB,QAAI,EAAG,WAAU,MAAM,CAAC;AAAA,EAC1B;AACA,QAAM,SAAS,CAAC,MAAmB;AACjC,UAAM,IAAI,OAAO,CAAC;AAClB,QAAI,EAAG,WAAU,KAAK,CAAC;AAAA,EACzB;AACA,QAAM,YAAY,CAAC,MAAmB;AACpC,UAAM,IAAI,OAAO,CAAC;AAClB,QAAI,EAAG,WAAU,UAAU,GAAI,EAAuB,OAAO,EAAE;AAAA,EACjE;AACA,QAAM,UAAU,CAAC,MAAmB;AAClC,UAAM,IAAI,OAAO,CAAC;AAClB,QAAI,EAAG,WAAU,MAAM,CAAC;AAAA,EAC1B;AACA,QAAM,UAAU,CAAC,MAAmB;AAGlC,QAAK,EAA6B,cAAc,mBAAmB;AACjE,YAAM,IAAI,OAAO,CAAC;AAClB,UAAI,EAAG,WAAU,MAAM,CAAC;AAAA,IAC1B;AAAA,EACF;AAEA,QAAM,MAAM,EAAE,SAAS,MAAM,SAAS,KAAK;AAC3C,QAAM,QAAwC;AAAA,IAC5C,CAAC,WAAW,OAAO;AAAA,IACnB,CAAC,YAAY,MAAM;AAAA,IACnB,CAAC,WAAW,SAAS;AAAA,IACrB,CAAC,SAAS,OAAO;AAAA,IACjB,CAAC,SAAS,OAAO;AAAA,EACnB;AACA,aAAW,CAAC,MAAM,EAAE,KAAK,MAAO,MAAK,iBAAiB,MAAM,IAAI,GAAG;AAEnE,MAAI,UAAU;AACd,SAAO;AAAA,IACL,UAAU,MAAM,UAAU,SAAS;AAAA,IACnC,MAAM,MAAM;AACV,UAAI,QAAS;AACb,gBAAU;AACV,iBAAW,CAAC,MAAM,EAAE,KAAK,MAAO,MAAK,oBAAoB,MAAM,IAAI,GAAG;AAAA,IACxE;AAAA,EACF;AACF;;;ACzMA,eAAsB,cACpB,MACwB;AASxB,QAAM,SAAS,MAAM,kBAAkB,EAAE,SAAS,KAAK,QAAQ,CAAC;AAChE,QAAM,aAAa;AAAA,IACjB,KAAK,wBAAwB,SACzB,EAAE,WAAW,KAAK,oBAAoB,IACtC,CAAC;AAAA,EACP;AACA,QAAM,eAAe,KAAK,MAAM,SAAS;AACzC,QAAM,KAAK,SAAS;AACpB,QAAM,YAAY,KAAK,MAAM,IAAI,EAAE,WAAW,MAAS;AAEvD,SAAO;AAAA,IACL,oBAAoB,OAAO;AAAA,IAC3B,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAMN,GAAI,OAAO,cAAc,YAAY,EAAE,UAAU,IAAI,CAAC;AAAA,MACtD,qBAAqB,WAAW;AAAA,MAChC,GAAI,eAAe,EAAE,mBAAmB,aAAa,kBAAkB,IAAI,CAAC;AAAA,MAC5E,GAAI,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;AAAA,IAC/B;AAAA,IACA,GAAI,eAAe,EAAE,MAAM,aAAa,IAAI,CAAC;AAAA,IAC7C,aAAa;AAAA,MACX,kBAAkB,WAAW;AAAA,MAC7B,oBAAoB,WAAW;AAAA,MAC/B,oBAAoB,OAAO;AAAA,MAC3B,sBAAsB,OAAO;AAAA,IAC/B;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,2 @@
1
+ "use strict";(()=>{var s=(e,t)=>{try{return e()}catch{return t}},d=()=>globalThis.navigator??{};var a=()=>globalThis.document,u=()=>typeof globalThis=="object"&&a()!==void 0,f=()=>s(()=>{let e=globalThis.screen;return{width:e?.width??0,height:e?.height??0,colorDepth:e?.colorDepth??0}},{width:0,height:0,colorDepth:0}),p=()=>s(()=>Intl.DateTimeFormat().resolvedOptions().timeZone??null,null),m=()=>s(()=>{let e=a();if(!e)return null;let t=e.createElement("canvas"),n=t.getContext("webgl")??t.getContext("experimental-webgl");if(!n)return null;let i=n.getExtension("WEBGL_debug_renderer_info");return i?{vendor:String(n.getParameter(i.UNMASKED_VENDOR_WEBGL)??""),renderer:String(n.getParameter(i.UNMASKED_RENDERER_WEBGL)??"")}:{vendor:null,renderer:null}},null),b=()=>s(()=>{let e=a();if(!e)return null;let t=e.createElement("canvas");t.width=240,t.height=60;let n=t.getContext("2d");return n?(n.textBaseline="top",n.font="14px 'Arial'",n.fillStyle="#f60",n.fillRect(125,1,62,20),n.fillStyle="#069",n.fillText("Vectoral \u26A1 sensor",2,15),n.fillStyle="rgba(102,204,0,0.7)",n.fillText("Vectoral \u26A1 sensor",4,17),t.toDataURL()):null},null);var x=1;function S(){let e=s(d,{}),t=f(),n=m(),i=b(),r=o=>s(o,null)??null;return{platform:r(()=>e.userAgentData?.platform??e.platform),brands:r(()=>e.userAgentData?.brands?.map(o=>o.brand).filter(o=>!/not.a.brand/i.test(o)).sort().join(",")),mobile:r(()=>e.userAgentData?.mobile===void 0?null:String(e.userAgentData.mobile)),language:r(()=>e.language),languages:r(()=>e.languages?.join(",")),timezone:p(),screen:r(()=>t.width>0?`${t.width}x${t.height}x${t.colorDepth}`:null),concurrency:r(()=>e.hardwareConcurrency?String(e.hardwareConcurrency):null),memory:r(()=>e.deviceMemory?String(e.deviceMemory):null),touch:r(()=>e.maxTouchPoints===void 0?null:String(e.maxTouchPoints)),webglVendor:n?.vendor??null,webglRenderer:n?.renderer??null,canvas:i}}async function h(e){if(!e?.siteKey)throw new Error("deviceFingerprint: `siteKey` is required (it is the salt)");if(!u())throw new Error("deviceFingerprint: no DOM \u2014 this runs in the browser only");let t=S(),n=Object.entries(t),i=n.filter(([,c])=>c!==null&&c!=="").length,r=[`v${x}`,e.siteKey,...n.map(([c,w])=>`${c}=${w??""}`)].join("|"),{digest:o,strong:l}=await D(r);return{fingerprint:`fp_${o.slice(0,32)}`,strong:l,coverage:{present:i,total:n.length},components:t}}async function D(e){let t=s(()=>globalThis.crypto?.subtle,void 0);if(t)try{let n=new TextEncoder().encode(e),i=await t.digest("SHA-256",n);return{digest:Array.from(new Uint8Array(i),o=>o.toString(16).padStart(2,"0")).join(""),strong:!0}}catch{}return{digest:E(e),strong:!1}}function E(e){return[2166136261,16777619,2654435769,2246822507].map(i=>{let r=i>>>0;for(let o=0;o<e.length;o++)r^=e.charCodeAt(o)&255,r=Math.imul(r,16777619)>>>0,r^=e.charCodeAt(o)>>>8,r=Math.imul(r,16777619)>>>0;return r.toString(16).padStart(8,"0")}).join("")}var g="vectoralFp",k=1;function R(e){let t=s(()=>new URL(e?.src??"").searchParams,null),n=e?.dataset??{};return{siteKey:t?.get("siteKey")||n.siteKey||null,debug:t?.get("debug")==="1"||n.debug==="true"}}function A(){let e=a();return e?e.currentScript??s(()=>e.querySelector?.('script[src*="vectoral-fingerprint"]'),null):null}async function P(e){if(!e.siteKey)throw new Error(`${g}: no siteKey configured \u2014 add data-site-key="pk_live_\u2026" to the script tag (it is the salt, so there is no default)`);let t=await h({siteKey:e.siteKey}),n={fingerprint:t.fingerprint,strong:t.strong,coverage:t.coverage};return e.debug&&(n.components=t.components),n}function v(){let e=R(A()),t=globalThis,n=null,i=()=>n??=P(e),r=t[g]?.q,o={version:k,get:i,q:{push:l=>y([l],o)}};t[g]=o,e.siteKey&&i().catch(()=>{}),Array.isArray(r)&&y(r,o)}function y(e,t){for(let n of e){if(!Array.isArray(n))continue;let[i,,r]=n,o=t[i];if(typeof o!="function"){r?.(new Error(`${g}: no such method \`${String(i)}\``));continue}Promise.resolve(o()).then(l=>r?.(null,l),l=>r?.(l))}}u()&&v();})();
2
+ //# sourceMappingURL=vectoral-fingerprint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/env.ts","../src/device.ts","../src/standalone.ts","../src/standalone.entry.ts"],"sourcesContent":["// Environment probing. Every probe is wrapped: a fingerprint that throws in a\n// hardened browser is worse than one with a missing component.\n\nexport const safe = <T>(fn: () => T, fallback: T): T => {\n try {\n return fn();\n } catch {\n return fallback;\n }\n};\n\nexport interface UserAgentData {\n brands?: { brand: string; version: string }[];\n mobile?: boolean;\n platform?: string;\n}\n\ninterface NavigatorLike {\n userAgent?: string;\n userAgentData?: UserAgentData;\n platform?: string;\n language?: string;\n languages?: readonly string[];\n hardwareConcurrency?: number;\n deviceMemory?: number;\n maxTouchPoints?: number;\n webdriver?: boolean;\n plugins?: { length: number };\n cookieEnabled?: boolean;\n permissions?: unknown;\n}\n\nexport const nav = (): NavigatorLike =>\n (globalThis as { navigator?: NavigatorLike }).navigator ?? {};\n\nexport const win = (): Record<string, unknown> =>\n globalThis as unknown as Record<string, unknown>;\n\nexport const doc = (): Document | undefined =>\n (globalThis as { document?: Document }).document;\n\n/** Is there enough of a DOM to collect anything at all? */\nexport const hasDom = (): boolean =>\n typeof globalThis === \"object\" && doc() !== undefined;\n\nexport interface ScreenInfo {\n width: number;\n height: number;\n colorDepth: number;\n}\n\nexport const screenInfo = (): ScreenInfo =>\n safe(\n () => {\n const s = (globalThis as { screen?: Screen }).screen;\n return {\n width: s?.width ?? 0,\n height: s?.height ?? 0,\n colorDepth: s?.colorDepth ?? 0,\n };\n },\n { width: 0, height: 0, colorDepth: 0 },\n );\n\nexport const timezone = (): string | null =>\n safe(() => Intl.DateTimeFormat().resolvedOptions().timeZone ?? null, null);\n\nexport interface WebglInfo {\n vendor: string | null;\n renderer: string | null;\n}\n\nexport const webglInfo = (): WebglInfo | null =>\n safe(() => {\n const d = doc();\n if (!d) return null;\n const c = d.createElement(\"canvas\");\n const gl = (c.getContext(\"webgl\") ??\n c.getContext(\"experimental-webgl\")) as WebGLRenderingContext | null;\n if (!gl) return null;\n const dbg = gl.getExtension(\"WEBGL_debug_renderer_info\");\n if (!dbg) return { vendor: null, renderer: null };\n return {\n vendor: String(gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL) ?? \"\"),\n renderer: String(gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) ?? \"\"),\n };\n }, null);\n\n/**\n * A canvas rendering, as a data URL. The caller hashes it — we return the raw\n * string so the whole fingerprint goes through one salted digest rather than\n * leaking a second, unsalted one.\n */\nexport const canvasSample = (): string | null =>\n safe(() => {\n const d = doc();\n if (!d) return null;\n const c = d.createElement(\"canvas\");\n c.width = 240;\n c.height = 60;\n const ctx = c.getContext(\"2d\");\n if (!ctx) return null;\n ctx.textBaseline = \"top\";\n ctx.font = \"14px 'Arial'\";\n ctx.fillStyle = \"#f60\";\n ctx.fillRect(125, 1, 62, 20);\n ctx.fillStyle = \"#069\";\n ctx.fillText(\"Vectoral ⚡ sensor\", 2, 15);\n ctx.fillStyle = \"rgba(102,204,0,0.7)\";\n ctx.fillText(\"Vectoral ⚡ sensor\", 4, 17);\n return c.toDataURL();\n }, null);\n","// Device fingerprint — a stable, tenant-scoped identifier for this browser.\n//\n// SALT POLICY. This runs in the user's browser, so nothing it holds is secret.\n// We salt with the PUBLISHABLE site key, which buys scoping rather than\n// secrecy: the same device produces different values for different Vectoral\n// customers, so a fingerprint leaked from your logs cannot be joined against\n// another tenant's. Never put a secret salt here — shipping one to the browser\n// only tells you it was a secret. See docs/concepts/salts.md.\n\nimport {\n canvasSample,\n hasDom,\n nav,\n safe,\n screenInfo,\n timezone,\n webglInfo,\n} from \"./env.js\";\n\n/** Bumping this deliberately invalidates every previously issued value. */\nconst FINGERPRINT_VERSION = 1;\n\nexport interface DeviceFingerprintOptions {\n /** Your publishable site key (`pk_live_…`). Scopes the value to your tenant. */\n siteKey: string;\n}\n\nexport interface DeviceFingerprint {\n /** `fp_` + 32 hex chars. Send as `device_fingerprint`. */\n fingerprint: string;\n /**\n * False when SubtleCrypto was unavailable (an insecure context) and a\n * non-cryptographic fallback hash was used. The value is still stable and\n * still comparable — it is just cheaper to reverse, so do not treat it as a\n * privacy boundary.\n */\n strong: boolean;\n /**\n * How many of the components actually resolved, out of the total attempted.\n * A very low count means a hardened or headless browser, which is itself\n * worth forwarding.\n */\n coverage: { present: number; total: number };\n /** The raw components, for debugging. Never send these anywhere. */\n components: Record<string, string | null>;\n}\n\n/**\n * Components chosen for STABILITY as much as entropy. Deliberately excluded:\n *\n * - the full user-agent string and browser version: churns on every update,\n * which would reset the fingerprint of every legitimate user monthly while\n * costing an attacker nothing.\n * - window size and devicePixelRatio: change with zoom and window dragging.\n * - `navigator.plugins` contents: empty on modern browsers.\n */\nfunction collectComponents(): Record<string, string | null> {\n const n = safe(nav, {});\n const s = screenInfo();\n const gl = webglInfo();\n const canvas = canvasSample();\n // Each component is probed independently: a hardened browser that throws on\n // one property should cost us that component, not the whole fingerprint. The\n // KEY SET must stay fixed regardless, or `coverage.total` would move and two\n // browsers would hash different material for the same device.\n const probe = (fn: () => string | null | undefined): string | null =>\n safe(fn, null) ?? null;\n\n return {\n platform: probe(() => n.userAgentData?.platform ?? n.platform),\n // Brand NAMES only — versions are dropped so a browser update does not\n // change the fingerprint.\n brands: probe(() =>\n n.userAgentData?.brands\n ?.map((b) => b.brand)\n .filter((b) => !/not.a.brand/i.test(b))\n .sort()\n .join(\",\"),\n ),\n mobile: probe(() =>\n n.userAgentData?.mobile === undefined ? null : String(n.userAgentData.mobile),\n ),\n language: probe(() => n.language),\n languages: probe(() => n.languages?.join(\",\")),\n timezone: timezone(),\n screen: probe(() => (s.width > 0 ? `${s.width}x${s.height}x${s.colorDepth}` : null)),\n concurrency: probe(() =>\n n.hardwareConcurrency ? String(n.hardwareConcurrency) : null,\n ),\n memory: probe(() => (n.deviceMemory ? String(n.deviceMemory) : null)),\n touch: probe(() =>\n n.maxTouchPoints === undefined ? null : String(n.maxTouchPoints),\n ),\n webglVendor: gl?.vendor ?? null,\n webglRenderer: gl?.renderer ?? null,\n canvas,\n };\n}\n\n/**\n * Compute this browser's device fingerprint.\n *\n * Async because canvas and WebGL probing are, and because SubtleCrypto is.\n * Call it once per page and reuse the result; it does not change within a\n * session.\n *\n * Send the `fingerprint` to YOUR backend, which forwards it as\n * `device_fingerprint`. It is the single highest-value optional field on a\n * registration: one device across many signups is the strongest farm signal\n * that exists, and it is invisible to Vectoral without it.\n */\nexport async function deviceFingerprint(\n opts: DeviceFingerprintOptions,\n): Promise<DeviceFingerprint> {\n if (!opts?.siteKey) {\n throw new Error(\"deviceFingerprint: `siteKey` is required (it is the salt)\");\n }\n if (!hasDom()) {\n throw new Error(\"deviceFingerprint: no DOM — this runs in the browser only\");\n }\n const components = collectComponents();\n const entries = Object.entries(components);\n const present = entries.filter(([, v]) => v !== null && v !== \"\").length;\n\n const material = [\n `v${FINGERPRINT_VERSION}`,\n opts.siteKey,\n ...entries.map(([k, v]) => `${k}=${v ?? \"\"}`),\n ].join(\"|\");\n\n const { digest, strong } = await hashHex(material);\n return {\n fingerprint: `fp_${digest.slice(0, 32)}`,\n strong,\n coverage: { present, total: entries.length },\n components,\n };\n}\n\nasync function hashHex(input: string): Promise<{ digest: string; strong: boolean }> {\n const subtle = safe(\n () => (globalThis as { crypto?: { subtle?: SubtleCrypto } }).crypto?.subtle,\n undefined,\n );\n if (subtle) {\n try {\n const bytes = new TextEncoder().encode(input);\n const buf = await subtle.digest(\"SHA-256\", bytes);\n const hex = Array.from(new Uint8Array(buf), (b) =>\n b.toString(16).padStart(2, \"0\"),\n ).join(\"\");\n return { digest: hex, strong: true };\n } catch {\n // fall through to the non-crypto path\n }\n }\n return { digest: fnv1a128(input), strong: false };\n}\n\n/**\n * Fallback for insecure contexts: four FNV-1a passes with different offset\n * bases, concatenated to 32 hex chars. Not cryptographic — it exists so the\n * fingerprint keeps working on `http://` origins, not to resist analysis.\n */\nfunction fnv1a128(input: string): string {\n const PRIME = 16777619;\n const bases = [0x811c9dc5, 0x01000193, 0x9e3779b9, 0x85ebca6b];\n return bases\n .map((base) => {\n let h = base >>> 0;\n for (let i = 0; i < input.length; i++) {\n h ^= input.charCodeAt(i) & 0xff;\n h = Math.imul(h, PRIME) >>> 0;\n h ^= input.charCodeAt(i) >>> 8;\n h = Math.imul(h, PRIME) >>> 0;\n }\n return h.toString(16).padStart(8, \"0\");\n })\n .join(\"\");\n}\n","// Standalone drop-in entry: the same device fingerprint as the npm package,\n// reachable from a plain <script> tag on a site with no bundler.\n//\n// <script async src=\"/vectoral-fingerprint.js\" data-site-key=\"pk_live_abc\"></script>\n// const { fingerprint } = await window.vectoralFp.get();\n//\n// It posts NOTHING. Like the rest of this package it only computes and returns\n// — your page forwards the value to your own backend. See docs/concepts/salts.md\n// for why the publishable site key is the right salt here.\n//\n// GLOBAL NAME. Deliberately `vectoralFp`, not `vectoral`: the hosted sensor\n// (cdn.vectoral.cloud/v1/sensor.js) assigns `window.vectoral = api` outright\n// rather than merging, so sharing the name would make load order decide which\n// script survives. A separate global lets both run on one page.\n\nimport { deviceFingerprint } from \"./device.js\";\nimport { doc, safe } from \"./env.js\";\n\n/** Where the API is installed. See the note above on why it is not `vectoral`. */\nexport const GLOBAL_NAME = \"vectoralFp\";\n\n/** Bumped when the shape of the global's API changes, not when the hash does. */\nexport const STANDALONE_VERSION = 1;\n\n/** The parts of a <script> element we read. Kept structural so tests need no DOM. */\ninterface ScriptLike {\n src?: string;\n dataset?: Record<string, string | undefined>;\n}\n\nexport interface StandaloneConfig {\n /** The publishable site key, or null when the tag carried none. */\n siteKey: string | null;\n /** Exposes raw components on the result. Off unless explicitly asked for. */\n debug: boolean;\n}\n\n/**\n * What `get()` resolves to. A deliberate subset of `DeviceFingerprint`:\n * `components` is withheld unless debug is on, because a global on a customer's\n * page puts raw probe values one JSON.stringify away from being sent somewhere.\n */\nexport interface PublicFingerprint {\n fingerprint: string;\n strong: boolean;\n coverage: { present: number; total: number };\n components?: Record<string, string | null>;\n}\n\n/**\n * Resolve configuration from the script tag that loaded this bundle.\n *\n * Query string wins over the data attribute, matching the convention the hosted\n * sensor already established, so one integration idiom covers both scripts.\n */\nexport function readConfig(el: ScriptLike | null | undefined): StandaloneConfig {\n // An inline tag, or one whose src is relative, gives an unparseable URL —\n // that costs us the query string, not the data attributes.\n const qs = safe<URLSearchParams | null>(\n () => new URL(el?.src ?? \"\").searchParams,\n null,\n );\n const ds = el?.dataset ?? {};\n return {\n siteKey: qs?.get(\"siteKey\") || ds.siteKey || null,\n debug: qs?.get(\"debug\") === \"1\" || ds.debug === \"true\",\n };\n}\n\n/**\n * The script element we were loaded by. `document.currentScript` is set while a\n * classic script runs (including `async` ones); the query is a fallback for the\n * cases where it is not, such as re-execution from a bundler.\n */\nfunction currentScript(): ScriptLike | null {\n const d = doc() as (Document & { currentScript?: ScriptLike }) | undefined;\n if (!d) return null;\n return (\n d.currentScript ??\n safe(\n () =>\n d.querySelector?.(\n 'script[src*=\"vectoral-fingerprint\"]',\n ) as unknown as ScriptLike | null,\n null,\n )\n );\n}\n\nasync function compute(cfg: StandaloneConfig): Promise<PublicFingerprint> {\n if (!cfg.siteKey) {\n throw new Error(\n `${GLOBAL_NAME}: no siteKey configured — add data-site-key=\"pk_live_…\" ` +\n \"to the script tag (it is the salt, so there is no default)\",\n );\n }\n const r = await deviceFingerprint({ siteKey: cfg.siteKey });\n const out: PublicFingerprint = {\n fingerprint: r.fingerprint,\n strong: r.strong,\n coverage: r.coverage,\n };\n if (cfg.debug) out.components = r.components;\n return out;\n}\n\ntype QueueEntry = [string, unknown, ((err: unknown, value?: unknown) => void)?];\n\n/**\n * Install the API onto the global object.\n *\n * Any calls queued against `vectoralFp.q` before this bundle finished loading\n * are flushed afterwards, so an `async` script tag never silently drops work:\n *\n * window.vectoralFp = window.vectoralFp || { q: [] };\n * vectoralFp.q.push([\"get\", null, (err, fp) => …]);\n */\nexport function install(): void {\n const cfg = readConfig(currentScript());\n const g = globalThis as unknown as Record<string, unknown>;\n\n // The fingerprint does not change within a session, so one computation is\n // both correct and the documented usage. The promise is the cache.\n let cached: Promise<PublicFingerprint> | null = null;\n const get = (): Promise<PublicFingerprint> => (cached ??= compute(cfg));\n\n const pending = (g[GLOBAL_NAME] as { q?: unknown } | undefined)?.q;\n\n // `q` survives installation, and its `push` runs the entry immediately. With\n // an `async` script tag the integration snippet may run before OR after this\n // bundle, and a `q` that only worked in one of those orders would make the\n // documented snippet a coin flip.\n const api = {\n version: STANDALONE_VERSION,\n get,\n q: { push: (entry: QueueEntry) => flush([entry], api) },\n };\n g[GLOBAL_NAME] = api;\n\n // Start early so the value is ready by the time a form is submitted. Only\n // when configured: kicking off a doomed promise would surface as an unhandled\n // rejection in the customer's console rather than at their call site.\n if (cfg.siteKey) void get().catch(() => {});\n\n if (Array.isArray(pending)) flush(pending as QueueEntry[], api);\n}\n\nfunction flush(queue: QueueEntry[], api: { get: () => Promise<PublicFingerprint> }): void {\n for (const entry of queue) {\n if (!Array.isArray(entry)) continue;\n const [method, , cb] = entry;\n const fn = (api as unknown as Record<string, unknown>)[method];\n if (typeof fn !== \"function\") {\n cb?.(new Error(`${GLOBAL_NAME}: no such method \\`${String(method)}\\``));\n continue;\n }\n Promise.resolve((fn as () => unknown)()).then(\n (value) => cb?.(null, value),\n (err) => cb?.(err),\n );\n }\n}\n","// Bundle entry for the standalone <script> build. Everything testable lives in\n// `standalone.ts`; this file exists only so the IIFE has a side effect to run.\n// The npm entry (`index.ts`) stays side-effect free.\n\nimport { install } from \"./standalone.js\";\nimport { hasDom } from \"./env.js\";\n\nif (hasDom()) install();\n"],"mappings":"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","names":["safe","fn","fallback","nav","doc","hasDom","screenInfo","safe","s","timezone","webglInfo","d","c","gl","dbg","canvasSample","ctx","FINGERPRINT_VERSION","collectComponents","n","safe","nav","s","screenInfo","gl","webglInfo","canvas","canvasSample","probe","fn","b","timezone","deviceFingerprint","opts","hasDom","components","entries","present","v","material","k","digest","strong","hashHex","input","subtle","bytes","buf","fnv1a128","base","h","i","GLOBAL_NAME","STANDALONE_VERSION","readConfig","el","qs","safe","ds","currentScript","d","doc","compute","cfg","r","deviceFingerprint","out","install","g","cached","get","pending","api","entry","flush","queue","method","cb","fn","value","err","hasDom","install"]}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@vectoral-labs/browser",
3
+ "version": "0.1.0",
4
+ "description": "Browser-side signal collection for Vectoral: device fingerprint, automation tells, and form-fill telemetry.",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs"
15
+ },
16
+ "./standalone": "./dist/vectoral-fingerprint.js"
17
+ },
18
+ "sideEffects": false,
19
+ "files": [
20
+ "dist",
21
+ "LICENSE"
22
+ ],
23
+ "scripts": {
24
+ "build": "rm -rf dist && tsup",
25
+ "typecheck": "tsc --noEmit",
26
+ "prepublishOnly": "npm run build"
27
+ },
28
+ "author": "Vectoral Labs, Inc.",
29
+ "homepage": "https://github.com/vectoral-labs/sdk/tree/main/typescript/packages/browser#readme",
30
+ "bugs": {
31
+ "url": "https://github.com/vectoral-labs/sdk/issues"
32
+ },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/vectoral-labs/sdk.git",
36
+ "directory": "typescript/packages/browser"
37
+ },
38
+ "engines": {
39
+ "node": ">=20"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public",
43
+ "tag": "beta"
44
+ }
45
+ }