forked from Talenttrust/Talenttrust-Backend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdeduplication.ts
More file actions
113 lines (100 loc) · 3.81 KB
/
Copy pathdeduplication.ts
File metadata and controls
113 lines (100 loc) · 3.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import { createHash, timingSafeEqual } from 'crypto';
import { ContractEvent, JsonValue } from '../events/types';
export class DeduplicationManager {
/**
* Computes a stable deduplication key for a contract event
* Format: contractId:eventId:sequence
* @param event The contract event to compute key for
* @returns Stable deduplication key string
*/
static computeDeduplicationKey(event: ContractEvent): string {
const keyComponents = [
event.contractId,
event.eventId,
event.sequence.toString()
];
return keyComponents.join(':');
}
/**
* Computes a hash of the event payload for integrity verification
* @param payload The event payload
* @returns SHA-256 hash of the payload
*/
static computePayloadHash(payload: JsonValue): string {
return createHash('sha256').update(canonicalize(payload)).digest('hex');
}
/**
* Validates that an event's payload hasn't been tampered with
* @param event The contract event
* @param expectedHash The expected payload hash
* @returns True if payload matches expected hash
*/
static validatePayloadIntegrity(event: ContractEvent, expectedHash: string): boolean {
const actualHash = this.computePayloadHash(event.payload);
return this.comparePayloadHashes(actualHash, expectedHash);
}
/**
* Compares two payload hashes using a timing‑safe constant‑time algorithm.
*
* The function first checks that the buffers are the same length. This guard
* prevents `crypto.timingSafeEqual` from being called with mismatched buffer
* lengths, which would otherwise throw. If the lengths differ we perform a
* self‑comparison (`timingSafeEqual(actual, actual)`) to keep the execution
* time consistent, then return `false`.
*
* When lengths match we delegate to `timingSafeEqual` which runs in constant
* time with respect to the contents of the buffers, providing resistance to
* timing‑attack leakage of hash equality.
*
* @param actualHash The hash computed from the received payload.
* @param expectedHash The hash stored for the idempotency key.
* @returns `true` when both hashes are identical SHA-256 digests.
*/
static comparePayloadHashes(actualHash: string, expectedHash: string): boolean {
const actualBuffer = Buffer.from(actualHash, 'hex');
const expectedBuffer = Buffer.from(expectedHash, 'hex');
if (actualBuffer.length !== expectedBuffer.length) {
timingSafeEqual(actualBuffer, actualBuffer);
return false;
}
return timingSafeEqual(actualBuffer, expectedBuffer);
}
/**
* Extracts components from a deduplication key
* @param deduplicationKey The deduplication key to parse
* @returns Object with contractId, eventId, and sequence
*/
static parseDeduplicationKey(deduplicationKey: string): {
contractId: string;
eventId: string;
sequence: number;
} {
const [contractId, eventId, sequenceStr] = deduplicationKey.split(':');
return {
contractId,
eventId,
sequence: parseInt(sequenceStr, 10)
};
}
/**
* Checks if two events represent the same logical event
* @param event1 First event
* @param event2 Second event
* @returns True if events are duplicates
*/
static areEventsDuplicates(event1: ContractEvent, event2: ContractEvent): boolean {
return this.computeDeduplicationKey(event1) === this.computeDeduplicationKey(event2);
}
}
function canonicalize(value: JsonValue): string {
if (value === null || typeof value !== 'object') {
return JSON.stringify(value);
}
if (Array.isArray(value)) {
return `[${value.map((item) => canonicalize(item)).join(',')}]`;
}
return `{${Object.entries(value)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalize(entry)}`)
.join(',')}}`;
}