@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/LICENSE +201 -0
- package/README.md +195 -0
- package/dist/index.d.ts +367 -0
- package/dist/index.js +1225 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
- package/src/headless/context-crypto.ts +91 -0
- package/src/headless/context-key-store.ts +124 -0
- package/src/headless/display-flow.ts +113 -0
- package/src/headless/identity-context-storage.ts +205 -0
- package/src/headless/identity-context-types.ts +30 -0
- package/src/headless/identity-flow.ts +106 -0
- package/src/headless/import-flow.ts +82 -0
- package/src/headless/index.ts +53 -0
- package/src/headless/seed-words.ts +75 -0
- package/src/headless/types.ts +21 -0
- package/src/headless/verify-flow.ts +112 -0
- package/src/index.ts +2 -0
- package/src/react/Me2emProvider.tsx +33 -0
- package/src/react/PassphraseInput.tsx +37 -0
- package/src/react/SeedPhraseDisplay.tsx +92 -0
- package/src/react/SeedPhraseImport.tsx +110 -0
- package/src/react/SeedPhraseVerify.tsx +128 -0
- package/src/react/context.ts +19 -0
- package/src/react/index.ts +23 -0
- package/src/react/useCreateIdentity.ts +77 -0
- package/src/react/useHandle.ts +27 -0
- package/src/react/useIdentityContext.ts +88 -0
- package/src/react/useImportSeed.ts +60 -0
- package/src/react/useSession.ts +157 -0
- package/src/react/useSubHandle.ts +30 -0
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
|