@totalreclaw/totalreclaw 1.6.0 → 3.0.6
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/CLAWHUB.md +134 -0
- package/README.md +407 -64
- package/SKILL.md +1032 -0
- package/api-client.ts +5 -5
- package/claims-helper.ts +686 -0
- package/config.ts +211 -0
- package/consolidation.ts +141 -33
- package/contradiction-sync.ts +1389 -0
- package/crypto.ts +63 -261
- package/digest-sync.ts +516 -0
- package/embedding.ts +69 -46
- package/extractor.ts +1307 -84
- package/hot-cache-wrapper.ts +1 -1
- package/import-adapters/gemini-adapter.ts +243 -0
- package/import-adapters/index.ts +3 -0
- package/import-adapters/types.ts +1 -1
- package/index.ts +1887 -323
- package/llm-client.ts +106 -53
- package/lsh.ts +21 -210
- package/package.json +20 -7
- package/pin.ts +502 -0
- package/reranker.ts +96 -124
- package/skill.json +213 -0
- package/subgraph-search.ts +112 -5
- package/subgraph-store.ts +559 -275
- package/consolidation.test.ts +0 -356
- package/extractor-dedup.test.ts +0 -168
- package/import-adapters/import-adapters.test.ts +0 -1123
- package/lsh.test.ts +0 -463
- package/pocv2-e2e-test.ts +0 -917
- package/porter-stemmer.d.ts +0 -4
- package/reranker.test.ts +0 -594
- package/semantic-dedup.test.ts +0 -392
- package/setup.sh +0 -19
- package/store-dedup-wiring.test.ts +0 -186
package/crypto.ts
CHANGED
|
@@ -1,142 +1,79 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* TotalReclaw Plugin - Crypto Operations
|
|
2
|
+
* TotalReclaw Plugin - Crypto Operations (WASM-backed)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* (`client/src/crypto/`) so that memories written by one can be read by
|
|
7
|
-
* the other.
|
|
4
|
+
* Thin re-exports over `@totalreclaw/core` WASM module. Same function
|
|
5
|
+
* signatures as the previous implementation so callers don't need to change.
|
|
8
6
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* -> HKDF-SHA256(masterKey, salt, "totalreclaw-auth-key-v1", 32) -> authKey
|
|
13
|
-
* -> HKDF-SHA256(masterKey, salt, "totalreclaw-encryption-key-v1", 32) -> encryptionKey
|
|
14
|
-
* -> HKDF-SHA256(masterKey, salt, "openmemory-dedup-v1", 32) -> dedupKey
|
|
7
|
+
* The WASM module handles BIP-39 key derivation, XChaCha20-Poly1305 encrypt/
|
|
8
|
+
* decrypt, SHA-256 blind indices, HMAC-SHA256 content fingerprints,
|
|
9
|
+
* and LSH seed derivation.
|
|
15
10
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
11
|
+
* Key derivation chain (BIP-39 — handled by WASM):
|
|
12
|
+
* mnemonic -> BIP-39 PBKDF2 -> 512-bit seed
|
|
13
|
+
* -> HKDF-SHA256(seed, salt, "totalreclaw-auth-key-v1", 32) -> authKey
|
|
14
|
+
* -> HKDF-SHA256(seed, salt, "totalreclaw-encryption-key-v1", 32) -> encryptionKey
|
|
15
|
+
* -> HKDF-SHA256(seed, salt, "openmemory-dedup-v1", 32) -> dedupKey
|
|
19
16
|
*/
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
import { stemmer } from 'porter-stemmer';
|
|
28
|
-
import crypto from 'node:crypto';
|
|
18
|
+
// Lazy-load WASM to avoid crash when npm install hasn't finished yet.
|
|
19
|
+
let _wasm: typeof import('@totalreclaw/core') | null = null;
|
|
20
|
+
function getWasm() {
|
|
21
|
+
if (!_wasm) _wasm = require('@totalreclaw/core');
|
|
22
|
+
return _wasm;
|
|
23
|
+
}
|
|
29
24
|
|
|
30
25
|
// ---------------------------------------------------------------------------
|
|
31
|
-
//
|
|
26
|
+
// BIP-39 Validation
|
|
32
27
|
// ---------------------------------------------------------------------------
|
|
33
28
|
|
|
34
|
-
/** HKDF context strings -- must match client/src/crypto/kdf.ts exactly. */
|
|
35
|
-
const AUTH_KEY_INFO = 'totalreclaw-auth-key-v1';
|
|
36
|
-
const ENCRYPTION_KEY_INFO = 'totalreclaw-encryption-key-v1';
|
|
37
|
-
const DEDUP_KEY_INFO = 'openmemory-dedup-v1';
|
|
38
|
-
|
|
39
|
-
/** Argon2id parameters -- OWASP recommendations, matching client defaults. */
|
|
40
|
-
const ARGON2_TIME_COST = 3;
|
|
41
|
-
const ARGON2_MEMORY_COST = 65536; // 64 MB in KiB
|
|
42
|
-
const ARGON2_PARALLELISM = 4;
|
|
43
|
-
const ARGON2_DK_LEN = 32;
|
|
44
|
-
|
|
45
|
-
/** AES-256-GCM constants. */
|
|
46
|
-
const IV_LENGTH = 12;
|
|
47
|
-
const TAG_LENGTH = 16;
|
|
48
|
-
const KEY_LENGTH = 32;
|
|
49
|
-
|
|
50
29
|
/**
|
|
51
|
-
* Check if the input looks like a BIP-39 mnemonic (12 or 24 words
|
|
30
|
+
* Check if the input looks like a BIP-39 mnemonic (12 or 24 words).
|
|
31
|
+
*
|
|
32
|
+
* Lenient: accepts phrases where all words look like valid BIP-39 words
|
|
33
|
+
* (allows invalid checksums, which LLMs sometimes generate).
|
|
52
34
|
*/
|
|
53
|
-
function isBip39Mnemonic(input: string): boolean {
|
|
35
|
+
export function isBip39Mnemonic(input: string): boolean {
|
|
54
36
|
const words = input.trim().split(/\s+/);
|
|
55
|
-
|
|
56
|
-
return validateMnemonic(input.trim(), wordlist);
|
|
37
|
+
return words.length === 12 || words.length === 24;
|
|
57
38
|
}
|
|
58
39
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
* Uses the 512-bit BIP-39 seed as HKDF input (NOT the derived private key)
|
|
62
|
-
* for proper key separation from the Ethereum signing key.
|
|
63
|
-
*/
|
|
64
|
-
function deriveKeysFromMnemonic(
|
|
65
|
-
mnemonic: string,
|
|
66
|
-
): { authKey: Buffer; encryptionKey: Buffer; dedupKey: Buffer; salt: Buffer } {
|
|
67
|
-
// BIP-39: mnemonic -> 512-bit seed via PBKDF2(mnemonic, "mnemonic", 2048 rounds)
|
|
68
|
-
const seed = mnemonicToSeedSync(mnemonic.trim());
|
|
69
|
-
|
|
70
|
-
// Use first 32 bytes of seed as deterministic salt for HKDF
|
|
71
|
-
// (BIP-39 mnemonics are self-salting via PBKDF2, no random salt needed)
|
|
72
|
-
const salt = Buffer.from(seed.slice(0, 32));
|
|
73
|
-
|
|
74
|
-
// HKDF-SHA256 from the full 512-bit seed, using distinct info strings
|
|
75
|
-
const enc = (s: string) => Buffer.from(s, 'utf8');
|
|
76
|
-
const seedBuf = Buffer.from(seed);
|
|
40
|
+
// Re-export for backward compatibility
|
|
41
|
+
export const validateMnemonic = isBip39Mnemonic;
|
|
77
42
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const encryptionKey = Buffer.from(
|
|
82
|
-
hkdf(sha256, seedBuf, salt, enc(ENCRYPTION_KEY_INFO), 32),
|
|
83
|
-
);
|
|
84
|
-
const dedupKey = Buffer.from(
|
|
85
|
-
hkdf(sha256, seedBuf, salt, enc(DEDUP_KEY_INFO), 32),
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
return { authKey, encryptionKey, dedupKey, salt };
|
|
89
|
-
}
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Key Derivation
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
90
46
|
|
|
91
47
|
/**
|
|
92
48
|
* Derive auth, encryption, and dedup keys from a recovery phrase.
|
|
93
49
|
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
50
|
+
* Delegates to the WASM module for BIP-39 seed derivation and HKDF
|
|
51
|
+
* key separation. Uses the lenient variant for phrases where all words
|
|
52
|
+
* are valid but the checksum fails.
|
|
97
53
|
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* account so that the derived keys match the original registration.
|
|
101
|
-
*
|
|
102
|
-
* @returns Object containing authKey, encryptionKey, dedupKey, and salt (all Buffers).
|
|
54
|
+
* @param password - BIP-39 12/24-word mnemonic
|
|
55
|
+
* @param existingSalt - Ignored for BIP-39 path (salt is deterministic)
|
|
103
56
|
*/
|
|
104
57
|
export function deriveKeys(
|
|
105
58
|
password: string,
|
|
106
59
|
existingSalt?: Buffer,
|
|
107
60
|
): { authKey: Buffer; encryptionKey: Buffer; dedupKey: Buffer; salt: Buffer } {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
61
|
+
const trimmed = password.trim();
|
|
62
|
+
|
|
63
|
+
// Try strict validation first, fall back to lenient
|
|
64
|
+
let result: { auth_key: string; encryption_key: string; dedup_key: string; salt: string };
|
|
65
|
+
try {
|
|
66
|
+
result = getWasm().deriveKeysFromMnemonic(trimmed);
|
|
67
|
+
} catch {
|
|
68
|
+
result = getWasm().deriveKeysFromMnemonicLenient(trimmed);
|
|
113
69
|
}
|
|
114
70
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
Buffer.from(password, 'utf8'),
|
|
122
|
-
salt,
|
|
123
|
-
{ t: ARGON2_TIME_COST, m: ARGON2_MEMORY_COST, p: ARGON2_PARALLELISM, dkLen: ARGON2_DK_LEN },
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
// Step 2 -- HKDF-SHA256 for each sub-key using distinct info strings.
|
|
127
|
-
// @noble/hashes v2 requires Uint8Array for info param.
|
|
128
|
-
const enc = (s: string) => Buffer.from(s, 'utf8');
|
|
129
|
-
const authKey = Buffer.from(
|
|
130
|
-
hkdf(sha256, masterKey, salt, enc(AUTH_KEY_INFO), 32),
|
|
131
|
-
);
|
|
132
|
-
const encryptionKey = Buffer.from(
|
|
133
|
-
hkdf(sha256, masterKey, salt, enc(ENCRYPTION_KEY_INFO), 32),
|
|
134
|
-
);
|
|
135
|
-
const dedupKey = Buffer.from(
|
|
136
|
-
hkdf(sha256, masterKey, salt, enc(DEDUP_KEY_INFO), 32),
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
return { authKey, encryptionKey, dedupKey, salt: Buffer.from(salt) };
|
|
71
|
+
return {
|
|
72
|
+
authKey: Buffer.from(result.auth_key, 'hex'),
|
|
73
|
+
encryptionKey: Buffer.from(result.encryption_key, 'hex'),
|
|
74
|
+
dedupKey: Buffer.from(result.dedup_key, 'hex'),
|
|
75
|
+
salt: Buffer.from(result.salt, 'hex'),
|
|
76
|
+
};
|
|
140
77
|
}
|
|
141
78
|
|
|
142
79
|
// ---------------------------------------------------------------------------
|
|
@@ -144,48 +81,16 @@ export function deriveKeys(
|
|
|
144
81
|
// ---------------------------------------------------------------------------
|
|
145
82
|
|
|
146
83
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* The LSH hasher needs a deterministic seed so that the same master key
|
|
150
|
-
* always generates the same random hyperplane matrices. We derive this seed
|
|
151
|
-
* from the master key using HKDF with a unique info string.
|
|
152
|
-
*
|
|
153
|
-
* For the BIP-39 path the HKDF input is the 512-bit BIP-39 seed; for the
|
|
154
|
-
* Argon2id path it is the 32-byte master key.
|
|
155
|
-
*/
|
|
156
|
-
const LSH_SEED_INFO = 'openmemory-lsh-seed-v1';
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Derive a 32-byte seed for the LSH hasher from the master key derivation
|
|
160
|
-
* chain.
|
|
84
|
+
* Derive a 32-byte seed for the LSH hasher.
|
|
161
85
|
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
* For the BIP-39 path we use the full 512-bit BIP-39 seed as IKM; for the
|
|
165
|
-
* Argon2id path we use the 32-byte Argon2id-derived master key. In both
|
|
166
|
-
* cases the salt from `deriveKeys()` is reused for domain separation.
|
|
86
|
+
* Delegates to the WASM module.
|
|
167
87
|
*/
|
|
168
88
|
export function deriveLshSeed(
|
|
169
89
|
password: string,
|
|
170
90
|
salt: Buffer,
|
|
171
91
|
): Uint8Array {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
return new Uint8Array(
|
|
175
|
-
hkdf(sha256, Buffer.from(seed), salt, Buffer.from(LSH_SEED_INFO, 'utf8'), 32),
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Argon2id path: re-derive the master key, then HKDF with LSH info string.
|
|
180
|
-
const masterKey = argon2id(
|
|
181
|
-
Buffer.from(password, 'utf8'),
|
|
182
|
-
salt,
|
|
183
|
-
{ t: ARGON2_TIME_COST, m: ARGON2_MEMORY_COST, p: ARGON2_PARALLELISM, dkLen: ARGON2_DK_LEN },
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
return new Uint8Array(
|
|
187
|
-
hkdf(sha256, masterKey, salt, Buffer.from(LSH_SEED_INFO, 'utf8'), 32),
|
|
188
|
-
);
|
|
92
|
+
const seedHex = getWasm().deriveLshSeed(password.trim(), salt.toString('hex'));
|
|
93
|
+
return new Uint8Array(Buffer.from(seedHex, 'hex'));
|
|
189
94
|
}
|
|
190
95
|
|
|
191
96
|
// ---------------------------------------------------------------------------
|
|
@@ -194,72 +99,30 @@ export function deriveLshSeed(
|
|
|
194
99
|
|
|
195
100
|
/**
|
|
196
101
|
* Compute the SHA-256 hash of the auth key.
|
|
197
|
-
*
|
|
198
|
-
* The server stores SHA256(authKey) during registration and uses it to look
|
|
199
|
-
* up users on every request. The hex string returned here is what the plugin
|
|
200
|
-
* sends to `/v1/register` as `auth_key_hash`.
|
|
201
102
|
*/
|
|
202
103
|
export function computeAuthKeyHash(authKey: Buffer): string {
|
|
203
|
-
return
|
|
104
|
+
return getWasm().computeAuthKeyHash(authKey.toString('hex'));
|
|
204
105
|
}
|
|
205
106
|
|
|
206
107
|
// ---------------------------------------------------------------------------
|
|
207
|
-
//
|
|
108
|
+
// XChaCha20-Poly1305 Encrypt / Decrypt
|
|
208
109
|
// ---------------------------------------------------------------------------
|
|
209
110
|
|
|
210
111
|
/**
|
|
211
|
-
* Encrypt a UTF-8 plaintext string with
|
|
112
|
+
* Encrypt a UTF-8 plaintext string with XChaCha20-Poly1305.
|
|
212
113
|
*
|
|
213
114
|
* Wire format (base64-encoded):
|
|
214
|
-
* [
|
|
215
|
-
*
|
|
216
|
-
* This matches `serializeEncryptedData` in `client/src/crypto/aes.ts`.
|
|
115
|
+
* [nonce: 24 bytes][tag: 16 bytes][ciphertext: variable]
|
|
217
116
|
*/
|
|
218
117
|
export function encrypt(plaintext: string, encryptionKey: Buffer): string {
|
|
219
|
-
|
|
220
|
-
throw new Error(`Invalid key length: expected ${KEY_LENGTH}, got ${encryptionKey.length}`);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const iv = crypto.randomBytes(IV_LENGTH);
|
|
224
|
-
const cipher = crypto.createCipheriv('aes-256-gcm', encryptionKey, iv, {
|
|
225
|
-
authTagLength: TAG_LENGTH,
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
|
229
|
-
const tag = cipher.getAuthTag();
|
|
230
|
-
|
|
231
|
-
// Combine: iv || tag || ciphertext (same order as client library)
|
|
232
|
-
const combined = Buffer.concat([iv, tag, ciphertext]);
|
|
233
|
-
return combined.toString('base64');
|
|
118
|
+
return getWasm().encrypt(plaintext, encryptionKey.toString('hex'));
|
|
234
119
|
}
|
|
235
120
|
|
|
236
121
|
/**
|
|
237
|
-
* Decrypt a base64-encoded
|
|
238
|
-
*
|
|
239
|
-
* Expects the wire format produced by `encrypt()` above.
|
|
122
|
+
* Decrypt a base64-encoded XChaCha20-Poly1305 blob back to a UTF-8 string.
|
|
240
123
|
*/
|
|
241
124
|
export function decrypt(encryptedBase64: string, encryptionKey: Buffer): string {
|
|
242
|
-
|
|
243
|
-
throw new Error(`Invalid key length: expected ${KEY_LENGTH}, got ${encryptionKey.length}`);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const combined = Buffer.from(encryptedBase64, 'base64');
|
|
247
|
-
|
|
248
|
-
if (combined.length < IV_LENGTH + TAG_LENGTH) {
|
|
249
|
-
throw new Error('Encrypted data too short');
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const iv = combined.subarray(0, IV_LENGTH);
|
|
253
|
-
const tag = combined.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
|
|
254
|
-
const ciphertext = combined.subarray(IV_LENGTH + TAG_LENGTH);
|
|
255
|
-
|
|
256
|
-
const decipher = crypto.createDecipheriv('aes-256-gcm', encryptionKey, iv, {
|
|
257
|
-
authTagLength: TAG_LENGTH,
|
|
258
|
-
});
|
|
259
|
-
decipher.setAuthTag(tag);
|
|
260
|
-
|
|
261
|
-
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
262
|
-
return plaintext.toString('utf8');
|
|
125
|
+
return getWasm().decrypt(encryptedBase64, encryptionKey.toString('hex'));
|
|
263
126
|
}
|
|
264
127
|
|
|
265
128
|
// ---------------------------------------------------------------------------
|
|
@@ -269,83 +132,22 @@ export function decrypt(encryptedBase64: string, encryptionKey: Buffer): string
|
|
|
269
132
|
/**
|
|
270
133
|
* Generate blind indices (SHA-256 hashes of tokens) for a text string.
|
|
271
134
|
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
* 2. Remove punctuation (keep Unicode letters, numbers, whitespace)
|
|
275
|
-
* 3. Split on whitespace
|
|
276
|
-
* 4. Filter tokens shorter than 2 characters
|
|
277
|
-
*
|
|
278
|
-
* Each surviving token is SHA-256 hashed and returned as a hex string.
|
|
279
|
-
* The returned array is deduplicated.
|
|
135
|
+
* Delegates to the WASM module which performs tokenization, stemming,
|
|
136
|
+
* and SHA-256 hashing.
|
|
280
137
|
*/
|
|
281
138
|
export function generateBlindIndices(text: string): string[] {
|
|
282
|
-
|
|
283
|
-
.toLowerCase()
|
|
284
|
-
.replace(/[^\p{L}\p{N}\s]/gu, ' ') // Remove punctuation, keep letters/numbers
|
|
285
|
-
.split(/\s+/)
|
|
286
|
-
.filter((t) => t.length >= 2);
|
|
287
|
-
|
|
288
|
-
const seen = new Set<string>();
|
|
289
|
-
const indices: string[] = [];
|
|
290
|
-
|
|
291
|
-
for (const token of tokens) {
|
|
292
|
-
// Exact word hash (unchanged behavior).
|
|
293
|
-
const hash = Buffer.from(sha256(Buffer.from(token, 'utf8'))).toString('hex');
|
|
294
|
-
if (!seen.has(hash)) {
|
|
295
|
-
seen.add(hash);
|
|
296
|
-
indices.push(hash);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Stemmed word hash. The stem is prefixed with "stem:" before hashing
|
|
300
|
-
// to avoid collisions between a word that happens to equal another
|
|
301
|
-
// word's stem (e.g., the word "commun" vs the stem of "community").
|
|
302
|
-
const stem = stemmer(token);
|
|
303
|
-
if (stem.length >= 2 && stem !== token) {
|
|
304
|
-
const stemHash = Buffer.from(
|
|
305
|
-
sha256(Buffer.from(`stem:${stem}`, 'utf8'))
|
|
306
|
-
).toString('hex');
|
|
307
|
-
if (!seen.has(stemHash)) {
|
|
308
|
-
seen.add(stemHash);
|
|
309
|
-
indices.push(stemHash);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return indices;
|
|
139
|
+
return getWasm().generateBlindIndices(text);
|
|
315
140
|
}
|
|
316
141
|
|
|
317
142
|
// ---------------------------------------------------------------------------
|
|
318
143
|
// Content Fingerprint (Dedup)
|
|
319
144
|
// ---------------------------------------------------------------------------
|
|
320
145
|
|
|
321
|
-
/**
|
|
322
|
-
* Normalize text for deterministic fingerprinting.
|
|
323
|
-
*
|
|
324
|
-
* Steps (matching `client/src/crypto/fingerprint.ts#normalizeText`):
|
|
325
|
-
* 1. Unicode NFC normalization
|
|
326
|
-
* 2. Lowercase
|
|
327
|
-
* 3. Collapse whitespace (spaces/tabs/newlines to single space)
|
|
328
|
-
* 4. Trim leading/trailing whitespace
|
|
329
|
-
*/
|
|
330
|
-
function normalizeText(text: string): string {
|
|
331
|
-
return text
|
|
332
|
-
.normalize('NFC')
|
|
333
|
-
.toLowerCase()
|
|
334
|
-
.replace(/\s+/g, ' ')
|
|
335
|
-
.trim();
|
|
336
|
-
}
|
|
337
|
-
|
|
338
146
|
/**
|
|
339
147
|
* Compute an HMAC-SHA256 content fingerprint for exact-duplicate detection.
|
|
340
148
|
*
|
|
341
|
-
* The server stores this fingerprint and uses it to reject duplicate writes
|
|
342
|
-
* without ever seeing the plaintext.
|
|
343
|
-
*
|
|
344
149
|
* @returns 64-character hex string.
|
|
345
150
|
*/
|
|
346
151
|
export function generateContentFingerprint(plaintext: string, dedupKey: Buffer): string {
|
|
347
|
-
|
|
348
|
-
return Buffer.from(
|
|
349
|
-
hmac(sha256, dedupKey, Buffer.from(normalized, 'utf8')),
|
|
350
|
-
).toString('hex');
|
|
152
|
+
return getWasm().generateContentFingerprint(plaintext, dedupKey.toString('hex'));
|
|
351
153
|
}
|