@nodaro/sdk 2.25.0 → 2.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/index.cjs +191 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +380 -20
- package/dist/index.d.ts +380 -20
- package/dist/index.js +135 -12
- package/dist/index.js.map +1 -1
- package/dist/signup-signals.cjs +88 -0
- package/dist/signup-signals.cjs.map +1 -0
- package/dist/signup-signals.d.cts +55 -0
- package/dist/signup-signals.d.ts +55 -0
- package/dist/signup-signals.js +82 -0
- package/dist/signup-signals.js.map +1 -0
- package/package.json +14 -3
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/signup-signals.ts
|
|
4
|
+
var DEVICE_KEY_VERSION = "v1";
|
|
5
|
+
var SIGNUP_SIGNALS_TIMEOUT_MS = 3e3;
|
|
6
|
+
async function sha256Hex(value) {
|
|
7
|
+
try {
|
|
8
|
+
const subtle = globalThis.crypto?.subtle;
|
|
9
|
+
if (!subtle) return null;
|
|
10
|
+
const digest = await subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
11
|
+
return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function webglRenderer() {
|
|
17
|
+
try {
|
|
18
|
+
const canvas = document.createElement("canvas");
|
|
19
|
+
const gl = canvas.getContext("webgl") ?? canvas.getContext("experimental-webgl");
|
|
20
|
+
if (!gl) return "";
|
|
21
|
+
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
|
|
22
|
+
const renderer = debugInfo ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER);
|
|
23
|
+
return typeof renderer === "string" ? renderer : "";
|
|
24
|
+
} catch {
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function canonicalDeviceString() {
|
|
29
|
+
const nav = navigator;
|
|
30
|
+
return [
|
|
31
|
+
DEVICE_KEY_VERSION,
|
|
32
|
+
`gl=${webglRenderer()}`,
|
|
33
|
+
`hc=${nav.hardwareConcurrency ?? ""}`,
|
|
34
|
+
`dm=${nav.deviceMemory ?? ""}`,
|
|
35
|
+
`sw=${screen.width ?? ""}`,
|
|
36
|
+
`sh=${screen.height ?? ""}`,
|
|
37
|
+
`cd=${screen.colorDepth ?? ""}`,
|
|
38
|
+
`dpr=${window.devicePixelRatio ?? ""}`,
|
|
39
|
+
`pf=${nav.platform ?? ""}`,
|
|
40
|
+
`tz=${Intl.DateTimeFormat().resolvedOptions().timeZone ?? ""}`
|
|
41
|
+
].join("|");
|
|
42
|
+
}
|
|
43
|
+
async function computeDeviceKey() {
|
|
44
|
+
try {
|
|
45
|
+
return await sha256Hex(canonicalDeviceString());
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function computeBrowserKey() {
|
|
51
|
+
try {
|
|
52
|
+
const { load } = await import('@fingerprintjs/fingerprintjs');
|
|
53
|
+
const agent = await load({ monitoring: false });
|
|
54
|
+
const { visitorId } = await agent.get();
|
|
55
|
+
return visitorId ? await sha256Hex(visitorId) : null;
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function collectSignupKeys(timeoutMs = SIGNUP_SIGNALS_TIMEOUT_MS) {
|
|
61
|
+
const keys = {};
|
|
62
|
+
let timer;
|
|
63
|
+
const settled = Promise.allSettled([
|
|
64
|
+
computeBrowserKey().then((key) => {
|
|
65
|
+
if (key) keys.browserKey = key;
|
|
66
|
+
}),
|
|
67
|
+
computeDeviceKey().then((key) => {
|
|
68
|
+
if (key) keys.deviceKey = key;
|
|
69
|
+
})
|
|
70
|
+
]);
|
|
71
|
+
const deadline = new Promise((resolve) => {
|
|
72
|
+
timer = setTimeout(resolve, timeoutMs);
|
|
73
|
+
});
|
|
74
|
+
try {
|
|
75
|
+
await Promise.race([settled, deadline]);
|
|
76
|
+
} finally {
|
|
77
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
78
|
+
}
|
|
79
|
+
return { ...keys };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
exports.SIGNUP_SIGNALS_TIMEOUT_MS = SIGNUP_SIGNALS_TIMEOUT_MS;
|
|
83
|
+
exports.collectSignupKeys = collectSignupKeys;
|
|
84
|
+
exports.computeBrowserKey = computeBrowserKey;
|
|
85
|
+
exports.computeDeviceKey = computeDeviceKey;
|
|
86
|
+
exports.sha256Hex = sha256Hex;
|
|
87
|
+
//# sourceMappingURL=signup-signals.cjs.map
|
|
88
|
+
//# sourceMappingURL=signup-signals.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/signup-signals.ts"],"names":[],"mappings":"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","file":"signup-signals.cjs","sourcesContent":["/**\n * The two browser signals a Nodaro app reports when it claims the free\n * signup grant (`client.credits.claimSignupGrant`) — a browser-level key\n * and a device-level key. This is the ONE implementation every Nodaro\n * surface uses, so a browser seen on one of them is recognised on all: the\n * keys are compared server-side, and two surfaces computing them differently\n * would silently stop matching each other.\n *\n * Browser-only (`document`, `screen`, `crypto.subtle`), hence its own\n * subpath (`@nodaro/sdk/signup-signals`) beside `@nodaro/sdk/supabase` —\n * the package's main entry stays Node-safe.\n *\n * Everything here fails open, on purpose. A key is a SIGNAL the server\n * scores, never a precondition: a browser that refuses to be measured must\n * still reach the claim. So nothing throws, nothing blocks past its\n * deadline, nothing is stored locally, no cookie is read, and no raw\n * component ever leaves the browser — only the digests.\n *\n * The browser key needs `@fingerprintjs/fingerprintjs` (an optional peer\n * dependency, loaded on demand); without it only the device key is sent.\n */\n\n/** Bump when the canonical shape changes — old and new keys must not collide. */\nconst DEVICE_KEY_VERSION = \"v1\"\n\n/** FingerprintJS probes canvas/audio/WebGL; first paint must not wait on it. */\nexport const SIGNUP_SIGNALS_TIMEOUT_MS = 3000\n\n/** What a claim carries. Each key is a SHA-256 hex digest, absent when it could not be computed. */\nexport interface SignupKeys {\n browserKey?: string\n deviceKey?: string\n}\n\n/** SHA-256 as lowercase hex, or null where SubtleCrypto is unavailable. */\nexport async function sha256Hex(value: string): Promise<string | null> {\n try {\n const subtle = globalThis.crypto?.subtle\n if (!subtle) return null\n const digest = await subtle.digest(\"SHA-256\", new TextEncoder().encode(value))\n return Array.from(new Uint8Array(digest))\n .map((byte) => byte.toString(16).padStart(2, \"0\"))\n .join(\"\")\n } catch {\n return null\n }\n}\n\n/**\n * The GPU string, which is the highest-entropy hardware attribute available.\n * Guarded on its own rather than with the rest: a null context is the NORMAL\n * case on a hardened browser, and it must cost only this one field instead of\n * the whole key. `WEBGL_debug_renderer_info` is being retired, so the masked\n * RENDERER is the fallback.\n */\nfunction webglRenderer(): string {\n try {\n const canvas = document.createElement(\"canvas\")\n const gl = (canvas.getContext(\"webgl\") ??\n canvas.getContext(\"experimental-webgl\")) as WebGLRenderingContext | null\n if (!gl) return \"\"\n const debugInfo = gl.getExtension(\"WEBGL_debug_renderer_info\")\n const renderer = debugInfo\n ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL)\n : gl.getParameter(gl.RENDERER)\n return typeof renderer === \"string\" ? renderer : \"\"\n } catch {\n return \"\"\n }\n}\n\n/**\n * Fixed order, one labelled field each, empty where an attribute is absent —\n * so a browser that hides two of them still produces a stable key, and adding\n * a field later cannot silently re-map an existing one.\n */\nfunction canonicalDeviceString(): string {\n const nav = navigator as Navigator & { deviceMemory?: number }\n return [\n DEVICE_KEY_VERSION,\n `gl=${webglRenderer()}`,\n `hc=${nav.hardwareConcurrency ?? \"\"}`,\n `dm=${nav.deviceMemory ?? \"\"}`,\n `sw=${screen.width ?? \"\"}`,\n `sh=${screen.height ?? \"\"}`,\n `cd=${screen.colorDepth ?? \"\"}`,\n `dpr=${window.devicePixelRatio ?? \"\"}`,\n `pf=${nav.platform ?? \"\"}`,\n `tz=${Intl.DateTimeFormat().resolvedOptions().timeZone ?? \"\"}`,\n ].join(\"|\")\n}\n\n/**\n * The per-DEVICE key: a SHA-256 over hardware-shaped attributes that survive\n * a new browser profile, an incognito window, or a cleared origin. It is\n * deliberately coarse and deliberately NOT unique — a whole model line of\n * laptop shares one. Null when it cannot be computed for any reason.\n */\nexport async function computeDeviceKey(): Promise<string | null> {\n try {\n return await sha256Hex(canonicalDeviceString())\n } catch {\n return null\n }\n}\n\n/**\n * The browser-level key. FingerprintJS contributes software entropy the\n * hardware-only device key cannot see (fonts, codecs, engine quirks), which\n * is what makes two keys worth having.\n *\n * Its `visitorId` is a 128-bit murmur hash — 32 hex characters — and the\n * server stores a key only when it is a SHA-256 hex, so it is re-hashed\n * here. Null when the agent is not installed, cannot load, or yields nothing.\n */\nexport async function computeBrowserKey(): Promise<string | null> {\n try {\n // Dynamic so the ~37 KB agent is a chunk nobody downloads on a boot that\n // has nothing to claim — and so a consumer without it still builds.\n const { load } = await import(\"@fingerprintjs/fingerprintjs\")\n // `monitoring: false` is mandatory: it disables FingerprintJS's own\n // install-stats beacon to m1.openfpcdn.io.\n const agent = await load({ monitoring: false })\n const { visitorId } = await agent.get()\n return visitorId ? await sha256Hex(visitorId) : null\n } catch {\n return null\n }\n}\n\n/**\n * Both keys, concurrently, under one deadline. Each writes itself on\n * success, so a deadline reached while one is still running still sends the\n * other — and a browser that yields neither sends `{}`.\n */\nexport async function collectSignupKeys(timeoutMs: number = SIGNUP_SIGNALS_TIMEOUT_MS): Promise<SignupKeys> {\n const keys: SignupKeys = {}\n let timer: ReturnType<typeof setTimeout> | undefined\n const settled = Promise.allSettled([\n computeBrowserKey().then((key) => {\n if (key) keys.browserKey = key\n }),\n computeDeviceKey().then((key) => {\n if (key) keys.deviceKey = key\n }),\n ])\n const deadline = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, timeoutMs)\n })\n try {\n await Promise.race([settled, deadline])\n } finally {\n if (timer !== undefined) clearTimeout(timer)\n }\n return { ...keys }\n}\n"]}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two browser signals a Nodaro app reports when it claims the free
|
|
3
|
+
* signup grant (`client.credits.claimSignupGrant`) — a browser-level key
|
|
4
|
+
* and a device-level key. This is the ONE implementation every Nodaro
|
|
5
|
+
* surface uses, so a browser seen on one of them is recognised on all: the
|
|
6
|
+
* keys are compared server-side, and two surfaces computing them differently
|
|
7
|
+
* would silently stop matching each other.
|
|
8
|
+
*
|
|
9
|
+
* Browser-only (`document`, `screen`, `crypto.subtle`), hence its own
|
|
10
|
+
* subpath (`@nodaro/sdk/signup-signals`) beside `@nodaro/sdk/supabase` —
|
|
11
|
+
* the package's main entry stays Node-safe.
|
|
12
|
+
*
|
|
13
|
+
* Everything here fails open, on purpose. A key is a SIGNAL the server
|
|
14
|
+
* scores, never a precondition: a browser that refuses to be measured must
|
|
15
|
+
* still reach the claim. So nothing throws, nothing blocks past its
|
|
16
|
+
* deadline, nothing is stored locally, no cookie is read, and no raw
|
|
17
|
+
* component ever leaves the browser — only the digests.
|
|
18
|
+
*
|
|
19
|
+
* The browser key needs `@fingerprintjs/fingerprintjs` (an optional peer
|
|
20
|
+
* dependency, loaded on demand); without it only the device key is sent.
|
|
21
|
+
*/
|
|
22
|
+
/** FingerprintJS probes canvas/audio/WebGL; first paint must not wait on it. */
|
|
23
|
+
declare const SIGNUP_SIGNALS_TIMEOUT_MS = 3000;
|
|
24
|
+
/** What a claim carries. Each key is a SHA-256 hex digest, absent when it could not be computed. */
|
|
25
|
+
interface SignupKeys {
|
|
26
|
+
browserKey?: string;
|
|
27
|
+
deviceKey?: string;
|
|
28
|
+
}
|
|
29
|
+
/** SHA-256 as lowercase hex, or null where SubtleCrypto is unavailable. */
|
|
30
|
+
declare function sha256Hex(value: string): Promise<string | null>;
|
|
31
|
+
/**
|
|
32
|
+
* The per-DEVICE key: a SHA-256 over hardware-shaped attributes that survive
|
|
33
|
+
* a new browser profile, an incognito window, or a cleared origin. It is
|
|
34
|
+
* deliberately coarse and deliberately NOT unique — a whole model line of
|
|
35
|
+
* laptop shares one. Null when it cannot be computed for any reason.
|
|
36
|
+
*/
|
|
37
|
+
declare function computeDeviceKey(): Promise<string | null>;
|
|
38
|
+
/**
|
|
39
|
+
* The browser-level key. FingerprintJS contributes software entropy the
|
|
40
|
+
* hardware-only device key cannot see (fonts, codecs, engine quirks), which
|
|
41
|
+
* is what makes two keys worth having.
|
|
42
|
+
*
|
|
43
|
+
* Its `visitorId` is a 128-bit murmur hash — 32 hex characters — and the
|
|
44
|
+
* server stores a key only when it is a SHA-256 hex, so it is re-hashed
|
|
45
|
+
* here. Null when the agent is not installed, cannot load, or yields nothing.
|
|
46
|
+
*/
|
|
47
|
+
declare function computeBrowserKey(): Promise<string | null>;
|
|
48
|
+
/**
|
|
49
|
+
* Both keys, concurrently, under one deadline. Each writes itself on
|
|
50
|
+
* success, so a deadline reached while one is still running still sends the
|
|
51
|
+
* other — and a browser that yields neither sends `{}`.
|
|
52
|
+
*/
|
|
53
|
+
declare function collectSignupKeys(timeoutMs?: number): Promise<SignupKeys>;
|
|
54
|
+
|
|
55
|
+
export { SIGNUP_SIGNALS_TIMEOUT_MS, type SignupKeys, collectSignupKeys, computeBrowserKey, computeDeviceKey, sha256Hex };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two browser signals a Nodaro app reports when it claims the free
|
|
3
|
+
* signup grant (`client.credits.claimSignupGrant`) — a browser-level key
|
|
4
|
+
* and a device-level key. This is the ONE implementation every Nodaro
|
|
5
|
+
* surface uses, so a browser seen on one of them is recognised on all: the
|
|
6
|
+
* keys are compared server-side, and two surfaces computing them differently
|
|
7
|
+
* would silently stop matching each other.
|
|
8
|
+
*
|
|
9
|
+
* Browser-only (`document`, `screen`, `crypto.subtle`), hence its own
|
|
10
|
+
* subpath (`@nodaro/sdk/signup-signals`) beside `@nodaro/sdk/supabase` —
|
|
11
|
+
* the package's main entry stays Node-safe.
|
|
12
|
+
*
|
|
13
|
+
* Everything here fails open, on purpose. A key is a SIGNAL the server
|
|
14
|
+
* scores, never a precondition: a browser that refuses to be measured must
|
|
15
|
+
* still reach the claim. So nothing throws, nothing blocks past its
|
|
16
|
+
* deadline, nothing is stored locally, no cookie is read, and no raw
|
|
17
|
+
* component ever leaves the browser — only the digests.
|
|
18
|
+
*
|
|
19
|
+
* The browser key needs `@fingerprintjs/fingerprintjs` (an optional peer
|
|
20
|
+
* dependency, loaded on demand); without it only the device key is sent.
|
|
21
|
+
*/
|
|
22
|
+
/** FingerprintJS probes canvas/audio/WebGL; first paint must not wait on it. */
|
|
23
|
+
declare const SIGNUP_SIGNALS_TIMEOUT_MS = 3000;
|
|
24
|
+
/** What a claim carries. Each key is a SHA-256 hex digest, absent when it could not be computed. */
|
|
25
|
+
interface SignupKeys {
|
|
26
|
+
browserKey?: string;
|
|
27
|
+
deviceKey?: string;
|
|
28
|
+
}
|
|
29
|
+
/** SHA-256 as lowercase hex, or null where SubtleCrypto is unavailable. */
|
|
30
|
+
declare function sha256Hex(value: string): Promise<string | null>;
|
|
31
|
+
/**
|
|
32
|
+
* The per-DEVICE key: a SHA-256 over hardware-shaped attributes that survive
|
|
33
|
+
* a new browser profile, an incognito window, or a cleared origin. It is
|
|
34
|
+
* deliberately coarse and deliberately NOT unique — a whole model line of
|
|
35
|
+
* laptop shares one. Null when it cannot be computed for any reason.
|
|
36
|
+
*/
|
|
37
|
+
declare function computeDeviceKey(): Promise<string | null>;
|
|
38
|
+
/**
|
|
39
|
+
* The browser-level key. FingerprintJS contributes software entropy the
|
|
40
|
+
* hardware-only device key cannot see (fonts, codecs, engine quirks), which
|
|
41
|
+
* is what makes two keys worth having.
|
|
42
|
+
*
|
|
43
|
+
* Its `visitorId` is a 128-bit murmur hash — 32 hex characters — and the
|
|
44
|
+
* server stores a key only when it is a SHA-256 hex, so it is re-hashed
|
|
45
|
+
* here. Null when the agent is not installed, cannot load, or yields nothing.
|
|
46
|
+
*/
|
|
47
|
+
declare function computeBrowserKey(): Promise<string | null>;
|
|
48
|
+
/**
|
|
49
|
+
* Both keys, concurrently, under one deadline. Each writes itself on
|
|
50
|
+
* success, so a deadline reached while one is still running still sends the
|
|
51
|
+
* other — and a browser that yields neither sends `{}`.
|
|
52
|
+
*/
|
|
53
|
+
declare function collectSignupKeys(timeoutMs?: number): Promise<SignupKeys>;
|
|
54
|
+
|
|
55
|
+
export { SIGNUP_SIGNALS_TIMEOUT_MS, type SignupKeys, collectSignupKeys, computeBrowserKey, computeDeviceKey, sha256Hex };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// src/signup-signals.ts
|
|
2
|
+
var DEVICE_KEY_VERSION = "v1";
|
|
3
|
+
var SIGNUP_SIGNALS_TIMEOUT_MS = 3e3;
|
|
4
|
+
async function sha256Hex(value) {
|
|
5
|
+
try {
|
|
6
|
+
const subtle = globalThis.crypto?.subtle;
|
|
7
|
+
if (!subtle) return null;
|
|
8
|
+
const digest = await subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
9
|
+
return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function webglRenderer() {
|
|
15
|
+
try {
|
|
16
|
+
const canvas = document.createElement("canvas");
|
|
17
|
+
const gl = canvas.getContext("webgl") ?? canvas.getContext("experimental-webgl");
|
|
18
|
+
if (!gl) return "";
|
|
19
|
+
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
|
|
20
|
+
const renderer = debugInfo ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER);
|
|
21
|
+
return typeof renderer === "string" ? renderer : "";
|
|
22
|
+
} catch {
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function canonicalDeviceString() {
|
|
27
|
+
const nav = navigator;
|
|
28
|
+
return [
|
|
29
|
+
DEVICE_KEY_VERSION,
|
|
30
|
+
`gl=${webglRenderer()}`,
|
|
31
|
+
`hc=${nav.hardwareConcurrency ?? ""}`,
|
|
32
|
+
`dm=${nav.deviceMemory ?? ""}`,
|
|
33
|
+
`sw=${screen.width ?? ""}`,
|
|
34
|
+
`sh=${screen.height ?? ""}`,
|
|
35
|
+
`cd=${screen.colorDepth ?? ""}`,
|
|
36
|
+
`dpr=${window.devicePixelRatio ?? ""}`,
|
|
37
|
+
`pf=${nav.platform ?? ""}`,
|
|
38
|
+
`tz=${Intl.DateTimeFormat().resolvedOptions().timeZone ?? ""}`
|
|
39
|
+
].join("|");
|
|
40
|
+
}
|
|
41
|
+
async function computeDeviceKey() {
|
|
42
|
+
try {
|
|
43
|
+
return await sha256Hex(canonicalDeviceString());
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function computeBrowserKey() {
|
|
49
|
+
try {
|
|
50
|
+
const { load } = await import('@fingerprintjs/fingerprintjs');
|
|
51
|
+
const agent = await load({ monitoring: false });
|
|
52
|
+
const { visitorId } = await agent.get();
|
|
53
|
+
return visitorId ? await sha256Hex(visitorId) : null;
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function collectSignupKeys(timeoutMs = SIGNUP_SIGNALS_TIMEOUT_MS) {
|
|
59
|
+
const keys = {};
|
|
60
|
+
let timer;
|
|
61
|
+
const settled = Promise.allSettled([
|
|
62
|
+
computeBrowserKey().then((key) => {
|
|
63
|
+
if (key) keys.browserKey = key;
|
|
64
|
+
}),
|
|
65
|
+
computeDeviceKey().then((key) => {
|
|
66
|
+
if (key) keys.deviceKey = key;
|
|
67
|
+
})
|
|
68
|
+
]);
|
|
69
|
+
const deadline = new Promise((resolve) => {
|
|
70
|
+
timer = setTimeout(resolve, timeoutMs);
|
|
71
|
+
});
|
|
72
|
+
try {
|
|
73
|
+
await Promise.race([settled, deadline]);
|
|
74
|
+
} finally {
|
|
75
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
76
|
+
}
|
|
77
|
+
return { ...keys };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export { SIGNUP_SIGNALS_TIMEOUT_MS, collectSignupKeys, computeBrowserKey, computeDeviceKey, sha256Hex };
|
|
81
|
+
//# sourceMappingURL=signup-signals.js.map
|
|
82
|
+
//# sourceMappingURL=signup-signals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/signup-signals.ts"],"names":[],"mappings":"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","file":"signup-signals.js","sourcesContent":["/**\n * The two browser signals a Nodaro app reports when it claims the free\n * signup grant (`client.credits.claimSignupGrant`) — a browser-level key\n * and a device-level key. This is the ONE implementation every Nodaro\n * surface uses, so a browser seen on one of them is recognised on all: the\n * keys are compared server-side, and two surfaces computing them differently\n * would silently stop matching each other.\n *\n * Browser-only (`document`, `screen`, `crypto.subtle`), hence its own\n * subpath (`@nodaro/sdk/signup-signals`) beside `@nodaro/sdk/supabase` —\n * the package's main entry stays Node-safe.\n *\n * Everything here fails open, on purpose. A key is a SIGNAL the server\n * scores, never a precondition: a browser that refuses to be measured must\n * still reach the claim. So nothing throws, nothing blocks past its\n * deadline, nothing is stored locally, no cookie is read, and no raw\n * component ever leaves the browser — only the digests.\n *\n * The browser key needs `@fingerprintjs/fingerprintjs` (an optional peer\n * dependency, loaded on demand); without it only the device key is sent.\n */\n\n/** Bump when the canonical shape changes — old and new keys must not collide. */\nconst DEVICE_KEY_VERSION = \"v1\"\n\n/** FingerprintJS probes canvas/audio/WebGL; first paint must not wait on it. */\nexport const SIGNUP_SIGNALS_TIMEOUT_MS = 3000\n\n/** What a claim carries. Each key is a SHA-256 hex digest, absent when it could not be computed. */\nexport interface SignupKeys {\n browserKey?: string\n deviceKey?: string\n}\n\n/** SHA-256 as lowercase hex, or null where SubtleCrypto is unavailable. */\nexport async function sha256Hex(value: string): Promise<string | null> {\n try {\n const subtle = globalThis.crypto?.subtle\n if (!subtle) return null\n const digest = await subtle.digest(\"SHA-256\", new TextEncoder().encode(value))\n return Array.from(new Uint8Array(digest))\n .map((byte) => byte.toString(16).padStart(2, \"0\"))\n .join(\"\")\n } catch {\n return null\n }\n}\n\n/**\n * The GPU string, which is the highest-entropy hardware attribute available.\n * Guarded on its own rather than with the rest: a null context is the NORMAL\n * case on a hardened browser, and it must cost only this one field instead of\n * the whole key. `WEBGL_debug_renderer_info` is being retired, so the masked\n * RENDERER is the fallback.\n */\nfunction webglRenderer(): string {\n try {\n const canvas = document.createElement(\"canvas\")\n const gl = (canvas.getContext(\"webgl\") ??\n canvas.getContext(\"experimental-webgl\")) as WebGLRenderingContext | null\n if (!gl) return \"\"\n const debugInfo = gl.getExtension(\"WEBGL_debug_renderer_info\")\n const renderer = debugInfo\n ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL)\n : gl.getParameter(gl.RENDERER)\n return typeof renderer === \"string\" ? renderer : \"\"\n } catch {\n return \"\"\n }\n}\n\n/**\n * Fixed order, one labelled field each, empty where an attribute is absent —\n * so a browser that hides two of them still produces a stable key, and adding\n * a field later cannot silently re-map an existing one.\n */\nfunction canonicalDeviceString(): string {\n const nav = navigator as Navigator & { deviceMemory?: number }\n return [\n DEVICE_KEY_VERSION,\n `gl=${webglRenderer()}`,\n `hc=${nav.hardwareConcurrency ?? \"\"}`,\n `dm=${nav.deviceMemory ?? \"\"}`,\n `sw=${screen.width ?? \"\"}`,\n `sh=${screen.height ?? \"\"}`,\n `cd=${screen.colorDepth ?? \"\"}`,\n `dpr=${window.devicePixelRatio ?? \"\"}`,\n `pf=${nav.platform ?? \"\"}`,\n `tz=${Intl.DateTimeFormat().resolvedOptions().timeZone ?? \"\"}`,\n ].join(\"|\")\n}\n\n/**\n * The per-DEVICE key: a SHA-256 over hardware-shaped attributes that survive\n * a new browser profile, an incognito window, or a cleared origin. It is\n * deliberately coarse and deliberately NOT unique — a whole model line of\n * laptop shares one. Null when it cannot be computed for any reason.\n */\nexport async function computeDeviceKey(): Promise<string | null> {\n try {\n return await sha256Hex(canonicalDeviceString())\n } catch {\n return null\n }\n}\n\n/**\n * The browser-level key. FingerprintJS contributes software entropy the\n * hardware-only device key cannot see (fonts, codecs, engine quirks), which\n * is what makes two keys worth having.\n *\n * Its `visitorId` is a 128-bit murmur hash — 32 hex characters — and the\n * server stores a key only when it is a SHA-256 hex, so it is re-hashed\n * here. Null when the agent is not installed, cannot load, or yields nothing.\n */\nexport async function computeBrowserKey(): Promise<string | null> {\n try {\n // Dynamic so the ~37 KB agent is a chunk nobody downloads on a boot that\n // has nothing to claim — and so a consumer without it still builds.\n const { load } = await import(\"@fingerprintjs/fingerprintjs\")\n // `monitoring: false` is mandatory: it disables FingerprintJS's own\n // install-stats beacon to m1.openfpcdn.io.\n const agent = await load({ monitoring: false })\n const { visitorId } = await agent.get()\n return visitorId ? await sha256Hex(visitorId) : null\n } catch {\n return null\n }\n}\n\n/**\n * Both keys, concurrently, under one deadline. Each writes itself on\n * success, so a deadline reached while one is still running still sends the\n * other — and a browser that yields neither sends `{}`.\n */\nexport async function collectSignupKeys(timeoutMs: number = SIGNUP_SIGNALS_TIMEOUT_MS): Promise<SignupKeys> {\n const keys: SignupKeys = {}\n let timer: ReturnType<typeof setTimeout> | undefined\n const settled = Promise.allSettled([\n computeBrowserKey().then((key) => {\n if (key) keys.browserKey = key\n }),\n computeDeviceKey().then((key) => {\n if (key) keys.deviceKey = key\n }),\n ])\n const deadline = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, timeoutMs)\n })\n try {\n await Promise.race([settled, deadline])\n } finally {\n if (timer !== undefined) clearTimeout(timer)\n }\n return { ...keys }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodaro/sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.27.0",
|
|
4
4
|
"description": "Typed REST client for the Nodaro API — workflows, jobs, executions, apps, characters, and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -24,10 +24,16 @@
|
|
|
24
24
|
"types": "./dist/supabase-browser.d.ts",
|
|
25
25
|
"import": "./dist/supabase-browser.js",
|
|
26
26
|
"require": "./dist/supabase-browser.cjs"
|
|
27
|
+
},
|
|
28
|
+
"./signup-signals": {
|
|
29
|
+
"types": "./dist/signup-signals.d.ts",
|
|
30
|
+
"import": "./dist/signup-signals.js",
|
|
31
|
+
"require": "./dist/signup-signals.cjs"
|
|
27
32
|
}
|
|
28
33
|
},
|
|
29
34
|
"files": [
|
|
30
35
|
"dist",
|
|
36
|
+
"!dist/metafile-*.json",
|
|
31
37
|
"README.md",
|
|
32
38
|
"LICENSE"
|
|
33
39
|
],
|
|
@@ -41,14 +47,18 @@
|
|
|
41
47
|
"test:watch": "vitest"
|
|
42
48
|
},
|
|
43
49
|
"dependencies": {
|
|
44
|
-
"@nodaro/shared": "^3.
|
|
45
|
-
"@nodaro/prompts": "^1.
|
|
50
|
+
"@nodaro/shared": "^3.32.0",
|
|
51
|
+
"@nodaro/prompts": "^1.38.0"
|
|
46
52
|
},
|
|
47
53
|
"peerDependencies": {
|
|
54
|
+
"@fingerprintjs/fingerprintjs": "^5.2.0",
|
|
48
55
|
"@supabase/ssr": "^0.12.3",
|
|
49
56
|
"@supabase/supabase-js": "^2.110.5"
|
|
50
57
|
},
|
|
51
58
|
"peerDependenciesMeta": {
|
|
59
|
+
"@fingerprintjs/fingerprintjs": {
|
|
60
|
+
"optional": true
|
|
61
|
+
},
|
|
52
62
|
"@supabase/ssr": {
|
|
53
63
|
"optional": true
|
|
54
64
|
},
|
|
@@ -57,6 +67,7 @@
|
|
|
57
67
|
}
|
|
58
68
|
},
|
|
59
69
|
"devDependencies": {
|
|
70
|
+
"@fingerprintjs/fingerprintjs": "5.2.0",
|
|
60
71
|
"@vitest/coverage-v8": "^4.1.2",
|
|
61
72
|
"tsup": "^8.5.0",
|
|
62
73
|
"typescript": "^5.7.3",
|