@prosopo/fingerprint 2.6.28 → 2.7.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/proof.js ADDED
@@ -0,0 +1,84 @@
1
+ import { hexHash } from "@prosopo/util-crypto";
2
+ import { buildMerkleLayers, merkleRootFromLayers, merkleProofFromLayers, verifyMerkleProof } from "./merkle.js";
3
+ const FINGERPRINT_PROOF_VERSION = 1;
4
+ const FINGERPRINT_DISCLOSURE_KEYS = [
5
+ "colorDepth",
6
+ "cookiesEnabled",
7
+ "sessionStorage",
8
+ "localStorage",
9
+ "timezone",
10
+ "hardwareConcurrency",
11
+ "languages",
12
+ "architecture"
13
+ ];
14
+ const escapeKey = (key) => key.replace(/([:|\\])/g, "\\$1");
15
+ const componentLeafContent = (key, component) => {
16
+ const serialized = "error" in component ? "error" : JSON.stringify(component.value);
17
+ return `${escapeKey(key)}:${serialized}`;
18
+ };
19
+ const disclosureLeafContent = (disclosure) => {
20
+ const serialized = disclosure.error ? "error" : JSON.stringify(disclosure.value);
21
+ return `${escapeKey(disclosure.key)}:${serialized}`;
22
+ };
23
+ const leafHashForContent = (content) => hexHash(content);
24
+ const buildFingerprintProofFromComponents = (components, discloseKeys) => {
25
+ const keys = Object.keys(components).sort();
26
+ if (keys.length === 0) {
27
+ throw new Error("fingerprint proof: no components to commit to");
28
+ }
29
+ const leafHashes = keys.map((key) => {
30
+ const component = components[key];
31
+ if (component === void 0) {
32
+ throw new Error(`fingerprint proof: missing component ${key}`);
33
+ }
34
+ return leafHashForContent(componentLeafContent(key, component));
35
+ });
36
+ const layers = buildMerkleLayers(leafHashes);
37
+ const root = merkleRootFromLayers(layers);
38
+ const disclose = discloseKeys ?? keys;
39
+ const disclosures = [];
40
+ for (const key of disclose) {
41
+ const index = keys.indexOf(key);
42
+ if (index === -1) {
43
+ continue;
44
+ }
45
+ const component = components[key];
46
+ const leafHash = leafHashes[index];
47
+ if (component === void 0 || leafHash === void 0) {
48
+ continue;
49
+ }
50
+ const proof = merkleProofFromLayers(layers, leafHash);
51
+ disclosures.push(
52
+ "error" in component ? { key, error: true, proof } : { key, value: component.value, proof }
53
+ );
54
+ }
55
+ return { v: FINGERPRINT_PROOF_VERSION, root, disclosures };
56
+ };
57
+ const verifyFingerprintProofStructure = (proof) => {
58
+ if (proof.v !== FINGERPRINT_PROOF_VERSION) {
59
+ return false;
60
+ }
61
+ if (typeof proof.root !== "string" || proof.disclosures.length === 0) {
62
+ return false;
63
+ }
64
+ for (const disclosure of proof.disclosures) {
65
+ const leafHash = leafHashForContent(disclosureLeafContent(disclosure));
66
+ if (!verifyMerkleProof(leafHash, disclosure.proof)) {
67
+ return false;
68
+ }
69
+ const rootLayer = disclosure.proof[disclosure.proof.length - 1];
70
+ if (rootLayer === void 0 || rootLayer[0] !== proof.root) {
71
+ return false;
72
+ }
73
+ }
74
+ return true;
75
+ };
76
+ const encodeFingerprintProof = (proof) => JSON.stringify(proof);
77
+ export {
78
+ FINGERPRINT_DISCLOSURE_KEYS,
79
+ FINGERPRINT_PROOF_VERSION,
80
+ buildFingerprintProofFromComponents,
81
+ encodeFingerprintProof,
82
+ leafHashForContent,
83
+ verifyFingerprintProofStructure
84
+ };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proof.js","sourceRoot":"","sources":["../src/proof.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=proof.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proof.test.d.ts","sourceRoot":"","sources":["../src/proof.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,78 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildFingerprintProofFromComponents, verifyFingerprintProofStructure, } from "./proof.js";
3
+ const sampleComponents = () => ({
4
+ colorDepth: { value: 24, duration: 1 },
5
+ cookiesEnabled: { value: true, duration: 1 },
6
+ hardwareConcurrency: { value: 8, duration: 1 },
7
+ timezone: { value: "UTC+1", duration: 2 },
8
+ canvas: {
9
+ value: { winding: true, geometry: "abc", text: "def" },
10
+ duration: 3,
11
+ },
12
+ languages: { value: [["en-GB"], ["en"]], duration: 1 },
13
+ broken: { error: new Error("source failed"), duration: 0 },
14
+ });
15
+ const HEX_HASH = /^0x[0-9a-f]{64}$/;
16
+ const clone = (proof) => JSON.parse(JSON.stringify(proof));
17
+ describe("fingerprint proof", () => {
18
+ it("commits to all components and discloses all by default", () => {
19
+ const proof = buildFingerprintProofFromComponents(sampleComponents());
20
+ expect(proof.root).toMatch(HEX_HASH);
21
+ expect(proof.disclosures).toHaveLength(7);
22
+ const keys = proof.disclosures.map((d) => d.key);
23
+ expect(keys).toStrictEqual([...keys].sort());
24
+ });
25
+ it("represents an errored source as an error leaf with no value", () => {
26
+ const proof = buildFingerprintProofFromComponents(sampleComponents());
27
+ const broken = proof.disclosures.find((d) => d.key === "broken");
28
+ expect(broken?.error).toBe(true);
29
+ expect(broken && "value" in broken).toBe(false);
30
+ });
31
+ it("verifies a freshly built proof", () => {
32
+ const proof = buildFingerprintProofFromComponents(sampleComponents());
33
+ expect(verifyFingerprintProofStructure(proof)).toBe(true);
34
+ });
35
+ it("verifies a selective disclosure of a subset of components", () => {
36
+ const proof = buildFingerprintProofFromComponents(sampleComponents(), [
37
+ "colorDepth",
38
+ "timezone",
39
+ ]);
40
+ expect(proof.disclosures).toHaveLength(2);
41
+ expect(verifyFingerprintProofStructure(proof)).toBe(true);
42
+ const full = buildFingerprintProofFromComponents(sampleComponents());
43
+ expect(proof.root).toBe(full.root);
44
+ });
45
+ it("skips requested keys that the fingerprint does not contain", () => {
46
+ const proof = buildFingerprintProofFromComponents(sampleComponents(), [
47
+ "colorDepth",
48
+ "doesNotExist",
49
+ ]);
50
+ expect(proof.disclosures.map((d) => d.key)).toStrictEqual(["colorDepth"]);
51
+ expect(verifyFingerprintProofStructure(proof)).toBe(true);
52
+ });
53
+ it("rejects a proof whose disclosed value was tampered with", () => {
54
+ const proof = clone(buildFingerprintProofFromComponents(sampleComponents()));
55
+ const target = proof.disclosures.find((d) => d.key === "colorDepth");
56
+ if (target === undefined) {
57
+ throw new Error("expected colorDepth disclosure");
58
+ }
59
+ target.value = 32;
60
+ expect(verifyFingerprintProofStructure(proof)).toBe(false);
61
+ });
62
+ it("rejects a proof whose root was tampered with", () => {
63
+ const proof = clone(buildFingerprintProofFromComponents(sampleComponents()));
64
+ proof.root = `0x${"0".repeat(64)}`;
65
+ expect(verifyFingerprintProofStructure(proof)).toBe(false);
66
+ });
67
+ it("rejects a disclosure carrying a non-sibling leaf's proof", () => {
68
+ const proof = clone(buildFingerprintProofFromComponents(sampleComponents()));
69
+ const broken = proof.disclosures.find((d) => d.key === "broken");
70
+ const timezone = proof.disclosures.find((d) => d.key === "timezone");
71
+ if (broken === undefined || timezone === undefined) {
72
+ throw new Error("expected broken and timezone disclosures");
73
+ }
74
+ broken.proof = timezone.proof;
75
+ expect(verifyFingerprintProofStructure(proof)).toBe(false);
76
+ });
77
+ });
78
+ //# sourceMappingURL=proof.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proof.test.js","sourceRoot":"","sources":["../src/proof.test.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prosopo/fingerprint",
3
- "version": "2.6.28",
3
+ "version": "2.7.0",
4
4
  "description": "Provider fingerprint",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -22,11 +22,13 @@
22
22
  "build:cross-env": "vite build --config vite.esm.config.ts",
23
23
  "build:tsc": "tsc --build --verbose",
24
24
  "build:cjs": "NODE_ENV=${NODE_ENV:-development}; vite build --config vite.cjs.config.ts --mode $NODE_ENV",
25
+ "test": "NODE_ENV=${NODE_ENV:-test}; npx vitest run --config ./vite.test.config.ts",
25
26
  "typecheck": "tsc --project tsconfig.types.json"
26
27
  },
27
28
  "repository": {
28
29
  "type": "git",
29
- "url": "git+ssh://git@github.com/prosopo/captcha.git"
30
+ "url": "git+https://github.com/prosopo/captcha.git",
31
+ "directory": "packages/fingerprint"
30
32
  },
31
33
  "author": "Prosopo Limited",
32
34
  "license": "Apache-2.0",
@@ -35,10 +37,12 @@
35
37
  },
36
38
  "homepage": "https://github.com/prosopo/captcha#readme",
37
39
  "dependencies": {
38
- "@fingerprintjs/fingerprintjs": "3.4.2"
40
+ "@prosopo/fingerprintjs": "5.2.0",
41
+ "@prosopo/types": "4.7.0",
42
+ "@prosopo/util-crypto": "13.5.29"
39
43
  },
40
44
  "devDependencies": {
41
- "@prosopo/config": "3.3.0",
45
+ "@prosopo/config": "3.3.2",
42
46
  "@types/node": "22.10.2",
43
47
  "@vitest/coverage-v8": "3.2.4",
44
48
  "concurrently": "9.0.1",
package/src/index.ts ADDED
@@ -0,0 +1,105 @@
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 {
17
+ type FingerprintProof,
18
+ buildFingerprintProofFromComponents,
19
+ } from "./proof.js";
20
+
21
+ // webGlExtensions walks every supported WebGL extension + shader-precision
22
+ // combination, costing 1.5-2s on real GPUs. Dropped from the visitorId hash;
23
+ // webGlBasics still covers vendor/UNMASKED_RENDERER.
24
+ const EXCLUDED_SOURCES = ["webGlExtensions"] as const;
25
+
26
+ const FingerprintJSImport = async () => import("@prosopo/fingerprintjs");
27
+
28
+ // Cache the resolved components (the expensive part) rather than just the hash,
29
+ // so the visitorId and the proof are derived from one identical run.
30
+ let componentsCache: Promise<UnknownComponents> | null = null;
31
+
32
+ const loadComponents = (): Promise<UnknownComponents> => {
33
+ if (!componentsCache) {
34
+ const pending = (async () => {
35
+ const { sources, loadSources, prepareForSources } =
36
+ await FingerprintJSImport();
37
+ await prepareForSources();
38
+ // Structural supertype of every per-source Options shape — fpjs's
39
+ // `BuiltinSourceOptions` isn't exported.
40
+ const sourceOptions: { cache: Record<string, unknown>; debug?: boolean } =
41
+ {
42
+ cache: {},
43
+ };
44
+ const getComponents = loadSources(
45
+ sources,
46
+ sourceOptions,
47
+ EXCLUDED_SOURCES,
48
+ );
49
+ return getComponents();
50
+ })();
51
+ componentsCache = pending;
52
+ // Don't cache a rejected run — let the next caller retry.
53
+ pending.catch(() => {
54
+ if (componentsCache === pending) {
55
+ componentsCache = null;
56
+ }
57
+ });
58
+ }
59
+ return componentsCache;
60
+ };
61
+
62
+ export const getFingerprint = async (): Promise<string> => {
63
+ const [{ hashComponents }, components] = await Promise.all([
64
+ FingerprintJSImport(),
65
+ loadComponents(),
66
+ ]);
67
+ return hashComponents(components);
68
+ };
69
+
70
+ /**
71
+ * Produce a proof of fingerprint: a Merkle commitment to the full component set
72
+ * plus a selective disclosure of the requested components. Callers should pass
73
+ * {@link FINGERPRINT_DISCLOSURE_KEYS} — omitting `discloseKeys` discloses every
74
+ * component, which produces a very large payload. Derived from the same
75
+ * component run as {@link getFingerprint}.
76
+ */
77
+ export const getFingerprintProof = async (
78
+ discloseKeys?: readonly string[],
79
+ ): Promise<FingerprintProof> => {
80
+ const components = await loadComponents();
81
+ return buildFingerprintProofFromComponents(components, discloseKeys);
82
+ };
83
+
84
+ export const prefetchFingerprint = (): void => {
85
+ loadComponents().catch(() => {
86
+ /* swallow — loadComponents already clears its own failed cache */
87
+ });
88
+ };
89
+
90
+ export type { FingerprintLeafDisclosure, FingerprintProof } from "./proof.js";
91
+ export {
92
+ FINGERPRINT_DISCLOSURE_KEYS,
93
+ FINGERPRINT_PROOF_VERSION,
94
+ buildFingerprintProofFromComponents,
95
+ encodeFingerprintProof,
96
+ leafHashForContent,
97
+ verifyFingerprintProofStructure,
98
+ } from "./proof.js";
99
+ export {
100
+ buildMerkleLayers,
101
+ hashMerklePair,
102
+ merkleProofFromLayers,
103
+ merkleRootFromLayers,
104
+ verifyMerkleProof,
105
+ } from "./merkle.js";
package/src/merkle.ts ADDED
@@ -0,0 +1,123 @@
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 { MerkleLeaf, MerkleProof, MerkleProofLayer } from "@prosopo/types";
16
+ import { hexHashArray } from "@prosopo/util-crypto";
17
+
18
+ // A standalone, dependency-light Merkle tree that mirrors the layout of
19
+ // @prosopo/datasets' CaptchaMerkleTree (odd nodes paired with themselves,
20
+ // parent = hexHashArray([left, right])) so the proofs use the shared
21
+ // @prosopo/types MerkleProof shape and the same blake2b-256 hash. Both the
22
+ // browser client and the (closed-source) provider-side validator can rely on
23
+ // the identical pure-JS hashing here, with no WASM and no implementation drift.
24
+
25
+ const at = <T>(arr: readonly T[], index: number): T => {
26
+ const value = arr[index];
27
+ if (value === undefined) {
28
+ throw new Error(
29
+ `merkle: index ${index} out of range (length ${arr.length})`,
30
+ );
31
+ }
32
+ return value;
33
+ };
34
+
35
+ /** Hash a pair of child hashes into their parent. */
36
+ export const hashMerklePair = (left: string, right: string): string =>
37
+ hexHashArray([left, right]);
38
+
39
+ /**
40
+ * Build every layer of the Merkle tree, bottom-up. Layer 0 is the leaf hashes,
41
+ * the final layer is `[root]`. An odd node at the end of a layer is paired with
42
+ * itself.
43
+ */
44
+ export const buildMerkleLayers = (leafHashes: string[]): string[][] => {
45
+ if (leafHashes.length === 0) {
46
+ throw new Error("merkle: cannot build a tree with no leaves");
47
+ }
48
+ const layers: string[][] = [[...leafHashes]];
49
+ let current: string[] = leafHashes;
50
+ while (current.length > 1) {
51
+ const next: string[] = [];
52
+ for (let i = 0; i < current.length; i += 2) {
53
+ const left = at(current, i);
54
+ const right = i + 1 < current.length ? at(current, i + 1) : left;
55
+ next.push(hashMerklePair(left, right));
56
+ }
57
+ layers.push(next);
58
+ current = next;
59
+ }
60
+ return layers;
61
+ };
62
+
63
+ export const merkleRootFromLayers = (layers: string[][]): string =>
64
+ at(at(layers, layers.length - 1), 0);
65
+
66
+ /** Produce the sibling path proving `leafHash` belongs to the tree. */
67
+ export const merkleProofFromLayers = (
68
+ layers: string[][],
69
+ leafHash: MerkleLeaf,
70
+ ): MerkleProof => {
71
+ const proof: MerkleProofLayer[] = [];
72
+ let hash = leafHash;
73
+ for (let layerNum = 0; layerNum < layers.length - 1; layerNum++) {
74
+ const layer = at(layers, layerNum);
75
+ const index = layer.indexOf(hash);
76
+ if (index === -1) {
77
+ throw new Error("merkle: leaf hash not present in layer");
78
+ }
79
+ let partnerIndex = index % 2 === 1 ? index - 1 : index + 1;
80
+ if (partnerIndex > layer.length - 1) {
81
+ // odd node at the end of the layer is paired with itself
82
+ partnerIndex = index;
83
+ }
84
+ const partner = at(layer, partnerIndex);
85
+ const pair: MerkleProofLayer =
86
+ partnerIndex > index ? [hash, partner] : [partner, hash];
87
+ proof.push(pair);
88
+ hash = hashMerklePair(pair[0], pair[1]);
89
+ }
90
+ return [...proof, [merkleRootFromLayers(layers)]];
91
+ };
92
+
93
+ /** Verify a leaf + sibling path recomputes to the proof's embedded root. */
94
+ export const verifyMerkleProof = (
95
+ leafHash: MerkleLeaf,
96
+ proof: MerkleProof,
97
+ ): boolean => {
98
+ try {
99
+ const firstLayer = proof[0];
100
+ if (firstLayer === undefined || firstLayer.indexOf(leafHash) === -1) {
101
+ return false;
102
+ }
103
+ // Single-leaf tree: the only layer is `[root]` and the leaf is the root.
104
+ if (proof.length === 1) {
105
+ return at(firstLayer, 0) === leafHash;
106
+ }
107
+ const root = at(at(proof, proof.length - 1), 0);
108
+ let hash = leafHash;
109
+ for (let i = 0; i < proof.length - 1; i++) {
110
+ const layer = at(proof, i);
111
+ if (layer.indexOf(hash) === -1) {
112
+ return false;
113
+ }
114
+ hash = hashMerklePair(at(layer, 0), at(layer, 1));
115
+ if (at(proof, i + 1).indexOf(hash) === -1) {
116
+ return false;
117
+ }
118
+ }
119
+ return hash === root;
120
+ } catch {
121
+ return false;
122
+ }
123
+ };
@@ -0,0 +1,119 @@
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
+ });