@fide.work/fcp 0.0.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +18 -0
  2. package/dist/attestation/create.d.ts +173 -0
  3. package/dist/attestation/create.js +179 -0
  4. package/dist/attestation/index.d.ts +7 -0
  5. package/dist/attestation/index.js +7 -0
  6. package/dist/attestation/verify.d.ts +56 -0
  7. package/dist/attestation/verify.js +94 -0
  8. package/dist/broadcasting/config.d.ts +27 -0
  9. package/dist/broadcasting/config.js +22 -0
  10. package/dist/broadcasting/index.d.ts +7 -0
  11. package/dist/broadcasting/index.js +7 -0
  12. package/dist/broadcasting/registry.d.ts +111 -0
  13. package/dist/broadcasting/registry.js +99 -0
  14. package/dist/experimental/attestation/create.d.ts +173 -0
  15. package/dist/experimental/attestation/create.js +188 -0
  16. package/dist/experimental/attestation/index.d.ts +7 -0
  17. package/dist/experimental/attestation/index.js +7 -0
  18. package/dist/experimental/attestation/verify.d.ts +56 -0
  19. package/dist/experimental/attestation/verify.js +94 -0
  20. package/dist/experimental/broadcasting/config.d.ts +27 -0
  21. package/dist/experimental/broadcasting/config.js +22 -0
  22. package/dist/experimental/broadcasting/index.d.ts +7 -0
  23. package/dist/experimental/broadcasting/index.js +7 -0
  24. package/dist/experimental/broadcasting/registry.d.ts +111 -0
  25. package/dist/experimental/broadcasting/registry.js +99 -0
  26. package/dist/experimental/index.d.ts +9 -0
  27. package/dist/experimental/index.js +13 -0
  28. package/dist/experimental/merkle/index.d.ts +6 -0
  29. package/dist/experimental/merkle/index.js +6 -0
  30. package/dist/experimental/merkle/tree.d.ts +72 -0
  31. package/dist/experimental/merkle/tree.js +154 -0
  32. package/dist/experimental/signing/ed25519.d.ts +116 -0
  33. package/dist/experimental/signing/ed25519.js +161 -0
  34. package/dist/experimental/signing/eip191.d.ts +50 -0
  35. package/dist/experimental/signing/eip191.js +96 -0
  36. package/dist/experimental/signing/eip712.d.ts +112 -0
  37. package/dist/experimental/signing/eip712.js +187 -0
  38. package/dist/experimental/signing/index.d.ts +8 -0
  39. package/dist/experimental/signing/index.js +11 -0
  40. package/dist/fide-id/calculateFideId.d.ts +21 -0
  41. package/dist/fide-id/calculateFideId.js +53 -0
  42. package/dist/fide-id/calculateStatementFideId.d.ts +21 -0
  43. package/dist/fide-id/calculateStatementFideId.js +38 -0
  44. package/dist/fide-id/constants.d.ts +43 -0
  45. package/dist/fide-id/constants.js +55 -0
  46. package/dist/fide-id/index.d.ts +10 -0
  47. package/dist/fide-id/index.js +12 -0
  48. package/dist/fide-id/types.d.ts +52 -0
  49. package/dist/fide-id/types.js +5 -0
  50. package/dist/fide-id/utils.d.ts +30 -0
  51. package/dist/fide-id/utils.js +65 -0
  52. package/dist/index.d.ts +11 -0
  53. package/dist/index.js +19 -0
  54. package/dist/merkle/index.d.ts +6 -0
  55. package/dist/merkle/index.js +6 -0
  56. package/dist/merkle/tree.d.ts +72 -0
  57. package/dist/merkle/tree.js +154 -0
  58. package/dist/schema/evaluations.d.ts +31 -0
  59. package/dist/schema/evaluations.js +31 -0
  60. package/dist/schema/index.d.ts +6 -0
  61. package/dist/schema/index.js +6 -0
  62. package/dist/schema/predicates.d.ts +110 -0
  63. package/dist/schema/predicates.js +122 -0
  64. package/dist/signing/ed25519.d.ts +116 -0
  65. package/dist/signing/ed25519.js +161 -0
  66. package/dist/signing/eip191.d.ts +50 -0
  67. package/dist/signing/eip191.js +96 -0
  68. package/dist/signing/eip712.d.ts +112 -0
  69. package/dist/signing/eip712.js +187 -0
  70. package/dist/signing/index.d.ts +8 -0
  71. package/dist/signing/index.js +11 -0
  72. package/dist/statement/build.d.ts +99 -0
  73. package/dist/statement/build.js +71 -0
  74. package/dist/statement/index.d.ts +6 -0
  75. package/dist/statement/index.js +6 -0
  76. package/dist/statement/policy.d.ts +33 -0
  77. package/dist/statement/policy.js +183 -0
  78. package/package.json +64 -0
@@ -0,0 +1,94 @@
1
+ /**
2
+ * FCP SDK Attestation Verification Utilities
3
+ *
4
+ * Full verification functions that combine Merkle proof and signature verification.
5
+ */
6
+ import { verifyStatementInAttestation, parseAttestationData } from "./create.js";
7
+ import { verifyEd25519 } from "../signing/ed25519.js";
8
+ import { verifyEip712 } from "../signing/eip712.js";
9
+ import { verifyEip191 } from "../signing/eip191.js";
10
+ /**
11
+ * Verify a complete attestation (both Merkle proof and signature)
12
+ *
13
+ * This function performs full verification:
14
+ * 1. Verifies the Merkle proof (statement is in the batch)
15
+ * 2. Verifies the signature (signer authorized the batch)
16
+ *
17
+ * @param statementFideId - The leaf value to verify in Merkle tree
18
+ * @param proof - The Merkle proof path (array of hashes from leaf to root)
19
+ * @param attestationData - The attestation data structure
20
+ * @param options - Configuration options for operation (method, public key/address, etc.)
21
+ * @returns True if both Merkle proof and signature are valid
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * // With Ed25519
26
+ * const isValid = await verifyAttestation(
27
+ * statementFideId,
28
+ * proof,
29
+ * attestationData,
30
+ * {
31
+ * method: 'ed25519',
32
+ * publicKeyOrAddress: publicKey
33
+ * }
34
+ * );
35
+ *
36
+ * // With EIP-712
37
+ * const isValid = await verifyAttestation(
38
+ * statementFideId,
39
+ * proof,
40
+ * attestationData,
41
+ * {
42
+ * method: 'eip712',
43
+ * publicKeyOrAddress: address
44
+ * }
45
+ * );
46
+ * ```
47
+ */
48
+ export async function verifyAttestation(statementFideId, proof, attestationData, options) {
49
+ // Parse attestation data if it's a string
50
+ const data = typeof attestationData === 'string'
51
+ ? parseAttestationData(attestationData)
52
+ : attestationData;
53
+ // 1. Verify Merkle proof
54
+ const merkleValid = await verifyStatementInAttestation(statementFideId, proof, data);
55
+ if (!merkleValid) {
56
+ return false;
57
+ }
58
+ // 2. Verify signature based on method
59
+ let signatureValid = false;
60
+ if (options.method === 'ed25519') {
61
+ if (typeof options.publicKeyOrAddress === 'string') {
62
+ throw new Error('Ed25519 verification requires a CryptoKey, not an address string');
63
+ }
64
+ signatureValid = await verifyEd25519(data.r, // merkle root
65
+ data.s, // signature
66
+ options.publicKeyOrAddress);
67
+ }
68
+ else if (options.method === 'eip712') {
69
+ if (typeof options.publicKeyOrAddress !== 'string') {
70
+ throw new Error('EIP-712 verification requires an address string, not a CryptoKey');
71
+ }
72
+ if (!data.s.startsWith('0x')) {
73
+ throw new Error('EIP-712 signature must be hex-encoded (0x prefix)');
74
+ }
75
+ signatureValid = await verifyEip712(data.r, // merkle root
76
+ data.s, // signature
77
+ options.publicKeyOrAddress);
78
+ }
79
+ else if (options.method === 'eip191') {
80
+ if (typeof options.publicKeyOrAddress !== 'string') {
81
+ throw new Error('EIP-191 verification requires an address string, not a CryptoKey');
82
+ }
83
+ if (!data.s.startsWith('0x')) {
84
+ throw new Error('EIP-191 signature must be hex-encoded (0x prefix)');
85
+ }
86
+ signatureValid = await verifyEip191(data.r, // merkle root
87
+ data.s, // signature
88
+ options.publicKeyOrAddress);
89
+ }
90
+ else {
91
+ throw new Error(`Unsupported signing method: ${options.method}`);
92
+ }
93
+ return signatureValid;
94
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * FCP Registry Configuration
3
+ *
4
+ * Allows registries to specify where attestations are located
5
+ * and other registry metadata.
6
+ */
7
+ export interface FCPRegistryConfig {
8
+ /** Path to attestations directory (relative to repo root) */
9
+ attestationsPath?: string;
10
+ /** Optional registry metadata */
11
+ metadata?: {
12
+ name?: string;
13
+ description?: string;
14
+ };
15
+ }
16
+ /**
17
+ * Default attestation paths to check if no config file exists
18
+ */
19
+ export declare const DEFAULT_ATTESTATION_PATHS: string[];
20
+ /**
21
+ * Default attestations path (used when generating paths)
22
+ */
23
+ export declare const DEFAULT_ATTESTATIONS_PATH = "attestations";
24
+ /**
25
+ * Registry config filename
26
+ */
27
+ export declare const REGISTRY_CONFIG_FILENAME = ".fcp-registry.json";
@@ -0,0 +1,22 @@
1
+ /**
2
+ * FCP Registry Configuration
3
+ *
4
+ * Allows registries to specify where attestations are located
5
+ * and other registry metadata.
6
+ */
7
+ /**
8
+ * Default attestation paths to check if no config file exists
9
+ */
10
+ export const DEFAULT_ATTESTATION_PATHS = [
11
+ 'attestations',
12
+ 'fcp-attestations',
13
+ '.fcp/attestations'
14
+ ];
15
+ /**
16
+ * Default attestations path (used when generating paths)
17
+ */
18
+ export const DEFAULT_ATTESTATIONS_PATH = 'attestations';
19
+ /**
20
+ * Registry config filename
21
+ */
22
+ export const REGISTRY_CONFIG_FILENAME = '.fcp-registry.json';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * FCP SDK - Broadcasting Module
3
+ *
4
+ * Re-exports all broadcasting utilities.
5
+ */
6
+ export { formatAttestationForJSONL, generateRegistryPath, generateJSONLFilename, type JSONLAttestation, type JSONLStatement } from "./registry.js";
7
+ export { DEFAULT_ATTESTATION_PATHS, DEFAULT_ATTESTATIONS_PATH, REGISTRY_CONFIG_FILENAME, type FCPRegistryConfig } from "./config.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * FCP SDK - Broadcasting Module
3
+ *
4
+ * Re-exports all broadcasting utilities.
5
+ */
6
+ export { formatAttestationForJSONL, generateRegistryPath, generateJSONLFilename } from "./registry.js";
7
+ export { DEFAULT_ATTESTATION_PATHS, DEFAULT_ATTESTATIONS_PATH, REGISTRY_CONFIG_FILENAME } from "./config.js";
@@ -0,0 +1,111 @@
1
+ /**
2
+ * FCP SDK Broadcasting Utilities
3
+ *
4
+ * Helpers for formatting attestations for JSONL registry files.
5
+ *
6
+ * Note: This module provides formatting helpers. Actual Git operations
7
+ * (commit, push) should be handled by your application using libraries
8
+ * like `simple-git` or `isomorphic-git`.
9
+ */
10
+ import type { AttestationResult } from "../attestation/index.js";
11
+ import type { Statement } from "../../statement/build.js";
12
+ /**
13
+ * Statement object in lean JSONL format
14
+ * Uses short keys for efficiency: s/sr, p/pr, o/or
15
+ */
16
+ export interface JSONLStatement {
17
+ /** Subject Fide ID (full did:fide:0x... format) */
18
+ s: string;
19
+ /** Subject raw identifier */
20
+ sr: string;
21
+ /** Predicate Fide ID (full did:fide:0x... format) */
22
+ p: string;
23
+ /** Predicate raw identifier */
24
+ pr: string;
25
+ /** Object Fide ID (full did:fide:0x... format) */
26
+ o: string;
27
+ /** Object raw identifier */
28
+ or: string;
29
+ }
30
+ /**
31
+ * Lean attestation format for JSONL registry files
32
+ *
33
+ * Optimized for indexers: minimal verbosity, all data needed for verification
34
+ * and materialization. Each line is one attestation batch.
35
+ *
36
+ * Format matches canonical attestation data structure: {m, r, s, u}
37
+ * Indexer derives attestation Fide ID from these fields.
38
+ */
39
+ export interface JSONLAttestation {
40
+ /** Method: Signing method (e.g., "ed25519", "eip712", "eip191") */
41
+ m: string;
42
+ /** User: CAIP-10 signer identifier */
43
+ u: string;
44
+ /** Root: Merkle root commitment */
45
+ r: string;
46
+ /** Signature: Cryptographic signature */
47
+ s: string;
48
+ /** Timestamp: ISO 8601 UTC timestamp */
49
+ t: string;
50
+ /** Data: Array of statements in this batch */
51
+ d: JSONLStatement[];
52
+ }
53
+ /**
54
+ * Format an attestation result for JSONL output
55
+ *
56
+ * Converts an AttestationResult into the lean JSONL format optimized for indexers.
57
+ * Each line in the JSONL file should be one attestation batch (one signature covering
58
+ * multiple statements via Merkle root).
59
+ *
60
+ * Format uses short keys (m, u, r, s, t, d) matching the canonical attestation
61
+ * data structure. Indexer derives attestation Fide ID from {m, r, s, u}.
62
+ *
63
+ * @param attestationResult - The result from createAttestation
64
+ * @param statements - The original statements that were attested (must match statementFideIds order)
65
+ * @param signedAt - ISO timestamp when the attestation was signed (defaults to current time)
66
+ * @returns Formatted attestation ready for JSONL output
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * const { statements } = await batchStatementsWithRoot([...]);
71
+ * const attestation = await createAttestation(statementFideIds, options);
72
+ * const jsonlAttestation = formatAttestationForJSONL(attestation, statements);
73
+ *
74
+ * // Write to JSONL file (one line per batch)
75
+ * fs.appendFileSync('attestation.jsonl', JSON.stringify(jsonlAttestation) + '\n');
76
+ * ```
77
+ */
78
+ export declare function formatAttestationForJSONL(attestationResult: AttestationResult, statements: Statement[], signedAt?: string): JSONLAttestation;
79
+ /**
80
+ * Generate the registry directory path for a given date
81
+ *
82
+ * Follows the YYYY/MM/DD structure required for Fide attestation registries.
83
+ * Uses UTC timezone for consistency across timezones.
84
+ *
85
+ * @param date - Date object (defaults to current UTC date)
86
+ * @returns Directory path (e.g., "2024/01/15")
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * const path = generateRegistryPath(new Date('2024-01-15T00:00:00Z'));
91
+ * // Result: "2024/01/15"
92
+ * ```
93
+ */
94
+ export declare function generateRegistryPath(date?: Date): string;
95
+ /**
96
+ * Generate a JSONL filename following the registry convention
97
+ *
98
+ * Format: YYYY-MM-DD-{HHmm}-{sequence}.jsonl
99
+ * Uses UTC timezone for consistency across timezones.
100
+ *
101
+ * @param date - Date object (defaults to current UTC date)
102
+ * @param sequence - Sequence number (defaults to 1)
103
+ * @returns Filename (e.g., "2024-01-15-1400-1.jsonl")
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * const filename = generateJSONLFilename(new Date('2024-01-15T14:00:00Z'), 1);
108
+ * // Result: "2024-01-15-1400-1.jsonl"
109
+ * ```
110
+ */
111
+ export declare function generateJSONLFilename(date?: Date, sequence?: number): string;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * FCP SDK Broadcasting Utilities
3
+ *
4
+ * Helpers for formatting attestations for JSONL registry files.
5
+ *
6
+ * Note: This module provides formatting helpers. Actual Git operations
7
+ * (commit, push) should be handled by your application using libraries
8
+ * like `simple-git` or `isomorphic-git`.
9
+ */
10
+ /**
11
+ * Format an attestation result for JSONL output
12
+ *
13
+ * Converts an AttestationResult into the lean JSONL format optimized for indexers.
14
+ * Each line in the JSONL file should be one attestation batch (one signature covering
15
+ * multiple statements via Merkle root).
16
+ *
17
+ * Format uses short keys (m, u, r, s, t, d) matching the canonical attestation
18
+ * data structure. Indexer derives attestation Fide ID from {m, r, s, u}.
19
+ *
20
+ * @param attestationResult - The result from createAttestation
21
+ * @param statements - The original statements that were attested (must match statementFideIds order)
22
+ * @param signedAt - ISO timestamp when the attestation was signed (defaults to current time)
23
+ * @returns Formatted attestation ready for JSONL output
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const { statements } = await batchStatementsWithRoot([...]);
28
+ * const attestation = await createAttestation(statementFideIds, options);
29
+ * const jsonlAttestation = formatAttestationForJSONL(attestation, statements);
30
+ *
31
+ * // Write to JSONL file (one line per batch)
32
+ * fs.appendFileSync('attestation.jsonl', JSON.stringify(jsonlAttestation) + '\n');
33
+ * ```
34
+ */
35
+ export function formatAttestationForJSONL(attestationResult, statements, signedAt = new Date().toISOString()) {
36
+ // Convert statements to lean JSONL format with short keys
37
+ const jsonlStatements = statements.map(stmt => ({
38
+ s: stmt.subjectFideId,
39
+ sr: stmt.subjectRawIdentifier,
40
+ p: stmt.predicateFideId,
41
+ pr: stmt.predicateRawIdentifier,
42
+ o: stmt.objectFideId,
43
+ or: stmt.objectRawIdentifier
44
+ }));
45
+ return {
46
+ m: attestationResult.attestationData.m,
47
+ u: attestationResult.attestationData.u,
48
+ r: attestationResult.merkleRoot,
49
+ s: attestationResult.attestationData.s,
50
+ t: signedAt,
51
+ d: jsonlStatements
52
+ };
53
+ }
54
+ /**
55
+ * Generate the registry directory path for a given date
56
+ *
57
+ * Follows the YYYY/MM/DD structure required for Fide attestation registries.
58
+ * Uses UTC timezone for consistency across timezones.
59
+ *
60
+ * @param date - Date object (defaults to current UTC date)
61
+ * @returns Directory path (e.g., "2024/01/15")
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * const path = generateRegistryPath(new Date('2024-01-15T00:00:00Z'));
66
+ * // Result: "2024/01/15"
67
+ * ```
68
+ */
69
+ export function generateRegistryPath(date = new Date()) {
70
+ const year = date.getUTCFullYear();
71
+ const month = String(date.getUTCMonth() + 1).padStart(2, '0');
72
+ const day = String(date.getUTCDate()).padStart(2, '0');
73
+ return `${year}/${month}/${day}`;
74
+ }
75
+ /**
76
+ * Generate a JSONL filename following the registry convention
77
+ *
78
+ * Format: YYYY-MM-DD-{HHmm}-{sequence}.jsonl
79
+ * Uses UTC timezone for consistency across timezones.
80
+ *
81
+ * @param date - Date object (defaults to current UTC date)
82
+ * @param sequence - Sequence number (defaults to 1)
83
+ * @returns Filename (e.g., "2024-01-15-1400-1.jsonl")
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * const filename = generateJSONLFilename(new Date('2024-01-15T14:00:00Z'), 1);
88
+ * // Result: "2024-01-15-1400-1.jsonl"
89
+ * ```
90
+ */
91
+ export function generateJSONLFilename(date = new Date(), sequence = 1) {
92
+ const year = date.getUTCFullYear();
93
+ const month = String(date.getUTCMonth() + 1).padStart(2, '0');
94
+ const day = String(date.getUTCDate()).padStart(2, '0');
95
+ const hours = String(date.getUTCHours()).padStart(2, '0');
96
+ const minutes = String(date.getUTCMinutes()).padStart(2, '0');
97
+ const timeWindow = `${hours}${minutes}`;
98
+ return `${year}-${month}-${day}-${timeWindow}-${sequence}.jsonl`;
99
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @fide.work/fcp experimental helpers
3
+ *
4
+ * These APIs are intentionally non-core and may change.
5
+ */
6
+ export { generateEd25519KeyPair, exportEd25519Keys, importEd25519Keys, signEd25519, verifyEd25519, getEthereumAddress, signEip712, verifyEip712, createEthereumCaip10, FCP_EIP712_DOMAIN, signEip191, verifyEip191, type Ed25519KeyPair, type ExportedEd25519Keys, type Eip712Domain, } from "./signing/index.js";
7
+ export { buildMerkleTree, verifyMerkleProof, type MerkleProofElement, type MerkleProof, type MerkleTreeResult, } from "./merkle/index.js";
8
+ export { createAttestation, createProvenanceStatements, verifyStatementInAttestation, verifyAttestation, parseAttestationData, verifyAttestationFideId, type SigningMethod, type AttestationData, type AttestationResult, type CreateAttestationOptions, type ProvenanceStatement, type VerifyAttestationOptions, } from "./attestation/index.js";
9
+ export { formatAttestationForJSONL, generateRegistryPath, generateJSONLFilename, DEFAULT_ATTESTATION_PATHS, DEFAULT_ATTESTATIONS_PATH, REGISTRY_CONFIG_FILENAME, type JSONLAttestation, type JSONLStatement, type FCPRegistryConfig, } from "./broadcasting/index.js";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @fide.work/fcp experimental helpers
3
+ *
4
+ * These APIs are intentionally non-core and may change.
5
+ */
6
+ // Signing helpers
7
+ export { generateEd25519KeyPair, exportEd25519Keys, importEd25519Keys, signEd25519, verifyEd25519, getEthereumAddress, signEip712, verifyEip712, createEthereumCaip10, FCP_EIP712_DOMAIN, signEip191, verifyEip191, } from "./signing/index.js";
8
+ // Merkle helpers
9
+ export { buildMerkleTree, verifyMerkleProof, } from "./merkle/index.js";
10
+ // Attestation helpers
11
+ export { createAttestation, createProvenanceStatements, verifyStatementInAttestation, verifyAttestation, parseAttestationData, verifyAttestationFideId, } from "./attestation/index.js";
12
+ // Broadcasting helpers
13
+ export { formatAttestationForJSONL, generateRegistryPath, generateJSONLFilename, DEFAULT_ATTESTATION_PATHS, DEFAULT_ATTESTATIONS_PATH, REGISTRY_CONFIG_FILENAME, } from "./broadcasting/index.js";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * FCP SDK - Merkle Tree Module
3
+ *
4
+ * Re-exports all Merkle tree utilities.
5
+ */
6
+ export { buildMerkleTree, verifyMerkleProof, type MerkleProofElement, type MerkleProof, type MerkleTreeResult } from "./tree.js";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * FCP SDK - Merkle Tree Module
3
+ *
4
+ * Re-exports all Merkle tree utilities.
5
+ */
6
+ export { buildMerkleTree, verifyMerkleProof } from "./tree.js";
@@ -0,0 +1,72 @@
1
+ /**
2
+ * FCP SDK Merkle Tree Utilities
3
+ * Build and verify Merkle trees for batch attestations
4
+ *
5
+ * Uses SHA-256 for hashing (same as Fide ID derivation).
6
+ * Zero external dependencies - uses native Web Crypto API.
7
+ */
8
+ /**
9
+ * Merkle proof element
10
+ */
11
+ export interface MerkleProofElement {
12
+ /** The sibling hash at this level */
13
+ hash: string;
14
+ /** Position of the sibling: 'left' or 'right' */
15
+ position: 'left' | 'right';
16
+ }
17
+ /**
18
+ * Merkle proof for a specific leaf
19
+ */
20
+ export type MerkleProof = MerkleProofElement[];
21
+ /**
22
+ * Result from building a Merkle tree
23
+ */
24
+ export interface MerkleTreeResult {
25
+ /** The Merkle root (hex string) */
26
+ root: string;
27
+ /** Proofs for each leaf, indexed by leaf value */
28
+ proofs: Map<string, MerkleProof>;
29
+ /** The leaves used to build the tree (normalized) */
30
+ leaves: string[];
31
+ }
32
+ /**
33
+ * Build a Merkle tree from an array of Fide IDs
34
+ *
35
+ * The tree is built bottom-up by iteratively hashing pairs of nodes.
36
+ * If there's an odd number of nodes, the last node is duplicated.
37
+ *
38
+ * @param leaves - Array of statement Fide IDs to include in the tree
39
+ * @returns MerkleTreeResult with root and proofs for each leaf
40
+ * @throws Error if leaves array is empty
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * const result = await buildMerkleTree([
45
+ * 'did:fide:0x00abc...',
46
+ * 'did:fide:0x00def...',
47
+ * 'did:fide:0x00ghi...'
48
+ * ]);
49
+ *
50
+ * console.log('Root:', result.root);
51
+ * console.log('Proof for first leaf:', result.proofs.get('did:fide:0x00abc...'));
52
+ * ```
53
+ */
54
+ export declare function buildMerkleTree(leaves: string[]): Promise<MerkleTreeResult>;
55
+ /**
56
+ * Verify that a leaf is part of a Merkle tree given its proof
57
+ *
58
+ * @param leaf - The leaf value to verify in Merkle tree
59
+ * @param proof - The Merkle proof path (array of hashes from leaf to root)
60
+ * @param root - The Merkle tree root hash (0x-prefixed hex)
61
+ * @returns True if the proof is valid
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * const isValid = await verifyMerkleProof(
66
+ * 'did:fide:0x00abc...',
67
+ * proof,
68
+ * root
69
+ * );
70
+ * ```
71
+ */
72
+ export declare function verifyMerkleProof(leaf: string, proof: MerkleProof, root: string): Promise<boolean>;
@@ -0,0 +1,154 @@
1
+ /**
2
+ * FCP SDK Merkle Tree Utilities
3
+ * Build and verify Merkle trees for batch attestations
4
+ *
5
+ * Uses SHA-256 for hashing (same as Fide ID derivation).
6
+ * Zero external dependencies - uses native Web Crypto API.
7
+ */
8
+ /**
9
+ * Get the Web Crypto API (works in browsers and Node.js)
10
+ */
11
+ async function getSubtleCrypto() {
12
+ if (globalThis.crypto?.subtle) {
13
+ return globalThis.crypto.subtle;
14
+ }
15
+ const { webcrypto } = await import("node:crypto");
16
+ return webcrypto.subtle;
17
+ }
18
+ /**
19
+ * Hash two values together using SHA-256
20
+ * Sorts inputs to ensure consistent ordering
21
+ */
22
+ async function hashPair(left, right) {
23
+ const subtle = await getSubtleCrypto();
24
+ // Concatenate left + right (already sorted at construction time)
25
+ const combined = left + right;
26
+ const encoder = new TextEncoder();
27
+ const data = encoder.encode(combined);
28
+ const hashBuffer = await subtle.digest('SHA-256', data);
29
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
30
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
31
+ }
32
+ /**
33
+ * Normalize a Fide ID to a consistent hex format for hashing
34
+ * Accepts: "did:fide:0x..." or "0x..."
35
+ * Returns: lowercase hex without prefix
36
+ */
37
+ function normalizeLeaf(fideId) {
38
+ const trimmed = fideId.trim().toLowerCase();
39
+ if (trimmed.startsWith('did:fide:0x')) {
40
+ return trimmed.slice('did:fide:0x'.length);
41
+ }
42
+ if (trimmed.startsWith('0x')) {
43
+ return trimmed.slice(2);
44
+ }
45
+ return trimmed;
46
+ }
47
+ /**
48
+ * Build a Merkle tree from an array of Fide IDs
49
+ *
50
+ * The tree is built bottom-up by iteratively hashing pairs of nodes.
51
+ * If there's an odd number of nodes, the last node is duplicated.
52
+ *
53
+ * @param leaves - Array of statement Fide IDs to include in the tree
54
+ * @returns MerkleTreeResult with root and proofs for each leaf
55
+ * @throws Error if leaves array is empty
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * const result = await buildMerkleTree([
60
+ * 'did:fide:0x00abc...',
61
+ * 'did:fide:0x00def...',
62
+ * 'did:fide:0x00ghi...'
63
+ * ]);
64
+ *
65
+ * console.log('Root:', result.root);
66
+ * console.log('Proof for first leaf:', result.proofs.get('did:fide:0x00abc...'));
67
+ * ```
68
+ */
69
+ export async function buildMerkleTree(leaves) {
70
+ if (leaves.length === 0) {
71
+ throw new Error('Cannot build Merkle tree from empty array');
72
+ }
73
+ // Normalize all leaves
74
+ const normalizedLeaves = leaves.map(normalizeLeaf);
75
+ // Initialize proofs map (keyed by original leaf value)
76
+ const proofs = new Map();
77
+ leaves.forEach(leaf => proofs.set(leaf, []));
78
+ // Also map normalized → original for lookup
79
+ const normalizedToOriginal = new Map();
80
+ leaves.forEach((original, i) => {
81
+ normalizedToOriginal.set(normalizedLeaves[i], original);
82
+ });
83
+ // Build tree level by level
84
+ let currentLevel = [...normalizedLeaves];
85
+ // Track which original leaves each node represents
86
+ // At level 0, each node represents itself
87
+ let nodeToLeaves = new Map();
88
+ normalizedLeaves.forEach(leaf => nodeToLeaves.set(leaf, [leaf]));
89
+ while (currentLevel.length > 1) {
90
+ const nextLevel = [];
91
+ const nextNodeToLeaves = new Map();
92
+ for (let i = 0; i < currentLevel.length; i += 2) {
93
+ const left = currentLevel[i];
94
+ const right = i + 1 < currentLevel.length ? currentLevel[i + 1] : left; // Duplicate if odd
95
+ const parent = await hashPair(left, right);
96
+ nextLevel.push(parent);
97
+ // Track which leaves this parent represents
98
+ const leftLeaves = nodeToLeaves.get(left) || [];
99
+ const rightLeaves = nodeToLeaves.get(right) || [];
100
+ nextNodeToLeaves.set(parent, [...leftLeaves, ...rightLeaves]);
101
+ // Add proof elements for all leaves on each side
102
+ // Leaves on the left get right sibling, leaves on the right get left sibling
103
+ for (const leafNorm of leftLeaves) {
104
+ const original = normalizedToOriginal.get(leafNorm);
105
+ const proof = proofs.get(original);
106
+ proof.push({ hash: right, position: 'right' });
107
+ }
108
+ // Only add if right != left (not a duplicate)
109
+ if (left !== right) {
110
+ for (const leafNorm of rightLeaves) {
111
+ const original = normalizedToOriginal.get(leafNorm);
112
+ const proof = proofs.get(original);
113
+ proof.push({ hash: left, position: 'left' });
114
+ }
115
+ }
116
+ }
117
+ currentLevel = nextLevel;
118
+ nodeToLeaves = nextNodeToLeaves;
119
+ }
120
+ return {
121
+ root: currentLevel[0],
122
+ proofs,
123
+ leaves
124
+ };
125
+ }
126
+ /**
127
+ * Verify that a leaf is part of a Merkle tree given its proof
128
+ *
129
+ * @param leaf - The leaf value to verify in Merkle tree
130
+ * @param proof - The Merkle proof path (array of hashes from leaf to root)
131
+ * @param root - The Merkle tree root hash (0x-prefixed hex)
132
+ * @returns True if the proof is valid
133
+ *
134
+ * @example
135
+ * ```ts
136
+ * const isValid = await verifyMerkleProof(
137
+ * 'did:fide:0x00abc...',
138
+ * proof,
139
+ * root
140
+ * );
141
+ * ```
142
+ */
143
+ export async function verifyMerkleProof(leaf, proof, root) {
144
+ let currentHash = normalizeLeaf(leaf);
145
+ for (const element of proof) {
146
+ if (element.position === 'left') {
147
+ currentHash = await hashPair(element.hash, currentHash);
148
+ }
149
+ else {
150
+ currentHash = await hashPair(currentHash, element.hash);
151
+ }
152
+ }
153
+ return currentHash === root;
154
+ }