@me2em/react 0.1.0-alpha.1

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/index.js ADDED
@@ -0,0 +1,1225 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/react/Me2emProvider.tsx
8
+ import {
9
+ useState,
10
+ useCallback,
11
+ useMemo
12
+ } from "react";
13
+ import { Identity } from "@me2em/core";
14
+
15
+ // src/react/context.ts
16
+ import { createContext, useContext } from "react";
17
+ var Me2emContext = createContext(null);
18
+ function useMe2emContext() {
19
+ const ctx = useContext(Me2emContext);
20
+ if (!ctx) {
21
+ throw new Error("Me2em hooks must be used inside <Me2emProvider>");
22
+ }
23
+ return ctx;
24
+ }
25
+
26
+ // src/react/Me2emProvider.tsx
27
+ import { jsx } from "react/jsx-runtime";
28
+ function Me2emProvider({ children }) {
29
+ const [identity, setIdentity] = useState(null);
30
+ const activateIdentity = useCallback(async (seed) => {
31
+ try {
32
+ const id = await Identity.fromSeed(seed);
33
+ setIdentity(id);
34
+ return true;
35
+ } catch {
36
+ return false;
37
+ }
38
+ }, []);
39
+ const clearIdentity = useCallback(() => {
40
+ setIdentity(null);
41
+ }, []);
42
+ const value = useMemo(
43
+ () => ({ identity, hasIdentity: identity !== null, activateIdentity, clearIdentity }),
44
+ [identity, activateIdentity, clearIdentity]
45
+ );
46
+ return /* @__PURE__ */ jsx(Me2emContext.Provider, { value, children });
47
+ }
48
+
49
+ // src/react/useCreateIdentity.ts
50
+ import {
51
+ useReducer,
52
+ useCallback as useCallback2
53
+ } from "react";
54
+ import {
55
+ generateSeedPhrase,
56
+ get32ByteSeedFromMnemonic
57
+ } from "@me2em/core";
58
+
59
+ // src/headless/identity-flow.ts
60
+ var initialIdentityFlowState = {
61
+ status: "idle",
62
+ seedWords: null,
63
+ identitySeed: null,
64
+ error: null
65
+ };
66
+ var UNEXPECTED_EVENTS = {
67
+ CONFIRM_WORDS: "CONFIRM_WORDS called from idle \u2014 no seed to confirm",
68
+ RESET: "RESET called from idle \u2014 nothing to reset"
69
+ };
70
+ function identityFlowReducer(state, event) {
71
+ const { type } = event;
72
+ switch (state.status) {
73
+ case "idle": {
74
+ if (type === "GENERATE") {
75
+ return {
76
+ status: "seed-generated",
77
+ seedWords: event.words,
78
+ identitySeed: event.seed,
79
+ error: null
80
+ };
81
+ }
82
+ if (type === "IMPORT") {
83
+ return {
84
+ status: "imported",
85
+ seedWords: null,
86
+ identitySeed: event.seed,
87
+ error: null
88
+ };
89
+ }
90
+ if (type === "FAIL") {
91
+ return {
92
+ status: "error",
93
+ seedWords: null,
94
+ identitySeed: null,
95
+ error: event.message
96
+ };
97
+ }
98
+ if (UNEXPECTED_EVENTS[type]) {
99
+ console.warn(UNEXPECTED_EVENTS[type]);
100
+ return state;
101
+ }
102
+ return state;
103
+ }
104
+ case "seed-generated": {
105
+ if (type === "CONFIRM_WORDS") {
106
+ return {
107
+ status: "verified",
108
+ seedWords: null,
109
+ identitySeed: state.identitySeed,
110
+ error: null
111
+ };
112
+ }
113
+ if (type === "FAIL") {
114
+ return {
115
+ status: "error",
116
+ seedWords: null,
117
+ identitySeed: null,
118
+ error: event.message
119
+ };
120
+ }
121
+ if (type === "RESET") {
122
+ return initialIdentityFlowState;
123
+ }
124
+ console.warn(`Unexpected event ${type} in seed-generated`);
125
+ return state;
126
+ }
127
+ case "verified":
128
+ case "imported": {
129
+ if (type === "RESET") {
130
+ return initialIdentityFlowState;
131
+ }
132
+ if (type === "FAIL") {
133
+ return {
134
+ status: "error",
135
+ seedWords: null,
136
+ identitySeed: null,
137
+ error: event.message
138
+ };
139
+ }
140
+ console.warn(`Unexpected event ${type} in ${state.status}`);
141
+ return state;
142
+ }
143
+ case "error": {
144
+ if (type === "RESET") {
145
+ return initialIdentityFlowState;
146
+ }
147
+ console.warn(`Unexpected event ${type} in error`);
148
+ return state;
149
+ }
150
+ default:
151
+ return state;
152
+ }
153
+ }
154
+
155
+ // src/react/useCreateIdentity.ts
156
+ function useCreateIdentity() {
157
+ const { activateIdentity, clearIdentity, identity } = useMe2emContext();
158
+ const [state, dispatch] = useReducer(identityFlowReducer, initialIdentityFlowState);
159
+ const generate = useCallback2(
160
+ (opts) => {
161
+ const words = generateSeedPhrase(opts?.strength ?? 128);
162
+ get32ByteSeedFromMnemonic(words).then((derivedSeed) => {
163
+ dispatch({ type: "GENERATE", words, seed: derivedSeed });
164
+ }).catch(() => {
165
+ dispatch({ type: "FAIL", message: "Failed to generate seed" });
166
+ });
167
+ },
168
+ []
169
+ );
170
+ const confirmWords = useCallback2(async () => {
171
+ if (state.status !== "seed-generated" || !state.identitySeed) return;
172
+ dispatch({ type: "CONFIRM_WORDS" });
173
+ const ok = await activateIdentity(state.identitySeed);
174
+ if (!ok) {
175
+ dispatch({ type: "FAIL", message: "Identity activation failed" });
176
+ }
177
+ }, [state.status, state.identitySeed, activateIdentity]);
178
+ const reset = useCallback2(() => {
179
+ dispatch({ type: "RESET" });
180
+ clearIdentity();
181
+ }, [clearIdentity]);
182
+ const fail = useCallback2((message) => {
183
+ dispatch({ type: "FAIL", message });
184
+ }, []);
185
+ return {
186
+ status: state.status,
187
+ seedWords: state.seedWords,
188
+ identity,
189
+ error: state.error,
190
+ generate,
191
+ confirmWords,
192
+ reset,
193
+ fail
194
+ };
195
+ }
196
+
197
+ // src/react/useImportSeed.ts
198
+ import { useReducer as useReducer2, useCallback as useCallback3 } from "react";
199
+ import {
200
+ validateSeedPhrase,
201
+ get32ByteSeedFromMnemonic as get32ByteSeedFromMnemonic2
202
+ } from "@me2em/core";
203
+ function useImportSeed() {
204
+ const { activateIdentity, clearIdentity, identity } = useMe2emContext();
205
+ const [state, dispatch] = useReducer2(identityFlowReducer, initialIdentityFlowState);
206
+ const importWords = useCallback3(
207
+ async (words, passphrase) => {
208
+ const validation = validateSeedPhrase(words);
209
+ if (!validation.isValid) {
210
+ dispatch({ type: "FAIL", message: validation.error ?? "Invalid seed phrase" });
211
+ return;
212
+ }
213
+ try {
214
+ const seed = await get32ByteSeedFromMnemonic2(words, passphrase ?? "");
215
+ dispatch({ type: "IMPORT", seed });
216
+ await activateIdentity(seed);
217
+ } catch (err) {
218
+ const message = err instanceof Error ? err.message : "Import failed";
219
+ dispatch({ type: "FAIL", message });
220
+ }
221
+ },
222
+ [activateIdentity]
223
+ );
224
+ const reset = useCallback3(() => {
225
+ dispatch({ type: "RESET" });
226
+ clearIdentity();
227
+ }, [clearIdentity]);
228
+ return {
229
+ status: state.status,
230
+ identity,
231
+ error: state.error,
232
+ importWords,
233
+ reset
234
+ };
235
+ }
236
+
237
+ // src/react/useHandle.ts
238
+ import { useState as useState2, useEffect } from "react";
239
+ function useHandle(name) {
240
+ const { identity } = useMe2emContext();
241
+ const [handle, setHandle] = useState2(null);
242
+ useEffect(() => {
243
+ let cancelled = false;
244
+ if (!identity) {
245
+ setHandle(null);
246
+ return;
247
+ }
248
+ identity.deriveHandle(name).then((h) => {
249
+ if (!cancelled) setHandle(h);
250
+ }).catch((err) => {
251
+ if (!cancelled) console.error("useHandle derivation failed", err);
252
+ });
253
+ return () => {
254
+ cancelled = true;
255
+ };
256
+ }, [identity, name]);
257
+ return handle;
258
+ }
259
+
260
+ // src/react/useSubHandle.ts
261
+ import { useState as useState3, useEffect as useEffect2 } from "react";
262
+ function useSubHandle(handleName, subName) {
263
+ const { identity } = useMe2emContext();
264
+ const [subHandle, setSubHandle] = useState3(null);
265
+ useEffect2(() => {
266
+ let cancelled = false;
267
+ if (!identity) {
268
+ setSubHandle(null);
269
+ return;
270
+ }
271
+ identity.deriveSubHandle(handleName, subName).then((h) => {
272
+ if (!cancelled) setSubHandle(h);
273
+ }).catch((err) => {
274
+ if (!cancelled) console.error("useSubHandle derivation failed", err);
275
+ });
276
+ return () => {
277
+ cancelled = true;
278
+ };
279
+ }, [identity, handleName, subName]);
280
+ return subHandle;
281
+ }
282
+
283
+ // src/react/SeedPhraseDisplay.tsx
284
+ import { useReducer as useReducer3, useState as useState4 } from "react";
285
+
286
+ // src/headless/display-flow.ts
287
+ var initialDisplayState = {
288
+ status: "hidden",
289
+ copyCount: 0,
290
+ revealedAt: null
291
+ };
292
+ function displayFlowReducer(state, event) {
293
+ const { type } = event;
294
+ switch (state.status) {
295
+ case "hidden": {
296
+ if (type === "REVEAL") {
297
+ return {
298
+ status: "revealed",
299
+ copyCount: 0,
300
+ revealedAt: event.at
301
+ };
302
+ }
303
+ if (type === "COPY") {
304
+ console.warn("Cannot copy while hidden");
305
+ return state;
306
+ }
307
+ if (type === "RESET") {
308
+ return initialDisplayState;
309
+ }
310
+ if (type === "DISMISS") {
311
+ console.warn("Cannot dismiss while hidden");
312
+ return state;
313
+ }
314
+ console.warn(`Unexpected event ${type} in hidden`);
315
+ return state;
316
+ }
317
+ case "revealed": {
318
+ if (type === "COPY") {
319
+ if (state.copyCount === 0) {
320
+ return {
321
+ status: "copied",
322
+ copyCount: 1,
323
+ revealedAt: state.revealedAt
324
+ };
325
+ }
326
+ console.warn("Already copied \u2014 duplicate copy blocked");
327
+ return state;
328
+ }
329
+ if (type === "DISMISS") {
330
+ return {
331
+ status: "dismissed",
332
+ copyCount: state.copyCount,
333
+ revealedAt: state.revealedAt
334
+ };
335
+ }
336
+ if (type === "RESET") {
337
+ return initialDisplayState;
338
+ }
339
+ console.warn(`Unexpected event ${type} in revealed`);
340
+ return state;
341
+ }
342
+ case "copied": {
343
+ if (type === "COPY") {
344
+ console.warn("Already copied \u2014 duplicate copy blocked");
345
+ return state;
346
+ }
347
+ if (type === "DISMISS") {
348
+ return {
349
+ status: "dismissed",
350
+ copyCount: 1,
351
+ revealedAt: state.revealedAt
352
+ };
353
+ }
354
+ if (type === "RESET") {
355
+ return initialDisplayState;
356
+ }
357
+ console.warn(`Unexpected event ${type} in copied`);
358
+ return state;
359
+ }
360
+ case "dismissed": {
361
+ if (type === "RESET") {
362
+ return initialDisplayState;
363
+ }
364
+ console.warn(`Unexpected event ${type} in dismissed`);
365
+ return state;
366
+ }
367
+ default:
368
+ return state;
369
+ }
370
+ }
371
+
372
+ // src/react/SeedPhraseDisplay.tsx
373
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
374
+ function SeedPhraseDisplay({
375
+ words,
376
+ onDismiss,
377
+ onCopyAllowed
378
+ }) {
379
+ const [state, dispatch] = useReducer3(displayFlowReducer, initialDisplayState);
380
+ const [copyFailed, setCopyFailed] = useState4(false);
381
+ const { status, copyCount } = state;
382
+ const handleReveal = () => {
383
+ dispatch({ type: "REVEAL", at: Date.now() });
384
+ };
385
+ const handleCopy = async () => {
386
+ if (status === "revealed" && copyCount === 0) {
387
+ try {
388
+ await navigator.clipboard.writeText(words.join(" "));
389
+ dispatch({ type: "COPY", at: Date.now() });
390
+ setCopyFailed(false);
391
+ onCopyAllowed?.();
392
+ } catch {
393
+ setCopyFailed(true);
394
+ }
395
+ }
396
+ };
397
+ const handleDismiss = () => {
398
+ dispatch({ type: "DISMISS" });
399
+ onDismiss?.();
400
+ };
401
+ if (status === "hidden") {
402
+ return /* @__PURE__ */ jsx2("button", { "data-testid": "reveal", onClick: handleReveal, children: "Reveal" });
403
+ }
404
+ if (status === "dismissed") {
405
+ return /* @__PURE__ */ jsx2("div", { "data-testid": "dismissed", children: "Phrase dismissed" });
406
+ }
407
+ return /* @__PURE__ */ jsxs("div", { children: [
408
+ /* @__PURE__ */ jsx2("ol", { "data-testid": "words", children: words.map((word, i) => /* @__PURE__ */ jsx2(
409
+ "li",
410
+ {
411
+ "data-testid": `word-${i}`,
412
+ "data-position": i,
413
+ children: word
414
+ },
415
+ i
416
+ )) }),
417
+ copyFailed && /* @__PURE__ */ jsx2("div", { "data-testid": "copy-error", children: "Copy failed \u2014 try again" }),
418
+ /* @__PURE__ */ jsx2(
419
+ "button",
420
+ {
421
+ "data-testid": "copy-all",
422
+ onClick: handleCopy,
423
+ disabled: copyCount === 1,
424
+ children: copyCount === 1 ? "Copied" : "Copy all"
425
+ }
426
+ ),
427
+ /* @__PURE__ */ jsx2("button", { "data-testid": "dismiss", onClick: handleDismiss, children: "Hide" })
428
+ ] });
429
+ }
430
+
431
+ // src/react/SeedPhraseVerify.tsx
432
+ import { useReducer as useReducer4, useCallback as useCallback4, useState as useState5, useEffect as useEffect3, useRef } from "react";
433
+
434
+ // src/headless/seed-words.ts
435
+ function shuffleWithSeed(items, seed) {
436
+ const arr = [...items];
437
+ let currentSeed = seed;
438
+ function nextRand() {
439
+ currentSeed = currentSeed * 1664525 + 1013904223 >>> 0;
440
+ return currentSeed;
441
+ }
442
+ for (let i = arr.length - 1; i > 0; i--) {
443
+ const j = nextRand() % (i + 1);
444
+ [arr[i], arr[j]] = [arr[j], arr[i]];
445
+ }
446
+ return arr;
447
+ }
448
+ function buildVerificationGrid(realWords, gridSize, randomSeed, allWords) {
449
+ if (gridSize < realWords.length) {
450
+ throw new Error(
451
+ `gridSize (${gridSize}) must be >= realWords.length (${realWords.length})`
452
+ );
453
+ }
454
+ const shuffled = shuffleWithSeed(realWords, randomSeed);
455
+ const realSet = new Set(realWords);
456
+ const available = allWords.filter((w) => !realSet.has(w));
457
+ const shuffledDecoys = shuffleWithSeed(available, randomSeed + 1);
458
+ const needed = gridSize - realWords.length;
459
+ const decoys = shuffledDecoys.slice(0, needed);
460
+ const words = [...shuffled, ...decoys];
461
+ if (words.length !== gridSize) {
462
+ throw new Error(
463
+ `Cannot fill grid: wordlist too small (got ${words.length}, need ${gridSize})`
464
+ );
465
+ }
466
+ const positions = /* @__PURE__ */ new Map();
467
+ words.forEach((w, i) => {
468
+ const list = positions.get(w);
469
+ if (list) list.push(i);
470
+ else positions.set(w, [i]);
471
+ });
472
+ const correctIndices = realWords.map((w) => {
473
+ const list = positions.get(w);
474
+ if (!list || list.length === 0) {
475
+ throw new Error(`Internal error: word "${w}" missing from grid`);
476
+ }
477
+ return list.shift();
478
+ });
479
+ return { words, correctIndices };
480
+ }
481
+ function isGridSelectionCorrect(grid, selected) {
482
+ if (selected.length !== grid.correctIndices.length) return false;
483
+ return selected.every((v, i) => v === grid.correctIndices[i]);
484
+ }
485
+
486
+ // src/headless/verify-flow.ts
487
+ function initVerifyState(realWords, gridSize, randomSeed, allWords) {
488
+ const grid = buildVerificationGrid(realWords, gridSize, randomSeed, allWords);
489
+ return {
490
+ status: "collecting",
491
+ grid,
492
+ selected: [],
493
+ attempts: 0
494
+ };
495
+ }
496
+ function verifyFlowReducer(state, event) {
497
+ const { type } = event;
498
+ if (type === "RESET") {
499
+ return initVerifyState(
500
+ event.realWords,
501
+ event.gridSize,
502
+ event.randomSeed,
503
+ event.allWords
504
+ );
505
+ }
506
+ if (state.status === "correct" || state.status === "incorrect") {
507
+ console.warn(
508
+ `Cannot ${type} in ${state.status} state \u2014 use RESET to retry`
509
+ );
510
+ return state;
511
+ }
512
+ if (type === "SELECT") {
513
+ const { index } = event;
514
+ const { grid, selected } = state;
515
+ if (selected.includes(index)) {
516
+ console.warn(`Index ${index} already selected \u2014 use DESELECT`);
517
+ return state;
518
+ }
519
+ if (index < 0 || index >= grid.words.length) {
520
+ console.warn(`Index ${index} out of grid bounds`);
521
+ return state;
522
+ }
523
+ return { ...state, selected: [...selected, index] };
524
+ }
525
+ if (type === "DESELECT") {
526
+ const { index } = event;
527
+ const idx = state.selected.indexOf(index);
528
+ if (idx === -1) {
529
+ console.warn(`Index ${index} not selected`);
530
+ return state;
531
+ }
532
+ return {
533
+ ...state,
534
+ selected: state.selected.filter((_, i) => i !== idx)
535
+ };
536
+ }
537
+ if (type === "CHECK") {
538
+ const { grid, selected, attempts } = state;
539
+ if (selected.length !== grid.correctIndices.length) {
540
+ console.warn(
541
+ `Cannot check: selected ${selected.length}, need ${grid.correctIndices.length}`
542
+ );
543
+ return state;
544
+ }
545
+ const correct = isGridSelectionCorrect(grid, selected);
546
+ return {
547
+ ...state,
548
+ status: correct ? "correct" : "incorrect",
549
+ attempts: attempts + 1
550
+ };
551
+ }
552
+ console.warn(`Unexpected event ${type} in collecting`);
553
+ return state;
554
+ }
555
+
556
+ // src/react/SeedPhraseVerify.tsx
557
+ import { wordlist } from "@me2em/core";
558
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
559
+ var DEFAULT_GRID_SIZE_OFFSET = 8;
560
+ function SeedPhraseVerify({
561
+ realWords,
562
+ gridSize,
563
+ wordlist: propWordlist,
564
+ onVerified,
565
+ onFailed
566
+ }) {
567
+ const grid = gridSize ?? realWords.length + DEFAULT_GRID_SIZE_OFFSET;
568
+ const allWords = propWordlist ?? wordlist;
569
+ const baseSeed = 42;
570
+ const [state, dispatch] = useReducer4(
571
+ verifyFlowReducer,
572
+ {
573
+ realWords,
574
+ gridSize: grid,
575
+ randomSeed: baseSeed,
576
+ allWords
577
+ },
578
+ (params) => initVerifyState(params.realWords, params.gridSize, params.randomSeed, params.allWords)
579
+ );
580
+ const [retryCount, setRetryCount] = useState5(0);
581
+ const { status, grid: gridData, selected } = state;
582
+ const notifiedRef = useRef(false);
583
+ const handleSelect = useCallback4(
584
+ (index) => {
585
+ if (status === "correct" || status === "incorrect") return;
586
+ if (selected.length >= gridData.correctIndices.length) return;
587
+ dispatch({ type: "SELECT", index });
588
+ },
589
+ [status, selected.length, gridData.correctIndices.length]
590
+ );
591
+ useEffect3(() => {
592
+ if (notifiedRef.current) return;
593
+ if (selected.length === gridData.correctIndices.length && selected.length > 0 && status === "collecting") {
594
+ dispatch({ type: "CHECK" });
595
+ return;
596
+ }
597
+ if (status === "correct") {
598
+ notifiedRef.current = true;
599
+ onVerified();
600
+ } else if (status === "incorrect") {
601
+ notifiedRef.current = true;
602
+ onFailed?.(state.attempts);
603
+ }
604
+ }, [selected.length, gridData.correctIndices.length, status, state.attempts, onVerified, onFailed]);
605
+ const handleRetry = () => {
606
+ notifiedRef.current = false;
607
+ setRetryCount((c) => c + 1);
608
+ dispatch({
609
+ type: "RESET",
610
+ realWords,
611
+ gridSize: grid,
612
+ randomSeed: baseSeed + retryCount + 1,
613
+ allWords
614
+ });
615
+ };
616
+ return /* @__PURE__ */ jsxs2("div", { children: [
617
+ /* @__PURE__ */ jsxs2("div", { "data-testid": "progress", children: [
618
+ "Select word ",
619
+ selected.length + 1,
620
+ " of ",
621
+ realWords.length
622
+ ] }),
623
+ /* @__PURE__ */ jsx3("ol", { "data-testid": "grid", children: gridData.words.map((word, i) => {
624
+ const isSelected = selected.includes(i);
625
+ const isDisabled = isSelected || status === "correct" || status === "incorrect";
626
+ return /* @__PURE__ */ jsx3("li", { children: /* @__PURE__ */ jsx3(
627
+ "button",
628
+ {
629
+ "data-testid": `cell-${i}`,
630
+ "data-word": word,
631
+ disabled: isDisabled,
632
+ onClick: () => handleSelect(i),
633
+ children: word
634
+ }
635
+ ) }, i);
636
+ }) }),
637
+ status === "incorrect" && /* @__PURE__ */ jsx3("button", { "data-testid": "retry", onClick: handleRetry, children: "Try again" })
638
+ ] });
639
+ }
640
+
641
+ // src/react/SeedPhraseImport.tsx
642
+ import { useReducer as useReducer5, useEffect as useEffect4, useRef as useRef2 } from "react";
643
+
644
+ // src/headless/import-flow.ts
645
+ import { validateSeedPhrase as validateSeedPhrase2 } from "@me2em/core";
646
+ function initImportState(expectedCount) {
647
+ return {
648
+ status: "entering",
649
+ words: Array(expectedCount).fill(null),
650
+ expectedCount
651
+ };
652
+ }
653
+ function importFlowReducer(state, event) {
654
+ const { type } = event;
655
+ if (type === "INIT") {
656
+ return initImportState(event.expectedCount);
657
+ }
658
+ if (type === "RESET") {
659
+ return initImportState(state.expectedCount);
660
+ }
661
+ if (type === "SET_WORD") {
662
+ const { position, word } = event;
663
+ if (position < 0 || position >= state.expectedCount) {
664
+ console.warn(`SET_WORD: position ${position} out of bounds`);
665
+ return state;
666
+ }
667
+ const normalized = word.toLowerCase().trim();
668
+ const newWords = [...state.words];
669
+ newWords[position] = normalized;
670
+ return { ...state, words: newWords, status: "entering" };
671
+ }
672
+ if (type === "CLEAR_WORD") {
673
+ const { position } = event;
674
+ if (position < 0 || position >= state.expectedCount) {
675
+ console.warn(`CLEAR_WORD: position ${position} out of bounds`);
676
+ return state;
677
+ }
678
+ const newWords = [...state.words];
679
+ newWords[position] = null;
680
+ return { ...state, words: newWords, status: "entering" };
681
+ }
682
+ if (type === "CHECK") {
683
+ const { words } = state;
684
+ const nullIdx = words.findIndex((w) => w === null);
685
+ if (nullIdx !== -1) {
686
+ return {
687
+ ...state,
688
+ status: "invalid"
689
+ };
690
+ }
691
+ const result = validateSeedPhrase2(words);
692
+ return {
693
+ ...state,
694
+ status: result.isValid ? "valid" : "invalid"
695
+ };
696
+ }
697
+ console.warn(`Unexpected event ${type} in import-flow`);
698
+ return state;
699
+ }
700
+
701
+ // src/react/SeedPhraseImport.tsx
702
+ import { wordlist as wordlist2 } from "@me2em/core";
703
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
704
+ function SeedPhraseImport({
705
+ expectedCount: propCount,
706
+ wordlist: propWordlist,
707
+ onImported
708
+ }) {
709
+ const allWords = propWordlist ?? wordlist2;
710
+ const [state, dispatch] = useReducer5(
711
+ importFlowReducer,
712
+ { expectedCount: propCount ?? 12 },
713
+ (params) => initImportState(params.expectedCount)
714
+ );
715
+ const count = state.expectedCount;
716
+ const prevStatusRef = useRef2(state.status);
717
+ const lastImportedRef = useRef2(null);
718
+ useEffect4(() => {
719
+ dispatch({ type: "CHECK" });
720
+ }, [state.words]);
721
+ useEffect4(() => {
722
+ const prevStatus = prevStatusRef.current;
723
+ prevStatusRef.current = state.status;
724
+ if (state.status === "valid" && prevStatus !== "valid") {
725
+ const key = state.words.join("\0");
726
+ if (lastImportedRef.current !== key) {
727
+ lastImportedRef.current = key;
728
+ onImported(state.words);
729
+ }
730
+ }
731
+ }, [state.status, state.words, onImported]);
732
+ const handleCountChange = (n) => {
733
+ dispatch({ type: "INIT", expectedCount: n });
734
+ };
735
+ const handleWordChange = (position, value) => {
736
+ dispatch({ type: "SET_WORD", position, word: value });
737
+ };
738
+ const isWordInvalid = (position) => {
739
+ if (state.status !== "invalid" && state.status !== "valid") return false;
740
+ const word = state.words[position];
741
+ if (word === null) return false;
742
+ return !allWords.includes(word);
743
+ };
744
+ const isImportEnabled = state.status === "valid";
745
+ return /* @__PURE__ */ jsxs3("div", { children: [
746
+ /* @__PURE__ */ jsxs3("div", { children: [
747
+ /* @__PURE__ */ jsx4(
748
+ "button",
749
+ {
750
+ "data-testid": "count-12",
751
+ onClick: () => handleCountChange(12),
752
+ disabled: count === 12,
753
+ children: "12 words"
754
+ }
755
+ ),
756
+ /* @__PURE__ */ jsx4(
757
+ "button",
758
+ {
759
+ "data-testid": "count-24",
760
+ onClick: () => handleCountChange(24),
761
+ disabled: count === 24,
762
+ children: "24 words"
763
+ }
764
+ )
765
+ ] }),
766
+ /* @__PURE__ */ jsx4("div", { "data-testid": "inputs", children: Array.from({ length: count }, (_, i) => /* @__PURE__ */ jsx4(
767
+ "input",
768
+ {
769
+ "data-testid": `word-${i}`,
770
+ "aria-label": `Word ${i + 1}`,
771
+ value: state.words[i] ?? "",
772
+ onChange: (e) => handleWordChange(i, e.target.value),
773
+ "data-invalid": isWordInvalid(i) ? "true" : void 0
774
+ },
775
+ i
776
+ )) }),
777
+ /* @__PURE__ */ jsx4(
778
+ "button",
779
+ {
780
+ "data-testid": "import",
781
+ disabled: !isImportEnabled,
782
+ onClick: () => dispatch({ type: "CHECK" }),
783
+ children: "Import"
784
+ }
785
+ )
786
+ ] });
787
+ }
788
+
789
+ // src/react/PassphraseInput.tsx
790
+ import { useCallback as useCallback6 } from "react";
791
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
792
+ function PassphraseInput({
793
+ onPassphraseChange,
794
+ showWarnings = true
795
+ }) {
796
+ const handleChange = useCallback6(
797
+ (e) => {
798
+ const value = e.target.value.normalize("NFKC");
799
+ onPassphraseChange(value);
800
+ },
801
+ [onPassphraseChange]
802
+ );
803
+ return /* @__PURE__ */ jsxs4("div", { children: [
804
+ /* @__PURE__ */ jsx5(
805
+ "input",
806
+ {
807
+ type: "password",
808
+ "data-testid": "passphrase",
809
+ "aria-label": "BIP39 passphrase",
810
+ onChange: handleChange
811
+ }
812
+ ),
813
+ showWarnings && /* @__PURE__ */ jsx5("div", { "data-testid": "warning", children: "Passphrase is case-sensitive and NOT recoverable. A different passphrase silently derives a different wallet." })
814
+ ] });
815
+ }
816
+
817
+ // src/react/useIdentityContext.ts
818
+ import { useState as useState7, useCallback as useCallback7, useEffect as useEffect5, useRef as useRef3 } from "react";
819
+ function publicKeyToId(publicKey) {
820
+ return Array.from(publicKey).map((b) => b.toString(16).padStart(2, "0")).join("");
821
+ }
822
+ function useIdentityContext(storage) {
823
+ const { identity } = useMe2emContext();
824
+ const [context, setContext] = useState7(null);
825
+ const [isLoaded, setIsLoaded] = useState7(false);
826
+ const [error, setError] = useState7(null);
827
+ const prevIdentityIdRef = useRef3(null);
828
+ useEffect5(() => {
829
+ let cancelled = false;
830
+ if (!identity) {
831
+ setContext(null);
832
+ setIsLoaded(true);
833
+ setError(null);
834
+ return;
835
+ }
836
+ const identityId = publicKeyToId(identity.getPublicKey());
837
+ if (prevIdentityIdRef.current !== null && prevIdentityIdRef.current !== identityId) {
838
+ storage.close().catch(() => {
839
+ });
840
+ }
841
+ prevIdentityIdRef.current = identityId;
842
+ storage.openAndLoad(identityId).then((data) => {
843
+ if (cancelled) return;
844
+ setContext(data);
845
+ setIsLoaded(true);
846
+ setError(null);
847
+ }).catch((err) => {
848
+ if (cancelled) return;
849
+ setError(err instanceof Error ? err.message : String(err));
850
+ setIsLoaded(true);
851
+ });
852
+ return () => {
853
+ cancelled = true;
854
+ };
855
+ }, [identity, storage]);
856
+ const save = useCallback7(async (data) => {
857
+ await storage.save(data);
858
+ setContext(data);
859
+ }, [storage]);
860
+ const clear = useCallback7(async () => {
861
+ await storage.clear();
862
+ setContext(null);
863
+ }, [storage]);
864
+ const refresh = useCallback7(async () => {
865
+ if (!identity) return;
866
+ const identityId = publicKeyToId(identity.getPublicKey());
867
+ const data = await storage.openAndLoad(identityId);
868
+ setContext(data);
869
+ }, [identity, storage]);
870
+ return { context, isLoaded, error, save, clear, refresh };
871
+ }
872
+
873
+ // src/react/useSession.ts
874
+ import { useState as useState8, useEffect as useEffect6, useCallback as useCallback8, useRef as useRef4 } from "react";
875
+ import { Session } from "@me2em/core";
876
+ function useSession(handle, options, autoRenew = true) {
877
+ const { identity } = useMe2emContext();
878
+ const [session, setSession] = useState8(null);
879
+ const [isLoaded, setIsLoaded] = useState8(false);
880
+ const [error, setError] = useState8(null);
881
+ const timerRef = useRef4(null);
882
+ const optionsRef = useRef4(options);
883
+ const mountedRef = useRef4(true);
884
+ optionsRef.current = options;
885
+ const clearTimer = useCallback8(() => {
886
+ if (timerRef.current) {
887
+ clearTimeout(timerRef.current);
888
+ timerRef.current = null;
889
+ }
890
+ }, []);
891
+ useEffect6(() => {
892
+ mountedRef.current = true;
893
+ return () => {
894
+ mountedRef.current = false;
895
+ clearTimer();
896
+ };
897
+ }, [clearTimer]);
898
+ const renew = useCallback8(() => {
899
+ clearTimer();
900
+ const opts = optionsRef.current;
901
+ if (!handle) {
902
+ if (!mountedRef.current) return;
903
+ setSession(null);
904
+ setIsLoaded(true);
905
+ setError(null);
906
+ return;
907
+ }
908
+ Session.create(handle, {
909
+ audience: opts.audience,
910
+ scopes: opts.scopes,
911
+ ttl: opts.ttl
912
+ }).then((s) => {
913
+ if (!mountedRef.current) return;
914
+ setSession(s);
915
+ setIsLoaded(true);
916
+ setError(null);
917
+ if (autoRenew) {
918
+ const halfLife = Math.floor((s.expiresAt * 1e3 - Date.now()) / 2);
919
+ if (halfLife > 0) {
920
+ timerRef.current = setTimeout(() => {
921
+ renew();
922
+ }, Math.max(halfLife, 1e3));
923
+ }
924
+ }
925
+ }).catch((err) => {
926
+ if (!mountedRef.current) return;
927
+ setSession(null);
928
+ setIsLoaded(true);
929
+ setError(err instanceof Error ? err.message : String(err));
930
+ });
931
+ }, [handle, autoRenew, clearTimer]);
932
+ useEffect6(() => {
933
+ if (mountedRef.current) {
934
+ renew();
935
+ }
936
+ }, [handle, renew]);
937
+ return {
938
+ session,
939
+ isLoaded,
940
+ error,
941
+ isExpired: session ? session.isExpired() : false,
942
+ renew
943
+ };
944
+ }
945
+
946
+ // src/headless/index.ts
947
+ var headless_exports = {};
948
+ __export(headless_exports, {
949
+ IdentityContextIDBStorage: () => IdentityContextIDBStorage,
950
+ buildVerificationGrid: () => buildVerificationGrid,
951
+ decryptContext: () => decryptContext,
952
+ deriveCacheKey: () => deriveCacheKey,
953
+ displayFlowReducer: () => displayFlowReducer,
954
+ encryptContext: () => encryptContext,
955
+ identityFlowReducer: () => identityFlowReducer,
956
+ importFlowReducer: () => importFlowReducer,
957
+ initImportState: () => initImportState,
958
+ initVerifyState: () => initVerifyState,
959
+ initialDisplayState: () => initialDisplayState,
960
+ initialIdentityFlowState: () => initialIdentityFlowState,
961
+ isGridSelectionCorrect: () => isGridSelectionCorrect,
962
+ shuffleWithSeed: () => shuffleWithSeed,
963
+ verifyFlowReducer: () => verifyFlowReducer
964
+ });
965
+
966
+ // src/headless/context-crypto.ts
967
+ function toB64(bytes) {
968
+ let binary = "";
969
+ for (const byte of bytes) {
970
+ binary += String.fromCharCode(byte);
971
+ }
972
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
973
+ }
974
+ function fromB64(s) {
975
+ let base64 = s.replace(/-/g, "+").replace(/_/g, "/");
976
+ while (base64.length % 4 !== 0) {
977
+ base64 += "=";
978
+ }
979
+ const binary = atob(base64);
980
+ const bytes = new Uint8Array(binary.length);
981
+ for (let i = 0; i < binary.length; i++) {
982
+ bytes[i] = binary.charCodeAt(i);
983
+ }
984
+ return bytes;
985
+ }
986
+ async function deriveCacheKey(loginMaterial, salt) {
987
+ const base = await crypto.subtle.importKey(
988
+ "raw",
989
+ loginMaterial,
990
+ "HKDF",
991
+ false,
992
+ ["deriveBits"]
993
+ );
994
+ const bits = await crypto.subtle.deriveBits(
995
+ {
996
+ name: "HKDF",
997
+ hash: "SHA-256",
998
+ salt,
999
+ info: new TextEncoder().encode("me2em/cache-wrap/v1")
1000
+ },
1001
+ base,
1002
+ 256
1003
+ );
1004
+ return crypto.subtle.importKey(
1005
+ "raw",
1006
+ bits,
1007
+ { name: "AES-GCM" },
1008
+ false,
1009
+ ["encrypt", "decrypt"]
1010
+ );
1011
+ }
1012
+ async function encryptContext(key, data) {
1013
+ const iv = crypto.getRandomValues(new Uint8Array(12));
1014
+ const salt = crypto.getRandomValues(new Uint8Array(32));
1015
+ const pt = new TextEncoder().encode(JSON.stringify(data));
1016
+ const ct = await crypto.subtle.encrypt(
1017
+ { name: "AES-GCM", iv },
1018
+ key,
1019
+ pt
1020
+ );
1021
+ return {
1022
+ v: 1,
1023
+ salt: toB64(salt),
1024
+ iv: toB64(iv),
1025
+ ct: toB64(new Uint8Array(ct))
1026
+ };
1027
+ }
1028
+ async function decryptContext(key, envelope) {
1029
+ if (envelope.v !== 1) throw new Error("Unsupported envelope version");
1030
+ const pt = await crypto.subtle.decrypt(
1031
+ { name: "AES-GCM", iv: fromB64(envelope.iv) },
1032
+ key,
1033
+ fromB64(envelope.ct)
1034
+ );
1035
+ return JSON.parse(new TextDecoder().decode(pt));
1036
+ }
1037
+
1038
+ // src/headless/identity-context-storage.ts
1039
+ var DB_NAME_PREFIX = "me2em_";
1040
+ var STORE_NAME = "context";
1041
+ var KEYS_STORE_NAME = "cryptoKeys";
1042
+ var CACHE_KEY = "cache";
1043
+ function hashToHex(data) {
1044
+ const hexParts = [];
1045
+ for (const byte of data) {
1046
+ hexParts.push(byte.toString(16).padStart(2, "0"));
1047
+ }
1048
+ return hexParts.join("");
1049
+ }
1050
+ async function sha256Hex(data) {
1051
+ const buffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
1052
+ const digest = await crypto.subtle.digest("SHA-256", buffer);
1053
+ return hashToHex(new Uint8Array(digest));
1054
+ }
1055
+ async function dbNameForIdentity(identityId) {
1056
+ const hash = await sha256Hex(new TextEncoder().encode(identityId));
1057
+ return DB_NAME_PREFIX + hash.slice(0, 16);
1058
+ }
1059
+ var IdentityContextIDBStorage = class {
1060
+ db = null;
1061
+ currentIdentityId = null;
1062
+ cacheKey = null;
1063
+ async getDb() {
1064
+ if (!this.db) {
1065
+ throw new Error("Storage not opened \u2014 call open() first");
1066
+ }
1067
+ return this.db;
1068
+ }
1069
+ async open(identityId) {
1070
+ if (this.currentIdentityId !== null && this.currentIdentityId !== identityId) {
1071
+ await this.close();
1072
+ }
1073
+ this.currentIdentityId = identityId;
1074
+ const name = await dbNameForIdentity(identityId);
1075
+ return new Promise((resolve, reject) => {
1076
+ const request = indexedDB.open(name, 1);
1077
+ request.onupgradeneeded = () => {
1078
+ const db = request.result;
1079
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
1080
+ db.createObjectStore(STORE_NAME, { keyPath: "key" });
1081
+ }
1082
+ if (!db.objectStoreNames.contains(KEYS_STORE_NAME)) {
1083
+ db.createObjectStore(KEYS_STORE_NAME, { keyPath: "key" });
1084
+ }
1085
+ };
1086
+ request.onsuccess = () => {
1087
+ this.db = request.result;
1088
+ resolve();
1089
+ };
1090
+ request.onerror = () => {
1091
+ reject(new Error("IndexedDB open failed: " + request.error?.message));
1092
+ };
1093
+ });
1094
+ }
1095
+ async openAndLoad(identityId) {
1096
+ await this.open(identityId);
1097
+ const key = this.cacheKey ?? await this.loadPersistedKey();
1098
+ if (!key) {
1099
+ return null;
1100
+ }
1101
+ this.cacheKey = key;
1102
+ return this.load();
1103
+ }
1104
+ async save(data) {
1105
+ const key = this.cacheKey;
1106
+ if (!key) {
1107
+ throw new Error("No cache key set \u2014 call setCacheKey before save");
1108
+ }
1109
+ const envelope = await encryptContext(key, data);
1110
+ const db = await this.getDb();
1111
+ return new Promise((resolve, reject) => {
1112
+ const tx = db.transaction(STORE_NAME, "readwrite");
1113
+ const store = tx.objectStore(STORE_NAME);
1114
+ store.put({ key: CACHE_KEY, envelope });
1115
+ tx.oncomplete = () => resolve();
1116
+ tx.onerror = () => reject(new Error("Save failed: " + tx.error?.message));
1117
+ });
1118
+ }
1119
+ async load() {
1120
+ const db = await this.getDb();
1121
+ const key = this.cacheKey;
1122
+ if (!key) {
1123
+ return null;
1124
+ }
1125
+ return new Promise((resolve, reject) => {
1126
+ const tx = db.transaction(STORE_NAME, "readonly");
1127
+ const store = tx.objectStore(STORE_NAME);
1128
+ const request = store.get(CACHE_KEY);
1129
+ request.onsuccess = () => {
1130
+ const record = request.result;
1131
+ if (!record || !record.envelope) {
1132
+ resolve(null);
1133
+ return;
1134
+ }
1135
+ decryptContext(key, record.envelope).then((data) => {
1136
+ if (data.identityId !== this.currentIdentityId) {
1137
+ resolve(null);
1138
+ return;
1139
+ }
1140
+ resolve(data);
1141
+ }).catch(() => {
1142
+ resolve(null);
1143
+ });
1144
+ };
1145
+ request.onerror = () => reject(new Error("Load failed: " + request.error?.message));
1146
+ });
1147
+ }
1148
+ async clear() {
1149
+ const db = await this.getDb();
1150
+ return new Promise((resolve, reject) => {
1151
+ const tx = db.transaction(STORE_NAME, "readwrite");
1152
+ const store = tx.objectStore(STORE_NAME);
1153
+ store.delete(CACHE_KEY);
1154
+ tx.oncomplete = () => resolve();
1155
+ tx.onerror = () => reject(new Error("Clear failed: " + tx.error?.message));
1156
+ });
1157
+ }
1158
+ async close() {
1159
+ const db = this.db;
1160
+ this.db = null;
1161
+ this.currentIdentityId = null;
1162
+ this.cacheKey = null;
1163
+ if (db) {
1164
+ db.close();
1165
+ }
1166
+ }
1167
+ setCacheKey(key) {
1168
+ this.cacheKey = key;
1169
+ }
1170
+ getCacheKey() {
1171
+ return this.cacheKey;
1172
+ }
1173
+ async persistCacheKey() {
1174
+ const key = this.cacheKey;
1175
+ if (!key) {
1176
+ throw new Error("No cache key to persist \u2014 call setCacheKey first");
1177
+ }
1178
+ const db = await this.getDb();
1179
+ return new Promise((resolve, reject) => {
1180
+ const tx = db.transaction(KEYS_STORE_NAME, "readwrite");
1181
+ const store = tx.objectStore(KEYS_STORE_NAME);
1182
+ store.put({ key: "cache", cryptoKey: key });
1183
+ tx.oncomplete = () => resolve();
1184
+ tx.onerror = () => reject(new Error("persistCacheKey failed: " + tx.error?.message));
1185
+ });
1186
+ }
1187
+ async loadPersistedKey() {
1188
+ const db = this.db;
1189
+ if (!db) {
1190
+ return null;
1191
+ }
1192
+ return new Promise((resolve, reject) => {
1193
+ const tx = db.transaction(KEYS_STORE_NAME, "readonly");
1194
+ const store = tx.objectStore(KEYS_STORE_NAME);
1195
+ const request = store.get("cache");
1196
+ request.onsuccess = () => {
1197
+ const record = request.result;
1198
+ if (!record || !record.cryptoKey) {
1199
+ resolve(null);
1200
+ return;
1201
+ }
1202
+ resolve(record.cryptoKey);
1203
+ };
1204
+ request.onerror = () => {
1205
+ resolve(null);
1206
+ };
1207
+ });
1208
+ }
1209
+ };
1210
+ export {
1211
+ Me2emProvider,
1212
+ PassphraseInput,
1213
+ SeedPhraseDisplay,
1214
+ SeedPhraseImport,
1215
+ SeedPhraseVerify,
1216
+ headless_exports as headless,
1217
+ useCreateIdentity,
1218
+ useHandle,
1219
+ useIdentityContext,
1220
+ useImportSeed,
1221
+ useMe2emContext,
1222
+ useSession,
1223
+ useSubHandle
1224
+ };
1225
+ //# sourceMappingURL=index.js.map