@me2em/react 0.1.0-alpha.2 → 1.0.0-rc.2
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/README.md +16 -7
- package/dist/index.d.ts +61 -1
- package/dist/index.js +96 -72
- package/dist/index.js.map +1 -1
- package/package.json +11 -6
- package/src/headless/context-crypto.ts +13 -30
- package/src/headless/identity-context-storage.ts +2 -2
- package/src/headless/identity-context-types.ts +9 -0
- package/src/headless/import-flow.ts +1 -1
- package/src/headless/types.ts +7 -0
- package/src/react/Me2emProvider.tsx +40 -0
- package/src/react/PassphraseInput.tsx +1 -1
- package/src/react/SeedPhraseDisplay.tsx +29 -8
- package/src/react/SeedPhraseImport.tsx +1 -1
- package/src/react/SeedPhraseVerify.tsx +9 -1
- package/src/react/context.ts +16 -0
- package/src/react/useCreateIdentity.ts +2 -2
- package/src/react/useHandle.ts +9 -0
- package/src/react/useIdentityContext.ts +37 -1
- package/src/react/useImportSeed.ts +2 -2
- package/src/react/useSession.ts +7 -5
- package/src/react/useSubHandle.ts +9 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## 📦 @me2em/react — React bindings
|
|
6
6
|
|
|
7
|
-
React bindings for the
|
|
7
|
+
React bindings for the Me2eM protocol: identity lifecycle, seed phrase UX, and hierarchical handle derivation.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -24,13 +24,13 @@ Requires React >= 18.0.0 as a peer dependency.
|
|
|
24
24
|
## Quick Start
|
|
25
25
|
|
|
26
26
|
```tsx
|
|
27
|
-
import {
|
|
27
|
+
import { Me2eMProvider, useCreateIdentity } from '@me2em/react';
|
|
28
28
|
|
|
29
29
|
function App() {
|
|
30
30
|
return (
|
|
31
|
-
<
|
|
31
|
+
<Me2eMProvider>
|
|
32
32
|
<CreateIdentityScreen />
|
|
33
|
-
</
|
|
33
|
+
</Me2eMProvider>
|
|
34
34
|
);
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -90,7 +90,7 @@ this package:
|
|
|
90
90
|
| **Context cache** | Per-identity encrypted storage of derived Handle context. |
|
|
91
91
|
|
|
92
92
|
Full glossary (including Attestation, SubHandle, Ephemeral material):
|
|
93
|
-
see the
|
|
93
|
+
see the root [GLOSSARY.md](../../GLOSSARY.md).
|
|
94
94
|
|
|
95
95
|
**This package never persists your seed.** The seed phrase exists in
|
|
96
96
|
two places only — your paper and transient memory during login. What
|
|
@@ -188,6 +188,15 @@ import {
|
|
|
188
188
|
function ContextPanel() {
|
|
189
189
|
const storage = useMemo(() => new IdentityContextIDBStorage(), []);
|
|
190
190
|
|
|
191
|
+
// Caller-owned lifecycle (R5): close the IDB connection on unmount.
|
|
192
|
+
// close() also drops the in-memory cache key — reopen + setCacheKey
|
|
193
|
+
// before the next use.
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
return () => {
|
|
196
|
+
void storage.close();
|
|
197
|
+
};
|
|
198
|
+
}, [storage]);
|
|
199
|
+
|
|
191
200
|
// At login time, derive and set the cache key:
|
|
192
201
|
useEffect(() => {
|
|
193
202
|
const salt = crypto.getRandomValues(new Uint8Array(32));
|
|
@@ -215,10 +224,10 @@ function ContextPanel() {
|
|
|
215
224
|
}
|
|
216
225
|
```
|
|
217
226
|
|
|
218
|
-
Each identity gets a separate IndexedDB database (name = SHA-256 hash of identity ID), guaranteeing full isolation between identities.
|
|
227
|
+
Each identity gets a separate IndexedDB database (name = SHA-256 hash of identity ID), guaranteeing full isolation between identities. The storage connection is caller-owned: `close()` on unmount (as above) also drops the in-memory cache key — `open()`/`openAndLoad()` plus `setCacheKey()` are required before the next use.
|
|
219
228
|
|
|
220
229
|
## Status
|
|
221
230
|
|
|
222
231
|
Alpha. API may change between 0.1.x releases. See [packages/core/README.md](../../packages/core/README.md) for the underlying protocol.
|
|
223
232
|
|
|
224
|
-
Roadmap:
|
|
233
|
+
Roadmap: `@me2em/crypto` shipped (0.1.0-alpha.1); `@me2em/messenger` planned (BACKLOG BL-29).
|
package/dist/index.d.ts
CHANGED
|
@@ -7,9 +7,25 @@ declare function Me2emProvider({ children }: {
|
|
|
7
7
|
}): react.JSX.Element;
|
|
8
8
|
|
|
9
9
|
interface Me2emContextValue {
|
|
10
|
+
/**
|
|
11
|
+
* Active identity, or null. The previously active identity is
|
|
12
|
+
* disposed (key material zeroed) the moment it is replaced by a new
|
|
13
|
+
* activation or by {@link clearIdentity} — stale references fail
|
|
14
|
+
* loudly with `DisposedError` per the core contract (BL-47).
|
|
15
|
+
*/
|
|
10
16
|
identity: Identity | null;
|
|
11
17
|
hasIdentity: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Activates an identity from raw seed bytes, replacing (and
|
|
20
|
+
* disposing) any previously active identity. Resolves `false` if
|
|
21
|
+
* derivation failed or the activation was superseded by a newer
|
|
22
|
+
* activation / `clearIdentity()` while in flight.
|
|
23
|
+
*
|
|
24
|
+
* Does not consume the seed — the caller owns the buffer and must
|
|
25
|
+
* zero it when done (BL-48).
|
|
26
|
+
*/
|
|
12
27
|
activateIdentity: (seed: Uint8Array) => Promise<boolean>;
|
|
28
|
+
/** Disposes the active identity (if any) and clears it. */
|
|
13
29
|
clearIdentity: () => void;
|
|
14
30
|
}
|
|
15
31
|
declare function useMe2emContext(): Me2emContextValue;
|
|
@@ -18,6 +34,13 @@ type IdentityFlowStatus = 'idle' | 'seed-generated' | 'verified' | 'ready' | 'im
|
|
|
18
34
|
interface IdentityFlowState {
|
|
19
35
|
status: IdentityFlowStatus;
|
|
20
36
|
seedWords: string[] | null;
|
|
37
|
+
/**
|
|
38
|
+
* Raw seed material, retained in state after 'verified'/'imported'
|
|
39
|
+
* by design — the flow does NOT self-consume it (BL-48): headless
|
|
40
|
+
* consumers may still read it at this point. Reachable via devtools
|
|
41
|
+
* in this window (R15 category). An explicit `discardSeed()` action
|
|
42
|
+
* is the planned v1.1 opt-in; until then the app owns dropping it.
|
|
43
|
+
*/
|
|
21
44
|
identitySeed: Uint8Array | null;
|
|
22
45
|
error: string | null;
|
|
23
46
|
}
|
|
@@ -60,11 +83,28 @@ interface UseImportSeedResult {
|
|
|
60
83
|
}
|
|
61
84
|
declare function useImportSeed(): UseImportSeedResult;
|
|
62
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Derives the named Handle for the active identity.
|
|
88
|
+
*
|
|
89
|
+
* Silent-failure contract (R7): on derivation failure the hook logs
|
|
90
|
+
* via console.error and keeps returning null — there is no thrown or
|
|
91
|
+
* returned error signal. The structured `{ handle, error }` shape is
|
|
92
|
+
* a post-freeze v1.1 candidate (BL-50).
|
|
93
|
+
*/
|
|
63
94
|
declare function useHandle(name: string): Handle | null;
|
|
64
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Derives the named SubHandle under the given Handle.
|
|
98
|
+
*
|
|
99
|
+
* Silent-failure contract (R7): on derivation failure the hook logs
|
|
100
|
+
* via console.error and keeps returning null — there is no thrown or
|
|
101
|
+
* returned error signal. The structured `{ handle, error }` shape is
|
|
102
|
+
* a post-freeze v1.1 candidate (BL-50).
|
|
103
|
+
*/
|
|
65
104
|
declare function useSubHandle(handleName: string, subName: string): SubHandle | null;
|
|
66
105
|
|
|
67
106
|
interface SeedPhraseDisplayProps {
|
|
107
|
+
/** Seed material is reachable via React DevTools in this window; by design. */
|
|
68
108
|
words: string[];
|
|
69
109
|
onDismiss?: () => void;
|
|
70
110
|
onCopyAllowed?: () => void;
|
|
@@ -78,6 +118,15 @@ interface SeedPhraseVerifyProps {
|
|
|
78
118
|
onVerified: () => void;
|
|
79
119
|
onFailed?: (attempts: number) => void;
|
|
80
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Confirmation grid for a generated seed phrase.
|
|
123
|
+
*
|
|
124
|
+
* The grid shuffle is deterministic by design (fixed base seed 42;
|
|
125
|
+
* each retry shifts it by `+retryCount + 1`) for testability and SSR
|
|
126
|
+
* stability — it is NOT a security control: decoy words come from the
|
|
127
|
+
* public wordlist, so the layout leaks nothing (BL-49). A
|
|
128
|
+
* `randomSeed` prop is deliberately not offered in the frozen 1.0 API.
|
|
129
|
+
*/
|
|
81
130
|
declare function SeedPhraseVerify({ realWords, gridSize, wordlist: propWordlist, onVerified, onFailed, }: SeedPhraseVerifyProps): react.JSX.Element;
|
|
82
131
|
|
|
83
132
|
interface SeedPhraseImportProps {
|
|
@@ -109,6 +158,15 @@ interface IdentityContextStorage {
|
|
|
109
158
|
save(data: IdentityContextData): Promise<void>;
|
|
110
159
|
load(): Promise<IdentityContextData | null>;
|
|
111
160
|
clear(): Promise<void>;
|
|
161
|
+
/**
|
|
162
|
+
* Closes the IDB connection. Caller-owned lifecycle (R5): close()
|
|
163
|
+
* also drops the in-memory cache key and the open identity binding,
|
|
164
|
+
* so every further operation throws "Storage not opened" until the
|
|
165
|
+
* storage is reopened via open()/openAndLoad() and the cache key is
|
|
166
|
+
* set again. open() with a different identityId closes the previous
|
|
167
|
+
* connection automatically; same-identity open() after close()
|
|
168
|
+
* simply opens a fresh connection.
|
|
169
|
+
*/
|
|
112
170
|
close(): Promise<void>;
|
|
113
171
|
/** Open storage for identity and atomically load cached context. */
|
|
114
172
|
openAndLoad(identityId: string): Promise<IdentityContextData | null>;
|
|
@@ -320,7 +378,9 @@ interface ContextEnvelope {
|
|
|
320
378
|
* Raw key bytes exist only transiently during importKey.
|
|
321
379
|
*
|
|
322
380
|
* @param loginMaterial - high-entropy bytes available at login
|
|
323
|
-
* (derived from the seed while it is still in memory).
|
|
381
|
+
* (derived from the seed while it is still in memory). Caller-owned:
|
|
382
|
+
* deriveCacheKey reads but never wipes them — zero the buffer (and
|
|
383
|
+
* the seed it came from) when done (R6).
|
|
324
384
|
* @param salt - FRESH random bytes for this login. Never reuse a
|
|
325
385
|
* salt across logins: the whole point is that a compromised
|
|
326
386
|
* cacheKey from session N is useless in session N+1.
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,9 @@ var __export = (target, all) => {
|
|
|
8
8
|
import {
|
|
9
9
|
useState,
|
|
10
10
|
useCallback,
|
|
11
|
-
useMemo
|
|
11
|
+
useMemo,
|
|
12
|
+
useEffect,
|
|
13
|
+
useRef
|
|
12
14
|
} from "react";
|
|
13
15
|
import { Identity } from "@me2em/core";
|
|
14
16
|
|
|
@@ -27,9 +29,18 @@ function useMe2emContext() {
|
|
|
27
29
|
import { jsx } from "react/jsx-runtime";
|
|
28
30
|
function Me2emProvider({ children }) {
|
|
29
31
|
const [identity, setIdentity] = useState(null);
|
|
32
|
+
const identityRef = useRef(null);
|
|
33
|
+
const genRef = useRef(0);
|
|
30
34
|
const activateIdentity = useCallback(async (seed) => {
|
|
35
|
+
const gen = ++genRef.current;
|
|
31
36
|
try {
|
|
32
37
|
const id = await Identity.fromSeed(seed);
|
|
38
|
+
if (gen !== genRef.current) {
|
|
39
|
+
id.dispose();
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
identityRef.current?.dispose();
|
|
43
|
+
identityRef.current = id;
|
|
33
44
|
setIdentity(id);
|
|
34
45
|
return true;
|
|
35
46
|
} catch {
|
|
@@ -37,8 +48,18 @@ function Me2emProvider({ children }) {
|
|
|
37
48
|
}
|
|
38
49
|
}, []);
|
|
39
50
|
const clearIdentity = useCallback(() => {
|
|
51
|
+
++genRef.current;
|
|
52
|
+
identityRef.current?.dispose();
|
|
53
|
+
identityRef.current = null;
|
|
40
54
|
setIdentity(null);
|
|
41
55
|
}, []);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
return () => {
|
|
58
|
+
++genRef.current;
|
|
59
|
+
identityRef.current?.dispose();
|
|
60
|
+
identityRef.current = null;
|
|
61
|
+
};
|
|
62
|
+
}, []);
|
|
42
63
|
const value = useMemo(
|
|
43
64
|
() => ({ identity, hasIdentity: identity !== null, activateIdentity, clearIdentity }),
|
|
44
65
|
[identity, activateIdentity, clearIdentity]
|
|
@@ -53,7 +74,7 @@ import {
|
|
|
53
74
|
} from "react";
|
|
54
75
|
import {
|
|
55
76
|
generateSeedPhrase,
|
|
56
|
-
|
|
77
|
+
get64ByteSeedFromMnemonic
|
|
57
78
|
} from "@me2em/core";
|
|
58
79
|
|
|
59
80
|
// src/headless/identity-flow.ts
|
|
@@ -159,7 +180,7 @@ function useCreateIdentity() {
|
|
|
159
180
|
const generate = useCallback2(
|
|
160
181
|
(opts) => {
|
|
161
182
|
const words = generateSeedPhrase(opts?.strength ?? 128);
|
|
162
|
-
|
|
183
|
+
get64ByteSeedFromMnemonic(words).then((derivedSeed) => {
|
|
163
184
|
dispatch({ type: "GENERATE", words, seed: derivedSeed });
|
|
164
185
|
}).catch(() => {
|
|
165
186
|
dispatch({ type: "FAIL", message: "Failed to generate seed" });
|
|
@@ -198,7 +219,7 @@ function useCreateIdentity() {
|
|
|
198
219
|
import { useReducer as useReducer2, useCallback as useCallback3 } from "react";
|
|
199
220
|
import {
|
|
200
221
|
validateSeedPhrase,
|
|
201
|
-
|
|
222
|
+
get64ByteSeedFromMnemonic as get64ByteSeedFromMnemonic2
|
|
202
223
|
} from "@me2em/core";
|
|
203
224
|
function useImportSeed() {
|
|
204
225
|
const { activateIdentity, clearIdentity, identity } = useMe2emContext();
|
|
@@ -211,7 +232,7 @@ function useImportSeed() {
|
|
|
211
232
|
return;
|
|
212
233
|
}
|
|
213
234
|
try {
|
|
214
|
-
const seed = await
|
|
235
|
+
const seed = await get64ByteSeedFromMnemonic2(words, passphrase ?? "");
|
|
215
236
|
dispatch({ type: "IMPORT", seed });
|
|
216
237
|
await activateIdentity(seed);
|
|
217
238
|
} catch (err) {
|
|
@@ -235,11 +256,11 @@ function useImportSeed() {
|
|
|
235
256
|
}
|
|
236
257
|
|
|
237
258
|
// src/react/useHandle.ts
|
|
238
|
-
import { useState as useState2, useEffect } from "react";
|
|
259
|
+
import { useState as useState2, useEffect as useEffect2 } from "react";
|
|
239
260
|
function useHandle(name) {
|
|
240
261
|
const { identity } = useMe2emContext();
|
|
241
262
|
const [handle, setHandle] = useState2(null);
|
|
242
|
-
|
|
263
|
+
useEffect2(() => {
|
|
243
264
|
let cancelled = false;
|
|
244
265
|
if (!identity) {
|
|
245
266
|
setHandle(null);
|
|
@@ -258,11 +279,11 @@ function useHandle(name) {
|
|
|
258
279
|
}
|
|
259
280
|
|
|
260
281
|
// src/react/useSubHandle.ts
|
|
261
|
-
import { useState as useState3, useEffect as
|
|
282
|
+
import { useState as useState3, useEffect as useEffect3 } from "react";
|
|
262
283
|
function useSubHandle(handleName, subName) {
|
|
263
284
|
const { identity } = useMe2emContext();
|
|
264
285
|
const [subHandle, setSubHandle] = useState3(null);
|
|
265
|
-
|
|
286
|
+
useEffect3(() => {
|
|
266
287
|
let cancelled = false;
|
|
267
288
|
if (!identity) {
|
|
268
289
|
setSubHandle(null);
|
|
@@ -371,13 +392,16 @@ function displayFlowReducer(state, event) {
|
|
|
371
392
|
|
|
372
393
|
// src/react/SeedPhraseDisplay.tsx
|
|
373
394
|
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
395
|
+
function clipboardWriteAvailable() {
|
|
396
|
+
return typeof navigator !== "undefined" && !!navigator.clipboard && typeof navigator.clipboard.writeText === "function";
|
|
397
|
+
}
|
|
374
398
|
function SeedPhraseDisplay({
|
|
375
399
|
words,
|
|
376
400
|
onDismiss,
|
|
377
401
|
onCopyAllowed
|
|
378
402
|
}) {
|
|
379
403
|
const [state, dispatch] = useReducer3(displayFlowReducer, initialDisplayState);
|
|
380
|
-
const [
|
|
404
|
+
const [copyError, setCopyError] = useState4(null);
|
|
381
405
|
const { status, copyCount } = state;
|
|
382
406
|
const handleReveal = () => {
|
|
383
407
|
dispatch({ type: "REVEAL", at: Date.now() });
|
|
@@ -387,10 +411,10 @@ function SeedPhraseDisplay({
|
|
|
387
411
|
try {
|
|
388
412
|
await navigator.clipboard.writeText(words.join(" "));
|
|
389
413
|
dispatch({ type: "COPY", at: Date.now() });
|
|
390
|
-
|
|
414
|
+
setCopyError(null);
|
|
391
415
|
onCopyAllowed?.();
|
|
392
416
|
} catch {
|
|
393
|
-
|
|
417
|
+
setCopyError(clipboardWriteAvailable() ? "transient" : "permanent");
|
|
394
418
|
}
|
|
395
419
|
}
|
|
396
420
|
};
|
|
@@ -414,13 +438,13 @@ function SeedPhraseDisplay({
|
|
|
414
438
|
},
|
|
415
439
|
i
|
|
416
440
|
)) }),
|
|
417
|
-
|
|
441
|
+
copyError && /* @__PURE__ */ jsx2("div", { "data-testid": "copy-error", "data-kind": copyError, children: copyError === "permanent" ? "Copy unavailable in this context \u2014 select the words manually" : "Copy failed \u2014 try again" }),
|
|
418
442
|
/* @__PURE__ */ jsx2(
|
|
419
443
|
"button",
|
|
420
444
|
{
|
|
421
445
|
"data-testid": "copy-all",
|
|
422
446
|
onClick: handleCopy,
|
|
423
|
-
disabled: copyCount === 1,
|
|
447
|
+
disabled: copyCount === 1 || copyError === "permanent",
|
|
424
448
|
children: copyCount === 1 ? "Copied" : "Copy all"
|
|
425
449
|
}
|
|
426
450
|
),
|
|
@@ -429,7 +453,7 @@ function SeedPhraseDisplay({
|
|
|
429
453
|
}
|
|
430
454
|
|
|
431
455
|
// src/react/SeedPhraseVerify.tsx
|
|
432
|
-
import { useReducer as useReducer4, useCallback as useCallback4, useState as useState5, useEffect as
|
|
456
|
+
import { useReducer as useReducer4, useCallback as useCallback4, useState as useState5, useEffect as useEffect4, useRef as useRef2 } from "react";
|
|
433
457
|
|
|
434
458
|
// src/headless/seed-words.ts
|
|
435
459
|
function shuffleWithSeed(items, seed) {
|
|
@@ -579,7 +603,7 @@ function SeedPhraseVerify({
|
|
|
579
603
|
);
|
|
580
604
|
const [retryCount, setRetryCount] = useState5(0);
|
|
581
605
|
const { status, grid: gridData, selected } = state;
|
|
582
|
-
const notifiedRef =
|
|
606
|
+
const notifiedRef = useRef2(false);
|
|
583
607
|
const handleSelect = useCallback4(
|
|
584
608
|
(index) => {
|
|
585
609
|
if (status === "correct" || status === "incorrect") return;
|
|
@@ -588,7 +612,7 @@ function SeedPhraseVerify({
|
|
|
588
612
|
},
|
|
589
613
|
[status, selected.length, gridData.correctIndices.length]
|
|
590
614
|
);
|
|
591
|
-
|
|
615
|
+
useEffect4(() => {
|
|
592
616
|
if (notifiedRef.current) return;
|
|
593
617
|
if (selected.length === gridData.correctIndices.length && selected.length > 0 && status === "collecting") {
|
|
594
618
|
dispatch({ type: "CHECK" });
|
|
@@ -639,7 +663,7 @@ function SeedPhraseVerify({
|
|
|
639
663
|
}
|
|
640
664
|
|
|
641
665
|
// src/react/SeedPhraseImport.tsx
|
|
642
|
-
import { useReducer as useReducer5, useEffect as
|
|
666
|
+
import { useReducer as useReducer5, useEffect as useEffect5, useRef as useRef3 } from "react";
|
|
643
667
|
|
|
644
668
|
// src/headless/import-flow.ts
|
|
645
669
|
import { validateSeedPhrase as validateSeedPhrase2 } from "@me2em/core";
|
|
@@ -713,12 +737,12 @@ function SeedPhraseImport({
|
|
|
713
737
|
(params) => initImportState(params.expectedCount)
|
|
714
738
|
);
|
|
715
739
|
const count = state.expectedCount;
|
|
716
|
-
const prevStatusRef =
|
|
717
|
-
const lastImportedRef =
|
|
718
|
-
|
|
740
|
+
const prevStatusRef = useRef3(state.status);
|
|
741
|
+
const lastImportedRef = useRef3(null);
|
|
742
|
+
useEffect5(() => {
|
|
719
743
|
dispatch({ type: "CHECK" });
|
|
720
744
|
}, [state.words]);
|
|
721
|
-
|
|
745
|
+
useEffect5(() => {
|
|
722
746
|
const prevStatus = prevStatusRef.current;
|
|
723
747
|
prevStatusRef.current = state.status;
|
|
724
748
|
if (state.status === "valid" && prevStatus !== "valid") {
|
|
@@ -787,13 +811,13 @@ function SeedPhraseImport({
|
|
|
787
811
|
}
|
|
788
812
|
|
|
789
813
|
// src/react/PassphraseInput.tsx
|
|
790
|
-
import { useCallback as
|
|
814
|
+
import { useCallback as useCallback5 } from "react";
|
|
791
815
|
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
792
816
|
function PassphraseInput({
|
|
793
817
|
onPassphraseChange,
|
|
794
818
|
showWarnings = true
|
|
795
819
|
}) {
|
|
796
|
-
const handleChange =
|
|
820
|
+
const handleChange = useCallback5(
|
|
797
821
|
(e) => {
|
|
798
822
|
const value = e.target.value.normalize("NFKC");
|
|
799
823
|
onPassphraseChange(value);
|
|
@@ -815,17 +839,23 @@ function PassphraseInput({
|
|
|
815
839
|
}
|
|
816
840
|
|
|
817
841
|
// src/react/useIdentityContext.ts
|
|
818
|
-
import { useState as
|
|
842
|
+
import { useState as useState6, useCallback as useCallback6, useEffect as useEffect6, useRef as useRef4 } from "react";
|
|
819
843
|
function publicKeyToId(publicKey) {
|
|
820
844
|
return Array.from(publicKey).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
821
845
|
}
|
|
846
|
+
function identityToId(identity) {
|
|
847
|
+
return identity ? publicKeyToId(identity.getPublicKey()) : null;
|
|
848
|
+
}
|
|
822
849
|
function useIdentityContext(storage) {
|
|
823
850
|
const { identity } = useMe2emContext();
|
|
824
|
-
const [context, setContext] =
|
|
825
|
-
const [isLoaded, setIsLoaded] =
|
|
826
|
-
const [error, setError] =
|
|
827
|
-
const prevIdentityIdRef =
|
|
828
|
-
|
|
851
|
+
const [context, setContext] = useState6(null);
|
|
852
|
+
const [isLoaded, setIsLoaded] = useState6(false);
|
|
853
|
+
const [error, setError] = useState6(null);
|
|
854
|
+
const prevIdentityIdRef = useRef4(null);
|
|
855
|
+
const opRef = useRef4(0);
|
|
856
|
+
const identityRef = useRef4(null);
|
|
857
|
+
identityRef.current = identity;
|
|
858
|
+
useEffect6(() => {
|
|
829
859
|
let cancelled = false;
|
|
830
860
|
if (!identity) {
|
|
831
861
|
setContext(null);
|
|
@@ -839,8 +869,9 @@ function useIdentityContext(storage) {
|
|
|
839
869
|
});
|
|
840
870
|
}
|
|
841
871
|
prevIdentityIdRef.current = identityId;
|
|
872
|
+
const op = ++opRef.current;
|
|
842
873
|
storage.openAndLoad(identityId).then((data) => {
|
|
843
|
-
if (cancelled) return;
|
|
874
|
+
if (cancelled || op !== opRef.current) return;
|
|
844
875
|
setContext(data);
|
|
845
876
|
setIsLoaded(true);
|
|
846
877
|
setError(null);
|
|
@@ -853,49 +884,60 @@ function useIdentityContext(storage) {
|
|
|
853
884
|
cancelled = true;
|
|
854
885
|
};
|
|
855
886
|
}, [identity, storage]);
|
|
856
|
-
const save =
|
|
887
|
+
const save = useCallback6(async (data) => {
|
|
888
|
+
const idAtStart = identityToId(identityRef.current);
|
|
889
|
+
opRef.current += 1;
|
|
857
890
|
await storage.save(data);
|
|
891
|
+
if (identityToId(identityRef.current) !== idAtStart) return;
|
|
858
892
|
setContext(data);
|
|
893
|
+
setError(null);
|
|
859
894
|
}, [storage]);
|
|
860
|
-
const clear =
|
|
895
|
+
const clear = useCallback6(async () => {
|
|
896
|
+
const idAtStart = identityToId(identityRef.current);
|
|
897
|
+
opRef.current += 1;
|
|
861
898
|
await storage.clear();
|
|
899
|
+
if (identityToId(identityRef.current) !== idAtStart) return;
|
|
862
900
|
setContext(null);
|
|
901
|
+
setError(null);
|
|
863
902
|
}, [storage]);
|
|
864
|
-
const refresh =
|
|
903
|
+
const refresh = useCallback6(async () => {
|
|
865
904
|
if (!identity) return;
|
|
866
905
|
const identityId = publicKeyToId(identity.getPublicKey());
|
|
906
|
+
const op = ++opRef.current;
|
|
867
907
|
const data = await storage.openAndLoad(identityId);
|
|
908
|
+
if (op !== opRef.current) return;
|
|
868
909
|
setContext(data);
|
|
869
910
|
}, [identity, storage]);
|
|
870
911
|
return { context, isLoaded, error, save, clear, refresh };
|
|
871
912
|
}
|
|
872
913
|
|
|
873
914
|
// src/react/useSession.ts
|
|
874
|
-
import { useState as
|
|
915
|
+
import { useState as useState7, useEffect as useEffect7, useCallback as useCallback7, useRef as useRef5 } from "react";
|
|
875
916
|
import { Session } from "@me2em/core";
|
|
876
917
|
function useSession(handle, options, autoRenew = true) {
|
|
877
|
-
const
|
|
878
|
-
const [
|
|
879
|
-
const [
|
|
880
|
-
const
|
|
881
|
-
const
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
918
|
+
const [session, setSession] = useState7(null);
|
|
919
|
+
const [isLoaded, setIsLoaded] = useState7(false);
|
|
920
|
+
const [error, setError] = useState7(null);
|
|
921
|
+
const timerRef = useRef5(null);
|
|
922
|
+
const optionsRef = useRef5(options);
|
|
923
|
+
const mountedRef = useRef5(true);
|
|
924
|
+
useEffect7(() => {
|
|
925
|
+
optionsRef.current = options;
|
|
926
|
+
});
|
|
927
|
+
const clearTimer = useCallback7(() => {
|
|
886
928
|
if (timerRef.current) {
|
|
887
929
|
clearTimeout(timerRef.current);
|
|
888
930
|
timerRef.current = null;
|
|
889
931
|
}
|
|
890
932
|
}, []);
|
|
891
|
-
|
|
933
|
+
useEffect7(() => {
|
|
892
934
|
mountedRef.current = true;
|
|
893
935
|
return () => {
|
|
894
936
|
mountedRef.current = false;
|
|
895
937
|
clearTimer();
|
|
896
938
|
};
|
|
897
939
|
}, [clearTimer]);
|
|
898
|
-
const renew =
|
|
940
|
+
const renew = useCallback7(() => {
|
|
899
941
|
clearTimer();
|
|
900
942
|
const opts = optionsRef.current;
|
|
901
943
|
if (!handle) {
|
|
@@ -929,7 +971,7 @@ function useSession(handle, options, autoRenew = true) {
|
|
|
929
971
|
setError(err instanceof Error ? err.message : String(err));
|
|
930
972
|
});
|
|
931
973
|
}, [handle, autoRenew, clearTimer]);
|
|
932
|
-
|
|
974
|
+
useEffect7(() => {
|
|
933
975
|
if (mountedRef.current) {
|
|
934
976
|
renew();
|
|
935
977
|
}
|
|
@@ -964,25 +1006,7 @@ __export(headless_exports, {
|
|
|
964
1006
|
});
|
|
965
1007
|
|
|
966
1008
|
// src/headless/context-crypto.ts
|
|
967
|
-
|
|
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
|
-
}
|
|
1009
|
+
import { base64urlEncode, base64urlDecode } from "@me2em/core";
|
|
986
1010
|
async function deriveCacheKey(loginMaterial, salt) {
|
|
987
1011
|
const base = await crypto.subtle.importKey(
|
|
988
1012
|
"raw",
|
|
@@ -1020,17 +1044,17 @@ async function encryptContext(key, data) {
|
|
|
1020
1044
|
);
|
|
1021
1045
|
return {
|
|
1022
1046
|
v: 1,
|
|
1023
|
-
salt:
|
|
1024
|
-
iv:
|
|
1025
|
-
ct:
|
|
1047
|
+
salt: base64urlEncode(salt),
|
|
1048
|
+
iv: base64urlEncode(iv),
|
|
1049
|
+
ct: base64urlEncode(new Uint8Array(ct))
|
|
1026
1050
|
};
|
|
1027
1051
|
}
|
|
1028
1052
|
async function decryptContext(key, envelope) {
|
|
1029
1053
|
if (envelope.v !== 1) throw new Error("Unsupported envelope version");
|
|
1030
1054
|
const pt = await crypto.subtle.decrypt(
|
|
1031
|
-
{ name: "AES-GCM", iv:
|
|
1055
|
+
{ name: "AES-GCM", iv: base64urlDecode(envelope.iv) },
|
|
1032
1056
|
key,
|
|
1033
|
-
|
|
1057
|
+
base64urlDecode(envelope.ct)
|
|
1034
1058
|
);
|
|
1035
1059
|
return JSON.parse(new TextDecoder().decode(pt));
|
|
1036
1060
|
}
|
|
@@ -1189,7 +1213,7 @@ var IdentityContextIDBStorage = class {
|
|
|
1189
1213
|
if (!db) {
|
|
1190
1214
|
return null;
|
|
1191
1215
|
}
|
|
1192
|
-
return new Promise((resolve
|
|
1216
|
+
return new Promise((resolve) => {
|
|
1193
1217
|
const tx = db.transaction(KEYS_STORE_NAME, "readonly");
|
|
1194
1218
|
const store = tx.objectStore(KEYS_STORE_NAME);
|
|
1195
1219
|
const request = store.get("cache");
|