@abelionorg/core 3.5.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/src/Brankas.ts ADDED
@@ -0,0 +1,322 @@
1
+ import { argon2id } from '@noble/hashes/argon2.js';
2
+
3
+ /**
4
+ * Brankas Engine: Web Crypto API & Argon2id implementation
5
+ * Standards: AES-GCM 256-bit, Argon2id (Pure JS)
6
+ *
7
+ * Version 3.4.0 Updates:
8
+ * - Added Quantum-Resistant Portable Backup Format
9
+ * - High-Memory Cost KDF for Backups
10
+ */
11
+
12
+ /**
13
+ * Hasil: Standar pengembalian data untuk operasi Brankas.
14
+ * Mencegah manipulasi error yang tidak terduga (crash).
15
+ */
16
+ export type Hasil<T> = { data: T; error: null } | { data: null; error: Error };
17
+
18
+ const ALGO_ENC = 'AES-GCM';
19
+ const MAX_CACHE_ITEMS = 100;
20
+
21
+ export class Brankas {
22
+ private static key: CryptoKey | null = null;
23
+
24
+ /**
25
+ * Cache for decrypted strings to improve performance on repeated reads.
26
+ * Cleared whenever the vault is locked or key changes.
27
+ */
28
+ private static decryptionCache = new Map<string, string>();
29
+
30
+ /**
31
+ * @param extractable Apakah kunci dapat diekspor (diperlukan untuk backup)
32
+ */
33
+ static async deriveKey(password: string, salt: Uint8Array, extractable = false): Promise<Hasil<CryptoKey>> {
34
+ const passwordBuffer = new TextEncoder().encode(password);
35
+ let hash: Uint8Array | null = null;
36
+
37
+ try {
38
+ hash = argon2id(passwordBuffer, salt, {
39
+ t: 2,
40
+ m: 19 * 1024,
41
+ dkLen: 32,
42
+ p: 1,
43
+ });
44
+
45
+ const key = await crypto.subtle.importKey(
46
+ 'raw',
47
+ hash as BufferSource,
48
+ { name: ALGO_ENC, length: 256 },
49
+ extractable,
50
+ ['encrypt', 'decrypt']
51
+ );
52
+ return { data: key, error: null };
53
+ } catch (error) {
54
+ console.error('[BRANKAS] Gagal menurunkan kunci (ERR_DRV_001)');
55
+ return { data: null, error: error instanceof Error ? error : new Error(String(error)) };
56
+ } finally {
57
+ passwordBuffer.fill(0);
58
+ if (hash) {
59
+ hash.fill(0);
60
+ }
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Generates a random 256-bit AES-GCM master key.
66
+ */
67
+ static async generateMasterKey(): Promise<Hasil<CryptoKey>> {
68
+ try {
69
+ const key = await crypto.subtle.generateKey(
70
+ { name: ALGO_ENC, length: 256 },
71
+ true,
72
+ ['encrypt', 'decrypt']
73
+ );
74
+ return { data: key, error: null };
75
+ } catch (e) {
76
+ return { data: null, error: e instanceof Error ? e : new Error(String(e)) };
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Imports a key from raw bytes.
82
+ */
83
+ static async importRawKey(keyBuffer: ArrayBuffer, extractable = true): Promise<Hasil<CryptoKey>> {
84
+ try {
85
+ const key = await crypto.subtle.importKey(
86
+ 'raw',
87
+ keyBuffer,
88
+ { name: ALGO_ENC, length: 256 },
89
+ extractable,
90
+ ['encrypt', 'decrypt']
91
+ );
92
+ return { data: key, error: null };
93
+ } catch (e) {
94
+ return { data: null, error: e instanceof Error ? e : new Error(String(e)) };
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Exports a key to raw bytes.
100
+ */
101
+ static async exportRawKey(key: CryptoKey): Promise<Hasil<ArrayBuffer>> {
102
+ try {
103
+ const buffer = await crypto.subtle.exportKey('raw', key);
104
+ return { data: buffer, error: null };
105
+ } catch (e) {
106
+ return { data: null, error: e instanceof Error ? e : new Error(String(e)) };
107
+ }
108
+ }
109
+
110
+ static setActiveKey(key: CryptoKey) {
111
+ this.key = key;
112
+ this.decryptionCache.clear();
113
+ }
114
+
115
+ static clearKey() {
116
+ this.key = null;
117
+ this.decryptionCache.clear();
118
+ }
119
+
120
+ static isLocked(): boolean {
121
+ return this.key === null;
122
+ }
123
+
124
+ static getActiveKey(): CryptoKey | null {
125
+ return this.key;
126
+ }
127
+
128
+ /**
129
+ * Mengenkripsi teks string
130
+ */
131
+ static async encrypt(text: string, customKey?: CryptoKey): Promise<Hasil<{ data: ArrayBuffer; iv: Uint8Array }>> {
132
+ const key = customKey || this.key;
133
+ if (!key) return { data: null, error: new Error('Brankas Terkunci: Kunci tidak aktif') };
134
+
135
+ try {
136
+ const iv = crypto.getRandomValues(new Uint8Array(12));
137
+ const encoder = new TextEncoder();
138
+
139
+ const data = await crypto.subtle.encrypt(
140
+ { name: ALGO_ENC, iv },
141
+ key,
142
+ encoder.encode(text)
143
+ );
144
+
145
+ return { data: { data, iv }, error: null };
146
+ } catch (e) {
147
+ return { data: null, error: e instanceof Error ? e : new Error(String(e)) };
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Mendekripsi ArrayBuffer kembali ke string
153
+ */
154
+ static async decrypt(encryptedData: ArrayBuffer, iv: Uint8Array, customKey?: CryptoKey): Promise<Hasil<string>> {
155
+ const key = customKey || this.key;
156
+ if (!key) return { data: null, error: new Error('Brankas Terkunci: Kunci tidak aktif') };
157
+
158
+ try {
159
+ const decrypted = await crypto.subtle.decrypt(
160
+ { name: ALGO_ENC, iv: iv as BufferSource },
161
+ key,
162
+ encryptedData
163
+ );
164
+
165
+ const decoder = new TextDecoder();
166
+ return { data: decoder.decode(decrypted), error: null };
167
+ } catch (e) {
168
+ return { data: null, error: e instanceof Error ? e : new Error(String(e)) };
169
+ }
170
+ }
171
+
172
+ static async encryptPacked(text: string, customKey?: CryptoKey): Promise<Hasil<string>> {
173
+ const result = await this.encrypt(text, customKey);
174
+ if (result.error) return { data: null, error: result.error };
175
+
176
+ const { data, iv } = result.data;
177
+ const ivHex = Array.from(iv).map(b => b.toString(16).padStart(2, '0')).join('');
178
+ const base64 = btoa(String.fromCharCode(...new Uint8Array(data)));
179
+ return { data: `${ivHex}|${base64}`, error: null };
180
+ }
181
+
182
+ /**
183
+ * Optimized hex string to Uint8Array conversion without regex.
184
+ */
185
+ private static hexToBytes(hex: string): Uint8Array {
186
+ const bytes = new Uint8Array(hex.length / 2);
187
+ for (let i = 0; i < bytes.length; i++) {
188
+ bytes[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
189
+ }
190
+ return bytes;
191
+ }
192
+
193
+ static async decryptPacked(packed: string, customKey?: CryptoKey): Promise<Hasil<string>> {
194
+ if (!packed || !packed.includes('|')) return { data: packed, error: null };
195
+
196
+ if (!customKey && this.decryptionCache.has(packed)) {
197
+ const cachedResult = this.decryptionCache.get(packed)!;
198
+ this.decryptionCache.delete(packed);
199
+ this.decryptionCache.set(packed, cachedResult);
200
+ return { data: cachedResult, error: null };
201
+ }
202
+
203
+ try {
204
+ const [ivHex, base64] = packed.split('|');
205
+ const iv = this.hexToBytes(ivHex);
206
+
207
+ const binaryString = atob(base64);
208
+ const bytes = new Uint8Array(binaryString.length);
209
+ for (let i = 0; i < binaryString.length; i++) {
210
+ bytes[i] = binaryString.charCodeAt(i);
211
+ }
212
+
213
+ const result = await this.decrypt(bytes.buffer, iv, customKey);
214
+ if (result.error) return result;
215
+
216
+ if (!customKey) {
217
+ if (this.decryptionCache.size >= MAX_CACHE_ITEMS) {
218
+ const firstKey = this.decryptionCache.keys().next().value;
219
+ if (firstKey !== undefined) {
220
+ this.decryptionCache.delete(firstKey);
221
+ }
222
+ }
223
+ this.decryptionCache.set(packed, result.data);
224
+ }
225
+
226
+ return result;
227
+ } catch (e) {
228
+ return { data: null, error: e instanceof Error ? e : new Error(String(e)) };
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Enkripsi Portabel (Quantum-Resistant Symmetric Structure)
234
+ * Format: [Magic:4][Ver:1][Salt:32][IV:12][Ciphertext:N]
235
+ */
236
+ static async encryptPortable(data: string, password: string): Promise<Hasil<Uint8Array>> {
237
+ try {
238
+ const salt = crypto.getRandomValues(new Uint8Array(32));
239
+ const iv = crypto.getRandomValues(new Uint8Array(12));
240
+
241
+ const hash = argon2id(password, salt, {
242
+ t: 4,
243
+ m: 64 * 1024,
244
+ dkLen: 32,
245
+ p: 1
246
+ });
247
+
248
+ const key = await crypto.subtle.importKey(
249
+ 'raw',
250
+ hash as BufferSource,
251
+ { name: ALGO_ENC, length: 256 },
252
+ false,
253
+ ['encrypt']
254
+ );
255
+
256
+ const encoder = new TextEncoder();
257
+ const encryptedBuffer = await crypto.subtle.encrypt(
258
+ { name: 'AES-GCM', iv },
259
+ key,
260
+ encoder.encode(data)
261
+ );
262
+
263
+ const magic = new Uint8Array([0x4C, 0x4D, 0x42, 0x52]);
264
+ const version = new Uint8Array([0x01]);
265
+
266
+ const result = new Uint8Array(
267
+ magic.length + version.length + salt.length + iv.length + encryptedBuffer.byteLength
268
+ );
269
+
270
+ let offset = 0;
271
+ result.set(magic, offset); offset += magic.length;
272
+ result.set(version, offset); offset += version.length;
273
+ result.set(salt, offset); offset += salt.length;
274
+ result.set(iv, offset); offset += iv.length;
275
+ result.set(new Uint8Array(encryptedBuffer), offset);
276
+
277
+ return { data: result, error: null };
278
+ } catch (e) {
279
+ return { data: null, error: e instanceof Error ? e : new Error(String(e)) };
280
+ }
281
+ }
282
+
283
+ static async decryptPortable(buffer: Uint8Array, password: string): Promise<Hasil<string>> {
284
+ try {
285
+ if (buffer.length < 50) return { data: null, error: new Error('Berkas terlalu kecil atau rusak') };
286
+
287
+ const magic = new TextDecoder().decode(buffer.slice(0, 4));
288
+ if (magic !== 'LMBR') return { data: null, error: new Error('Format berkas tidak valid (Magic mismatch)') };
289
+
290
+ const version = buffer[4];
291
+ if (version !== 1) return { data: null, error: new Error(`Versi berkas v${version} tidak didukung`) };
292
+
293
+ const salt = buffer.slice(5, 37);
294
+ const iv = buffer.slice(37, 49);
295
+ const ciphertext = buffer.slice(49);
296
+
297
+ const hash = argon2id(password, salt, {
298
+ t: 4,
299
+ m: 64 * 1024,
300
+ dkLen: 32,
301
+ p: 1
302
+ });
303
+
304
+ const key = await crypto.subtle.importKey(
305
+ 'raw',
306
+ hash as BufferSource,
307
+ { name: ALGO_ENC, length: 256 },
308
+ false,
309
+ ['decrypt']
310
+ );
311
+
312
+ const decrypted = await crypto.subtle.decrypt(
313
+ { name: 'AES-GCM', iv },
314
+ key,
315
+ ciphertext
316
+ );
317
+ return { data: new TextDecoder().decode(decrypted), error: null };
318
+ } catch (e) {
319
+ return { data: null, error: new Error('Gagal membuka berkas: Password salah atau data rusak.', { cause: e }) };
320
+ }
321
+ }
322
+ }
@@ -0,0 +1,45 @@
1
+ import { AuditLog } from './AuditLog';
2
+
3
+ export class CrashReporter {
4
+ private static terpasang = false;
5
+
6
+ /**
7
+ * Memasang pendengar (listener) global untuk menangkap crash aplikasi.
8
+ * Sangat disarankan untuk dipanggil sekali di root aplikasi (misalnya Layout Next.js atau Main CLI).
9
+ */
10
+ static pasang() {
11
+ if (this.terpasang) return;
12
+
13
+ // Lingkungan Browser
14
+ if (typeof window !== 'undefined') {
15
+ window.addEventListener('error', async (event) => {
16
+ const message = `[FATAL BROWSER CRASH] ${event.message}`;
17
+ const stack = event.error?.stack || 'Tanpa stack trace';
18
+
19
+ await AuditLog.catat('KESALAHAN', `${message}\nTrace:\n${stack}`);
20
+ });
21
+
22
+ window.addEventListener('unhandledrejection', async (event) => {
23
+ const reason = event.reason instanceof Error ? event.reason.stack : String(event.reason);
24
+ await AuditLog.catat('KESALAHAN', `[UNHANDLED PROMISE REJECTION]\nAlasan:\n${reason}`);
25
+ });
26
+ }
27
+
28
+ // Lingkungan Node (CLI/Server RSC)
29
+ if (typeof process !== 'undefined') {
30
+ process.on('uncaughtException', (error) => {
31
+ AuditLog.catat('KESALAHAN', `[FATAL NODE CRASH] Uncaught Exception\nTrace:\n${error.stack}`).catch(console.error);
32
+ });
33
+
34
+ process.on('unhandledRejection', (reason) => {
35
+ const r = reason instanceof Error ? reason.stack : String(reason);
36
+ AuditLog.catat('KESALAHAN', `[FATAL NODE CRASH] Unhandled Rejection\nAlasan:\n${r}`).catch(console.error);
37
+ });
38
+ }
39
+
40
+ this.terpasang = true;
41
+
42
+ // Log inisialisasi awal
43
+ AuditLog.catat('INFO', 'Crash Reporter Lembaran diaktifkan.').catch(() => {});
44
+ }
45
+ }
package/src/Gudang.ts ADDED
@@ -0,0 +1,63 @@
1
+ import { StorageAdapter, LembaranSchema } from './storage/types';
2
+ import { BrowserAdapter } from './storage/BrowserAdapter';
3
+
4
+ // Singleton instance adapter
5
+ let adapter: StorageAdapter;
6
+
7
+ const getAdapter = async (): Promise<StorageAdapter> => {
8
+ if (adapter) return adapter;
9
+
10
+ if (typeof window !== 'undefined' && typeof window.indexedDB !== 'undefined') {
11
+ adapter = new BrowserAdapter();
12
+ } else {
13
+ // Dynamic import to avoid 'fs' in browser bundle
14
+ const { FileAdapter } = await import('./storage/FileAdapter');
15
+ adapter = new FileAdapter();
16
+ }
17
+ return adapter;
18
+ };
19
+
20
+ // Re-export Schema for other consumers
21
+ export type { LembaranSchema };
22
+
23
+ export const Gudang = {
24
+ async inisialisasi(customPath?: string) {
25
+ if (process.env.DEBUG === 'true') {
26
+ if (process.env.DEBUG === 'true') console.log(`[GUDANG] Inisialisasi: ${customPath || 'default'}`);
27
+ }
28
+ if (typeof window === 'undefined') {
29
+ const { FileAdapter } = await import('./storage/FileAdapter');
30
+ adapter = new FileAdapter(customPath);
31
+ }
32
+ },
33
+
34
+ async set<K extends keyof LembaranSchema>(store: K, key: string, value: LembaranSchema[K]['value']) {
35
+ const adp = await getAdapter();
36
+ return adp.set(store, key, value);
37
+ },
38
+
39
+ async get<K extends keyof LembaranSchema>(store: K, key: string) {
40
+ const adp = await getAdapter();
41
+ return adp.get(store, key);
42
+ },
43
+
44
+ async getAll<K extends keyof LembaranSchema>(store: K) {
45
+ const adp = await getAdapter();
46
+ return adp.getAll(store);
47
+ },
48
+
49
+ async delete(store: keyof LembaranSchema, key: string) {
50
+ const adp = await getAdapter();
51
+ return adp.delete(store, key);
52
+ },
53
+
54
+ async count(store: keyof LembaranSchema) {
55
+ const adp = await getAdapter();
56
+ return adp.count(store);
57
+ },
58
+
59
+ async clear(store: keyof LembaranSchema) {
60
+ const adp = await getAdapter();
61
+ return adp.clear(store);
62
+ }
63
+ };
@@ -0,0 +1,19 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+
5
+ export function useGunakanTunda<T>(value: T, delay: number): T {
6
+ const [debouncedValue, setDebouncedValue] = useState<T>(value);
7
+
8
+ useEffect(() => {
9
+ const handler = setTimeout(() => {
10
+ setDebouncedValue(value);
11
+ }, delay);
12
+
13
+ return () => {
14
+ clearTimeout(handler);
15
+ };
16
+ }, [value, delay]);
17
+
18
+ return debouncedValue;
19
+ }
package/src/Indera.ts ADDED
@@ -0,0 +1,61 @@
1
+ export const haptic = {
2
+ light: () => {
3
+ if (typeof navigator !== 'undefined' && navigator.vibrate) {
4
+ navigator.vibrate(10);
5
+ }
6
+ },
7
+ medium: () => {
8
+ if (typeof navigator !== 'undefined' && navigator.vibrate) {
9
+ navigator.vibrate(25);
10
+ }
11
+ },
12
+ heavy: () => {
13
+ if (typeof navigator !== 'undefined' && navigator.vibrate) {
14
+ navigator.vibrate(50);
15
+ }
16
+ },
17
+ error: () => {
18
+ if (typeof navigator !== 'undefined' && navigator.vibrate) {
19
+ navigator.vibrate([30, 50, 30, 50]);
20
+ }
21
+ },
22
+ success: () => {
23
+ if (typeof navigator !== 'undefined' && navigator.vibrate) {
24
+ navigator.vibrate([10, 30, 10]);
25
+ }
26
+ }
27
+ };
28
+
29
+ export const audio = {
30
+ play: (frequency: number, type: OscillatorType = 'sine', duration: number = 0.1) => {
31
+ if (typeof window === 'undefined') return;
32
+ try {
33
+ const ctx = new (window.AudioContext || (window as any).webkitAudioContext)();
34
+ const osc = ctx.createOscillator();
35
+ const gain = ctx.createGain();
36
+
37
+ osc.type = type;
38
+ osc.frequency.setValueAtTime(frequency, ctx.currentTime);
39
+
40
+ gain.gain.setValueAtTime(0.05, ctx.currentTime);
41
+ gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + duration);
42
+
43
+ osc.connect(gain);
44
+ gain.connect(ctx.destination);
45
+
46
+ osc.start();
47
+ osc.stop(ctx.currentTime + duration);
48
+ } catch (_e) {
49
+ // Silently fail as haptics/audio are non-critical enhancements
50
+ }
51
+ },
52
+ click: () => audio.play(440, 'sine', 0.05),
53
+ unlock: () => {
54
+ audio.play(523.25, 'sine', 0.1); // C5
55
+ setTimeout(() => audio.play(659.25, 'sine', 0.2), 100); // E5
56
+ },
57
+ lock: () => {
58
+ audio.play(329.63, 'sine', 0.1); // E4
59
+ setTimeout(() => audio.play(261.63, 'sine', 0.2), 100); // C4
60
+ }
61
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Integritas Module: Handles data hashing for integrity checks.
3
+ * Follows Data Integrity Policy: Excludes metadata (_hash, _timestamp) during calculation.
4
+ */
5
+
6
+ const encoder = new TextEncoder();
7
+
8
+ export const Integritas = {
9
+ /**
10
+ * Calculates a SHA-256 hash of an object for integrity verification.
11
+ * Metadata fields are stripped before calculation using a replacer function.
12
+ * This is more efficient than cloning and deleting keys.
13
+ */
14
+ async hitungHash(data: unknown): Promise<string> {
15
+ // Exclude transient metadata per policy using JSON.stringify replacer
16
+ const text = JSON.stringify(data, (key, value) => {
17
+ if (key === '_hash' || key === '_timestamp' || key === 'updatedAt') {
18
+ return undefined;
19
+ }
20
+ return value;
21
+ });
22
+
23
+ const buffer = encoder.encode(text);
24
+
25
+ const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
26
+ const hashArray = new Uint8Array(hashBuffer);
27
+
28
+ // Optimized bytes-to-hex conversion using a pre-allocated string for better performance
29
+ let hashHex = '';
30
+ for (let i = 0; i < hashArray.length; i++) {
31
+ hashHex += hashArray[i].toString(16).padStart(2, '0');
32
+ }
33
+
34
+ return hashHex;
35
+ }
36
+ };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Kalender.ts
3
+ * Utilitas untuk memproses tanggal puitis dan cerdas.
4
+ */
5
+
6
+ export const parseSmartDate = (text: string): Date | null => {
7
+ const today = new Date();
8
+ const cleanText = text.toLowerCase().trim();
9
+
10
+ if (cleanText === '@hari_ini' || cleanText === '@sekarang') {
11
+ return today;
12
+ }
13
+
14
+ if (cleanText === '@besok') {
15
+ const tomorrow = new Date(today);
16
+ tomorrow.setDate(today.getDate() + 1);
17
+ return tomorrow;
18
+ }
19
+
20
+ if (cleanText === '@kemarin') {
21
+ const yesterday = new Date(today);
22
+ yesterday.setDate(today.getDate() - 1);
23
+ return yesterday;
24
+ }
25
+
26
+ if (cleanText === '@lusa') {
27
+ const afterTomorrow = new Date(today);
28
+ afterTomorrow.setDate(today.getDate() + 2);
29
+ return afterTomorrow;
30
+ }
31
+
32
+ if (cleanText === '@minggu_depan') {
33
+ const nextWeek = new Date(today);
34
+ nextWeek.setDate(today.getDate() + 7);
35
+ return nextWeek;
36
+ }
37
+
38
+ // Hari dalam seminggu
39
+ const days = ['minggu', 'senin', 'selasa', 'rabu', 'kamis', 'jumat', 'sabtu'];
40
+ for (let i = 0; i < days.length; i++) {
41
+ if (cleanText === `@${days[i]}`) {
42
+ const resultDate = new Date(today);
43
+ let diff = i - today.getDay();
44
+ if (diff <= 0) diff += 7; // Ambil hari tersebut di minggu depan
45
+ resultDate.setDate(today.getDate() + diff);
46
+ return resultDate;
47
+ }
48
+ }
49
+
50
+ return null;
51
+ };
52
+
53
+ export const formatArsipDate = (date: Date): string => {
54
+ return date.toLocaleDateString('id-ID', {
55
+ day: 'numeric',
56
+ month: 'long',
57
+ year: 'numeric'
58
+ });
59
+ };
@@ -0,0 +1,27 @@
1
+ const WORDLIST = [
2
+ "abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse",
3
+ "access", "accident", "account", "accuse", "achieve", "acid", "acoustic", "acquire", "across", "act",
4
+ "action", "actor", "actress", "actual", "adapt", "add", "addict", "address", "adjust", "admit",
5
+ "adult", "advance", "advice", "aerobic", "affair", "afford", "afraid", "again", "age", "agent",
6
+ "agree", "ahead", "aim", "air", "airport", "aisle", "alarm", "album", "alcohol", "alert",
7
+ "alien", "all", "alley", "allow", "almost", "alone", "alpha", "already", "also", "alter",
8
+ "always", "amaze", "ambition", "amount", "amuse", "analyst", "anchor", "ancient", "anger", "angle",
9
+ "angry", "animal", "ankle", "announce", "annual", "another", "answer", "antenna", "antique", "anxiety",
10
+ "any", "apart", "apology", "appear", "apple", "approve", "april", "arch", "arctic", "area",
11
+ "arena", "argue", "arm", "armed", "armor", "army", "around", "arrange", "arrest", "arrive",
12
+ "arrow", "art", "artefact", "artist", "artwork", "ask", "aspect", "assault", "asset", "assist",
13
+ "assume", "asthma", "athlete", "atom", "attack", "attend", "attitude", "attract", "auction", "audit"
14
+ ]; // Reduced for space, in production use full 2048 words
15
+
16
+ export const generateMnemonic = (wordCount: number = 12): string => {
17
+ const words: string[] = [];
18
+ const randomValues = new Uint32Array(wordCount);
19
+ crypto.getRandomValues(randomValues);
20
+
21
+ for (let i = 0; i < wordCount; i++) {
22
+ const index = randomValues[i] % WORDLIST.length;
23
+ words.push(WORDLIST[index]);
24
+ }
25
+
26
+ return words.join(' ');
27
+ };