@prosopo/fingerprint 2.7.57 → 2.7.58

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/src/proof.test.ts DELETED
@@ -1,119 +0,0 @@
1
- // Copyright 2021-2026 Prosopo (UK) Ltd.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- import type { UnknownComponents } from "@prosopo/fingerprintjs";
16
- import { describe, expect, it } from "vitest";
17
- import {
18
- type FingerprintProof,
19
- buildFingerprintProofFromComponents,
20
- verifyFingerprintProofStructure,
21
- } from "./proof.js";
22
-
23
- const sampleComponents = (): UnknownComponents => ({
24
- colorDepth: { value: 24, duration: 1 },
25
- cookiesEnabled: { value: true, duration: 1 },
26
- hardwareConcurrency: { value: 8, duration: 1 },
27
- timezone: { value: "UTC+1", duration: 2 },
28
- canvas: {
29
- value: { winding: true, geometry: "abc", text: "def" },
30
- duration: 3,
31
- },
32
- languages: { value: [["en-GB"], ["en"]], duration: 1 },
33
- broken: { error: new Error("source failed"), duration: 0 },
34
- });
35
-
36
- const HEX_HASH = /^0x[0-9a-f]{64}$/;
37
-
38
- const clone = (proof: FingerprintProof): FingerprintProof =>
39
- JSON.parse(JSON.stringify(proof)) as FingerprintProof;
40
-
41
- describe("fingerprint proof", () => {
42
- it("commits to all components and discloses all by default", () => {
43
- const proof = buildFingerprintProofFromComponents(sampleComponents());
44
- expect(proof.root).toMatch(HEX_HASH);
45
- expect(proof.disclosures).toHaveLength(7);
46
- const keys = proof.disclosures.map((d) => d.key);
47
- // disclosures follow the sorted commitment order
48
- expect(keys).toStrictEqual([...keys].sort());
49
- });
50
-
51
- it("represents an errored source as an error leaf with no value", () => {
52
- const proof = buildFingerprintProofFromComponents(sampleComponents());
53
- const broken = proof.disclosures.find((d) => d.key === "broken");
54
- expect(broken?.error).toBe(true);
55
- expect(broken && "value" in broken).toBe(false);
56
- });
57
-
58
- it("verifies a freshly built proof", () => {
59
- const proof = buildFingerprintProofFromComponents(sampleComponents());
60
- expect(verifyFingerprintProofStructure(proof)).toBe(true);
61
- });
62
-
63
- it("verifies a selective disclosure of a subset of components", () => {
64
- const proof = buildFingerprintProofFromComponents(sampleComponents(), [
65
- "colorDepth",
66
- "timezone",
67
- ]);
68
- expect(proof.disclosures).toHaveLength(2);
69
- expect(verifyFingerprintProofStructure(proof)).toBe(true);
70
- // the root still commits to every component, not just the disclosed ones
71
- const full = buildFingerprintProofFromComponents(sampleComponents());
72
- expect(proof.root).toBe(full.root);
73
- });
74
-
75
- it("skips requested keys that the fingerprint does not contain", () => {
76
- const proof = buildFingerprintProofFromComponents(sampleComponents(), [
77
- "colorDepth",
78
- "doesNotExist",
79
- ]);
80
- expect(proof.disclosures.map((d) => d.key)).toStrictEqual(["colorDepth"]);
81
- expect(verifyFingerprintProofStructure(proof)).toBe(true);
82
- });
83
-
84
- it("rejects a proof whose disclosed value was tampered with", () => {
85
- const proof = clone(
86
- buildFingerprintProofFromComponents(sampleComponents()),
87
- );
88
- const target = proof.disclosures.find((d) => d.key === "colorDepth");
89
- if (target === undefined) {
90
- throw new Error("expected colorDepth disclosure");
91
- }
92
- target.value = 32;
93
- expect(verifyFingerprintProofStructure(proof)).toBe(false);
94
- });
95
-
96
- it("rejects a proof whose root was tampered with", () => {
97
- const proof = clone(
98
- buildFingerprintProofFromComponents(sampleComponents()),
99
- );
100
- proof.root = `0x${"0".repeat(64)}`;
101
- expect(verifyFingerprintProofStructure(proof)).toBe(false);
102
- });
103
-
104
- it("rejects a disclosure carrying a non-sibling leaf's proof", () => {
105
- const proof = clone(
106
- buildFingerprintProofFromComponents(sampleComponents()),
107
- );
108
- // `broken` and `timezone` are the first and last leaves in sorted order, so
109
- // they are not Merkle siblings — `broken`'s leaf does not appear anywhere in
110
- // `timezone`'s proof, so lending it that proof must fail verification.
111
- const broken = proof.disclosures.find((d) => d.key === "broken");
112
- const timezone = proof.disclosures.find((d) => d.key === "timezone");
113
- if (broken === undefined || timezone === undefined) {
114
- throw new Error("expected broken and timezone disclosures");
115
- }
116
- broken.proof = timezone.proof;
117
- expect(verifyFingerprintProofStructure(proof)).toBe(false);
118
- });
119
- });
package/src/proof.ts DELETED
@@ -1,182 +0,0 @@
1
- // Copyright 2021-2026 Prosopo (UK) Ltd.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- import type { Component, UnknownComponents } from "@prosopo/fingerprintjs";
16
- import type { MerkleProof } from "@prosopo/types";
17
- import { hexHash } from "@prosopo/util-crypto";
18
- import {
19
- buildMerkleLayers,
20
- merkleProofFromLayers,
21
- merkleRootFromLayers,
22
- verifyMerkleProof,
23
- } from "./merkle.js";
24
-
25
- export const FINGERPRINT_PROOF_VERSION = 1;
26
-
27
- /**
28
- * The fixed set of components the client discloses in a proof of fingerprint.
29
- * Disclosing every component (the previous default) made the proof enormous —
30
- * each disclosure carries a full Merkle path and its raw value, and components
31
- * like webgl/audio/fonts have very large values. Selective disclosure keeps the
32
- * commitment (`root`) over the whole component set while only these cheap,
33
- * stable scalar components carry a path + value, shrinking the payload ~10x.
34
- *
35
- * The provider-side validator's bounds MUST be a subset of these keys: a bounded
36
- * key that isn't disclosed would always fail validation as `missing:<key>`.
37
- */
38
- export const FINGERPRINT_DISCLOSURE_KEYS: readonly string[] = [
39
- "colorDepth",
40
- "cookiesEnabled",
41
- "sessionStorage",
42
- "localStorage",
43
- "timezone",
44
- "hardwareConcurrency",
45
- "languages",
46
- "architecture",
47
- ];
48
-
49
- /**
50
- * A single disclosed fingerprint component: its raw value (or an error flag)
51
- * together with the Merkle proof that it belongs to the committed root. The
52
- * value is left as `unknown` because each FingerprintJS source produces a
53
- * different shape (string / number / boolean / array / object); the proof
54
- * validator is responsible for bounding it.
55
- */
56
- export interface FingerprintLeafDisclosure {
57
- key: string;
58
- value?: unknown;
59
- error?: boolean;
60
- proof: MerkleProof;
61
- }
62
-
63
- /**
64
- * A proof of fingerprint: a commitment (`root`) to the whole, deterministically
65
- * ordered set of FingerprintJS components, plus a selective disclosure of some
66
- * of those components. It lets a verifier check that the disclosed values were
67
- * part of one coherent, genuinely-constructed fingerprint without the client
68
- * being able to mix-and-match values across requests.
69
- */
70
- export interface FingerprintProof {
71
- v: number;
72
- root: string;
73
- disclosures: FingerprintLeafDisclosure[];
74
- }
75
-
76
- // Escape a component key exactly as FingerprintJS does in its canonical string,
77
- // so a leaf can never be forged by shifting a `:`/`|` between key and value.
78
- const escapeKey = (key: string): string => key.replace(/([:|\\])/g, "\\$1");
79
-
80
- // Canonical per-component leaf content. Mirrors one segment of FingerprintJS'
81
- // `componentsToCanonicalString`, so the leaf commits to exactly the value the
82
- // visitorId hash is derived from.
83
- const componentLeafContent = (
84
- key: string,
85
- component: Component<unknown>,
86
- ): string => {
87
- const serialized =
88
- "error" in component ? "error" : JSON.stringify(component.value);
89
- return `${escapeKey(key)}:${serialized}`;
90
- };
91
-
92
- const disclosureLeafContent = (
93
- disclosure: FingerprintLeafDisclosure,
94
- ): string => {
95
- const serialized = disclosure.error
96
- ? "error"
97
- : JSON.stringify(disclosure.value);
98
- return `${escapeKey(disclosure.key)}:${serialized}`;
99
- };
100
-
101
- export const leafHashForContent = (content: string): string => hexHash(content);
102
-
103
- /**
104
- * Build a {@link FingerprintProof} from an already-resolved FingerprintJS
105
- * components map. The commitment always covers every component (in sorted key
106
- * order); `discloseKeys` selects which components are revealed (defaulting to
107
- * all). Keys that are not present in the fingerprint are skipped — a validator
108
- * should treat an expected-but-absent disclosure as a failure.
109
- */
110
- export const buildFingerprintProofFromComponents = (
111
- components: UnknownComponents,
112
- discloseKeys?: readonly string[],
113
- ): FingerprintProof => {
114
- const keys = Object.keys(components).sort();
115
- if (keys.length === 0) {
116
- throw new Error("fingerprint proof: no components to commit to");
117
- }
118
-
119
- const leafHashes = keys.map((key) => {
120
- const component = components[key];
121
- if (component === undefined) {
122
- throw new Error(`fingerprint proof: missing component ${key}`);
123
- }
124
- return leafHashForContent(componentLeafContent(key, component));
125
- });
126
-
127
- const layers = buildMerkleLayers(leafHashes);
128
- const root = merkleRootFromLayers(layers);
129
-
130
- const disclose = discloseKeys ?? keys;
131
- const disclosures: FingerprintLeafDisclosure[] = [];
132
- for (const key of disclose) {
133
- const index = keys.indexOf(key);
134
- if (index === -1) {
135
- continue;
136
- }
137
- const component = components[key];
138
- const leafHash = leafHashes[index];
139
- if (component === undefined || leafHash === undefined) {
140
- continue;
141
- }
142
- const proof = merkleProofFromLayers(layers, leafHash);
143
- disclosures.push(
144
- "error" in component
145
- ? { key, error: true, proof }
146
- : { key, value: component.value, proof },
147
- );
148
- }
149
-
150
- return { v: FINGERPRINT_PROOF_VERSION, root, disclosures };
151
- };
152
-
153
- /**
154
- * Structural verification of a proof: every disclosed `(key, value)` re-hashes
155
- * to a leaf that is proven to belong to the single committed `root`. This is
156
- * the non-secret half of validation — the value-bounding rules live in the
157
- * provider-side (closed-source) validator and are layered on top of this.
158
- */
159
- export const verifyFingerprintProofStructure = (
160
- proof: FingerprintProof,
161
- ): boolean => {
162
- if (proof.v !== FINGERPRINT_PROOF_VERSION) {
163
- return false;
164
- }
165
- if (typeof proof.root !== "string" || proof.disclosures.length === 0) {
166
- return false;
167
- }
168
- for (const disclosure of proof.disclosures) {
169
- const leafHash = leafHashForContent(disclosureLeafContent(disclosure));
170
- if (!verifyMerkleProof(leafHash, disclosure.proof)) {
171
- return false;
172
- }
173
- const rootLayer = disclosure.proof[disclosure.proof.length - 1];
174
- if (rootLayer === undefined || rootLayer[0] !== proof.root) {
175
- return false;
176
- }
177
- }
178
- return true;
179
- };
180
-
181
- export const encodeFingerprintProof = (proof: FingerprintProof): string =>
182
- JSON.stringify(proof);
package/tsconfig.cjs.json DELETED
@@ -1,21 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.cjs.json",
3
- "compilerOptions": {
4
- "rootDir": "./src",
5
- "outDir": "./dist/cjs"
6
- },
7
- "include": [
8
- "./src/**/*.ts",
9
- "./src/**/*.json",
10
- "./src/**/*.d.ts",
11
- "./src/**/*.tsx"
12
- ],
13
- "references": [
14
- {
15
- "path": "../../dev/config/tsconfig.cjs.json"
16
- },
17
- {
18
- "path": "../fingerprintjs"
19
- }
20
- ]
21
- }
package/tsconfig.json DELETED
@@ -1,28 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.esm.json",
3
- "compilerOptions": {
4
- "rootDir": "./src",
5
- "outDir": "./dist"
6
- },
7
- "include": [
8
- "src",
9
- "src/**/*.json",
10
- "src/**/*.ts",
11
- "src/**/*.tsx",
12
- "src/**/*.d.ts"
13
- ],
14
- "references": [
15
- {
16
- "path": "../../dev/config/tsconfig.json"
17
- },
18
- {
19
- "path": "../fingerprintjs"
20
- },
21
- {
22
- "path": "../types"
23
- },
24
- {
25
- "path": "../util-crypto"
26
- }
27
- ]
28
- }