@me2em/react 0.1.0-alpha.1 → 1.0.0-rc.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/README.md +40 -11
- package/dist/index.d.ts +2 -1
- package/dist/index.js +55 -52
- package/dist/index.js.map +1 -1
- package/package.json +12 -6
- package/src/headless/context-crypto.ts +10 -29
- package/src/headless/identity-context-storage.ts +2 -2
- package/src/headless/import-flow.ts +1 -1
- 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 +0 -1
- package/src/react/index.ts +3 -0
- package/src/react/useCreateIdentity.ts +2 -2
- package/src/react/useHandle.ts +1 -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 +1 -0
- package/src/headless/context-key-store.ts +0 -124
package/README.md
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
[](https://www.npmjs.com/package/@me2em/react)
|
|
2
|
+
[](./LICENSE)
|
|
3
|
+
[](https://react.me2em.com)
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
places only: your paper and transient memory during login. This
|
|
5
|
-
package stores only *derived, encrypted context caches* — losing your
|
|
6
|
-
browser data never loses your identity, because your identity lives
|
|
7
|
-
in your head.
|
|
5
|
+
## 📦 @me2em/react — React bindings
|
|
8
6
|
|
|
9
|
-
React bindings for the
|
|
7
|
+
React bindings for the Me2eM protocol: identity lifecycle, seed phrase UX, and hierarchical handle derivation.
|
|
10
8
|
|
|
11
9
|
## Install
|
|
12
10
|
|
|
@@ -16,16 +14,23 @@ pnpm add @me2em/react react
|
|
|
16
14
|
|
|
17
15
|
Requires React >= 18.0.0 as a peer dependency.
|
|
18
16
|
|
|
17
|
+
> 💡 This package requires [`@me2em/core`](../core/README.md) (peer
|
|
18
|
+
> dependency via workspace) and React ≥ 18. It is self-contained —
|
|
19
|
+
> for E2EE channels, X3DH, and Argon2id see
|
|
20
|
+
> [`@me2em/crypto`](../crypto/README.md). All three packages are
|
|
21
|
+
> designed to work together — see the root
|
|
22
|
+
> [README](../../README.md) for the full package map.
|
|
23
|
+
|
|
19
24
|
## Quick Start
|
|
20
25
|
|
|
21
26
|
```tsx
|
|
22
|
-
import {
|
|
27
|
+
import { Me2eMProvider, useCreateIdentity } from '@me2em/react';
|
|
23
28
|
|
|
24
29
|
function App() {
|
|
25
30
|
return (
|
|
26
|
-
<
|
|
31
|
+
<Me2eMProvider>
|
|
27
32
|
<CreateIdentityScreen />
|
|
28
|
-
</
|
|
33
|
+
</Me2eMProvider>
|
|
29
34
|
);
|
|
30
35
|
}
|
|
31
36
|
|
|
@@ -69,6 +74,30 @@ const state = headless.identityFlowReducer(headless.initialIdentityFlowState, {
|
|
|
69
74
|
});
|
|
70
75
|
```
|
|
71
76
|
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 📖 Terminology (selected)
|
|
80
|
+
|
|
81
|
+
Protocol terms are **domain-neutral**. The terms most relevant to
|
|
82
|
+
this package:
|
|
83
|
+
|
|
84
|
+
| Term | Definition |
|
|
85
|
+
|------|------------|
|
|
86
|
+
| **Identity** | Root cryptographic identity derived from a seed phrase. Constant forever. |
|
|
87
|
+
| **Handle** | A derived, attested, named key representing a distinct identity context. |
|
|
88
|
+
| **Session** | A stateless signed authorization token. |
|
|
89
|
+
| **Transient material** | Key bytes existing only in RAM at the moment of use. |
|
|
90
|
+
| **Context cache** | Per-identity encrypted storage of derived Handle context. |
|
|
91
|
+
|
|
92
|
+
Full glossary (including Attestation, SubHandle, Ephemeral material):
|
|
93
|
+
see the root [GLOSSARY.md](../../GLOSSARY.md).
|
|
94
|
+
|
|
95
|
+
**This package never persists your seed.** The seed phrase exists in
|
|
96
|
+
two places only — your paper and transient memory during login. What
|
|
97
|
+
this package stores is derived, encrypted context caches. Losing your
|
|
98
|
+
browser data never loses your identity, because your identity lives
|
|
99
|
+
in your head.
|
|
100
|
+
|
|
72
101
|
## Components
|
|
73
102
|
|
|
74
103
|
### SeedPhraseDisplay
|
|
@@ -192,4 +221,4 @@ Each identity gets a separate IndexedDB database (name = SHA-256 hash of identit
|
|
|
192
221
|
|
|
193
222
|
Alpha. API may change between 0.1.x releases. See [packages/core/README.md](../../packages/core/README.md) for the underlying protocol.
|
|
194
223
|
|
|
195
|
-
Roadmap:
|
|
224
|
+
Roadmap: `@me2em/crypto` shipped (0.1.0-alpha.1); `@me2em/messenger` planned (BACKLOG BL-29).
|
package/dist/index.d.ts
CHANGED
|
@@ -65,6 +65,7 @@ declare function useHandle(name: string): Handle | null;
|
|
|
65
65
|
declare function useSubHandle(handleName: string, subName: string): SubHandle | null;
|
|
66
66
|
|
|
67
67
|
interface SeedPhraseDisplayProps {
|
|
68
|
+
/** Seed material is reachable via React DevTools in this window; by design. */
|
|
68
69
|
words: string[];
|
|
69
70
|
onDismiss?: () => void;
|
|
70
71
|
onCopyAllowed?: () => void;
|
|
@@ -364,4 +365,4 @@ declare namespace index {
|
|
|
364
365
|
export { type index_ContextEnvelope as ContextEnvelope, type index_DisplayEvent as DisplayEvent, type index_DisplayState as DisplayState, type index_DisplayStatus as DisplayStatus, type index_IdentityContextData as IdentityContextData, index_IdentityContextIDBStorage as IdentityContextIDBStorage, type index_IdentityContextStorage as IdentityContextStorage, type index_IdentityFlowEvent as IdentityFlowEvent, type index_IdentityFlowState as IdentityFlowState, type index_IdentityFlowStatus as IdentityFlowStatus, type index_ImportEvent as ImportEvent, type index_ImportState as ImportState, type index_ImportStatus as ImportStatus, type index_VerifyEvent as VerifyEvent, type index_VerifyState as VerifyState, type index_VerifyStatus as VerifyStatus, index_buildVerificationGrid as buildVerificationGrid, index_decryptContext as decryptContext, index_deriveCacheKey as deriveCacheKey, index_displayFlowReducer as displayFlowReducer, index_encryptContext as encryptContext, index_identityFlowReducer as identityFlowReducer, index_importFlowReducer as importFlowReducer, index_initImportState as initImportState, index_initVerifyState as initVerifyState, index_initialDisplayState as initialDisplayState, index_initialIdentityFlowState as initialIdentityFlowState, index_isGridSelectionCorrect as isGridSelectionCorrect, index_shuffleWithSeed as shuffleWithSeed, index_verifyFlowReducer as verifyFlowReducer };
|
|
365
366
|
}
|
|
366
367
|
|
|
367
|
-
export { Me2emProvider, PassphraseInput, type PassphraseInputProps, SeedPhraseDisplay, type SeedPhraseDisplayProps, SeedPhraseImport, type SeedPhraseImportProps, SeedPhraseVerify, type SeedPhraseVerifyProps, type UseIdentityContextResult, type UseSessionOptions, type UseSessionResult, index as headless, useCreateIdentity, useHandle, useIdentityContext, useImportSeed, useMe2emContext, useSession, useSubHandle };
|
|
368
|
+
export { type Me2emContextValue, Me2emProvider, PassphraseInput, type PassphraseInputProps, SeedPhraseDisplay, type SeedPhraseDisplayProps, SeedPhraseImport, type SeedPhraseImportProps, SeedPhraseVerify, type SeedPhraseVerifyProps, type UseCreateIdentityResult, type UseIdentityContextResult, type UseImportSeedResult, type UseSessionOptions, type UseSessionResult, index as headless, useCreateIdentity, useHandle, useIdentityContext, useImportSeed, useMe2emContext, useSession, useSubHandle };
|
package/dist/index.js
CHANGED
|
@@ -53,7 +53,7 @@ import {
|
|
|
53
53
|
} from "react";
|
|
54
54
|
import {
|
|
55
55
|
generateSeedPhrase,
|
|
56
|
-
|
|
56
|
+
get64ByteSeedFromMnemonic
|
|
57
57
|
} from "@me2em/core";
|
|
58
58
|
|
|
59
59
|
// src/headless/identity-flow.ts
|
|
@@ -159,7 +159,7 @@ function useCreateIdentity() {
|
|
|
159
159
|
const generate = useCallback2(
|
|
160
160
|
(opts) => {
|
|
161
161
|
const words = generateSeedPhrase(opts?.strength ?? 128);
|
|
162
|
-
|
|
162
|
+
get64ByteSeedFromMnemonic(words).then((derivedSeed) => {
|
|
163
163
|
dispatch({ type: "GENERATE", words, seed: derivedSeed });
|
|
164
164
|
}).catch(() => {
|
|
165
165
|
dispatch({ type: "FAIL", message: "Failed to generate seed" });
|
|
@@ -198,7 +198,7 @@ function useCreateIdentity() {
|
|
|
198
198
|
import { useReducer as useReducer2, useCallback as useCallback3 } from "react";
|
|
199
199
|
import {
|
|
200
200
|
validateSeedPhrase,
|
|
201
|
-
|
|
201
|
+
get64ByteSeedFromMnemonic as get64ByteSeedFromMnemonic2
|
|
202
202
|
} from "@me2em/core";
|
|
203
203
|
function useImportSeed() {
|
|
204
204
|
const { activateIdentity, clearIdentity, identity } = useMe2emContext();
|
|
@@ -211,7 +211,7 @@ function useImportSeed() {
|
|
|
211
211
|
return;
|
|
212
212
|
}
|
|
213
213
|
try {
|
|
214
|
-
const seed = await
|
|
214
|
+
const seed = await get64ByteSeedFromMnemonic2(words, passphrase ?? "");
|
|
215
215
|
dispatch({ type: "IMPORT", seed });
|
|
216
216
|
await activateIdentity(seed);
|
|
217
217
|
} catch (err) {
|
|
@@ -371,13 +371,16 @@ function displayFlowReducer(state, event) {
|
|
|
371
371
|
|
|
372
372
|
// src/react/SeedPhraseDisplay.tsx
|
|
373
373
|
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
374
|
+
function clipboardWriteAvailable() {
|
|
375
|
+
return typeof navigator !== "undefined" && !!navigator.clipboard && typeof navigator.clipboard.writeText === "function";
|
|
376
|
+
}
|
|
374
377
|
function SeedPhraseDisplay({
|
|
375
378
|
words,
|
|
376
379
|
onDismiss,
|
|
377
380
|
onCopyAllowed
|
|
378
381
|
}) {
|
|
379
382
|
const [state, dispatch] = useReducer3(displayFlowReducer, initialDisplayState);
|
|
380
|
-
const [
|
|
383
|
+
const [copyError, setCopyError] = useState4(null);
|
|
381
384
|
const { status, copyCount } = state;
|
|
382
385
|
const handleReveal = () => {
|
|
383
386
|
dispatch({ type: "REVEAL", at: Date.now() });
|
|
@@ -387,10 +390,10 @@ function SeedPhraseDisplay({
|
|
|
387
390
|
try {
|
|
388
391
|
await navigator.clipboard.writeText(words.join(" "));
|
|
389
392
|
dispatch({ type: "COPY", at: Date.now() });
|
|
390
|
-
|
|
393
|
+
setCopyError(null);
|
|
391
394
|
onCopyAllowed?.();
|
|
392
395
|
} catch {
|
|
393
|
-
|
|
396
|
+
setCopyError(clipboardWriteAvailable() ? "transient" : "permanent");
|
|
394
397
|
}
|
|
395
398
|
}
|
|
396
399
|
};
|
|
@@ -414,13 +417,13 @@ function SeedPhraseDisplay({
|
|
|
414
417
|
},
|
|
415
418
|
i
|
|
416
419
|
)) }),
|
|
417
|
-
|
|
420
|
+
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
421
|
/* @__PURE__ */ jsx2(
|
|
419
422
|
"button",
|
|
420
423
|
{
|
|
421
424
|
"data-testid": "copy-all",
|
|
422
425
|
onClick: handleCopy,
|
|
423
|
-
disabled: copyCount === 1,
|
|
426
|
+
disabled: copyCount === 1 || copyError === "permanent",
|
|
424
427
|
children: copyCount === 1 ? "Copied" : "Copy all"
|
|
425
428
|
}
|
|
426
429
|
),
|
|
@@ -787,13 +790,13 @@ function SeedPhraseImport({
|
|
|
787
790
|
}
|
|
788
791
|
|
|
789
792
|
// src/react/PassphraseInput.tsx
|
|
790
|
-
import { useCallback as
|
|
793
|
+
import { useCallback as useCallback5 } from "react";
|
|
791
794
|
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
792
795
|
function PassphraseInput({
|
|
793
796
|
onPassphraseChange,
|
|
794
797
|
showWarnings = true
|
|
795
798
|
}) {
|
|
796
|
-
const handleChange =
|
|
799
|
+
const handleChange = useCallback5(
|
|
797
800
|
(e) => {
|
|
798
801
|
const value = e.target.value.normalize("NFKC");
|
|
799
802
|
onPassphraseChange(value);
|
|
@@ -815,16 +818,22 @@ function PassphraseInput({
|
|
|
815
818
|
}
|
|
816
819
|
|
|
817
820
|
// src/react/useIdentityContext.ts
|
|
818
|
-
import { useState as
|
|
821
|
+
import { useState as useState6, useCallback as useCallback6, useEffect as useEffect5, useRef as useRef3 } from "react";
|
|
819
822
|
function publicKeyToId(publicKey) {
|
|
820
823
|
return Array.from(publicKey).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
821
824
|
}
|
|
825
|
+
function identityToId(identity) {
|
|
826
|
+
return identity ? publicKeyToId(identity.getPublicKey()) : null;
|
|
827
|
+
}
|
|
822
828
|
function useIdentityContext(storage) {
|
|
823
829
|
const { identity } = useMe2emContext();
|
|
824
|
-
const [context, setContext] =
|
|
825
|
-
const [isLoaded, setIsLoaded] =
|
|
826
|
-
const [error, setError] =
|
|
830
|
+
const [context, setContext] = useState6(null);
|
|
831
|
+
const [isLoaded, setIsLoaded] = useState6(false);
|
|
832
|
+
const [error, setError] = useState6(null);
|
|
827
833
|
const prevIdentityIdRef = useRef3(null);
|
|
834
|
+
const opRef = useRef3(0);
|
|
835
|
+
const identityRef = useRef3(null);
|
|
836
|
+
identityRef.current = identity;
|
|
828
837
|
useEffect5(() => {
|
|
829
838
|
let cancelled = false;
|
|
830
839
|
if (!identity) {
|
|
@@ -839,8 +848,9 @@ function useIdentityContext(storage) {
|
|
|
839
848
|
});
|
|
840
849
|
}
|
|
841
850
|
prevIdentityIdRef.current = identityId;
|
|
851
|
+
const op = ++opRef.current;
|
|
842
852
|
storage.openAndLoad(identityId).then((data) => {
|
|
843
|
-
if (cancelled) return;
|
|
853
|
+
if (cancelled || op !== opRef.current) return;
|
|
844
854
|
setContext(data);
|
|
845
855
|
setIsLoaded(true);
|
|
846
856
|
setError(null);
|
|
@@ -853,36 +863,47 @@ function useIdentityContext(storage) {
|
|
|
853
863
|
cancelled = true;
|
|
854
864
|
};
|
|
855
865
|
}, [identity, storage]);
|
|
856
|
-
const save =
|
|
866
|
+
const save = useCallback6(async (data) => {
|
|
867
|
+
const idAtStart = identityToId(identityRef.current);
|
|
868
|
+
opRef.current += 1;
|
|
857
869
|
await storage.save(data);
|
|
870
|
+
if (identityToId(identityRef.current) !== idAtStart) return;
|
|
858
871
|
setContext(data);
|
|
872
|
+
setError(null);
|
|
859
873
|
}, [storage]);
|
|
860
|
-
const clear =
|
|
874
|
+
const clear = useCallback6(async () => {
|
|
875
|
+
const idAtStart = identityToId(identityRef.current);
|
|
876
|
+
opRef.current += 1;
|
|
861
877
|
await storage.clear();
|
|
878
|
+
if (identityToId(identityRef.current) !== idAtStart) return;
|
|
862
879
|
setContext(null);
|
|
880
|
+
setError(null);
|
|
863
881
|
}, [storage]);
|
|
864
|
-
const refresh =
|
|
882
|
+
const refresh = useCallback6(async () => {
|
|
865
883
|
if (!identity) return;
|
|
866
884
|
const identityId = publicKeyToId(identity.getPublicKey());
|
|
885
|
+
const op = ++opRef.current;
|
|
867
886
|
const data = await storage.openAndLoad(identityId);
|
|
887
|
+
if (op !== opRef.current) return;
|
|
868
888
|
setContext(data);
|
|
869
889
|
}, [identity, storage]);
|
|
870
890
|
return { context, isLoaded, error, save, clear, refresh };
|
|
871
891
|
}
|
|
872
892
|
|
|
873
893
|
// src/react/useSession.ts
|
|
874
|
-
import { useState as
|
|
894
|
+
import { useState as useState7, useEffect as useEffect6, useCallback as useCallback7, useRef as useRef4 } from "react";
|
|
875
895
|
import { Session } from "@me2em/core";
|
|
876
896
|
function useSession(handle, options, autoRenew = true) {
|
|
877
|
-
const
|
|
878
|
-
const [
|
|
879
|
-
const [
|
|
880
|
-
const [error, setError] = useState8(null);
|
|
897
|
+
const [session, setSession] = useState7(null);
|
|
898
|
+
const [isLoaded, setIsLoaded] = useState7(false);
|
|
899
|
+
const [error, setError] = useState7(null);
|
|
881
900
|
const timerRef = useRef4(null);
|
|
882
901
|
const optionsRef = useRef4(options);
|
|
883
902
|
const mountedRef = useRef4(true);
|
|
884
|
-
|
|
885
|
-
|
|
903
|
+
useEffect6(() => {
|
|
904
|
+
optionsRef.current = options;
|
|
905
|
+
});
|
|
906
|
+
const clearTimer = useCallback7(() => {
|
|
886
907
|
if (timerRef.current) {
|
|
887
908
|
clearTimeout(timerRef.current);
|
|
888
909
|
timerRef.current = null;
|
|
@@ -895,7 +916,7 @@ function useSession(handle, options, autoRenew = true) {
|
|
|
895
916
|
clearTimer();
|
|
896
917
|
};
|
|
897
918
|
}, [clearTimer]);
|
|
898
|
-
const renew =
|
|
919
|
+
const renew = useCallback7(() => {
|
|
899
920
|
clearTimer();
|
|
900
921
|
const opts = optionsRef.current;
|
|
901
922
|
if (!handle) {
|
|
@@ -964,25 +985,7 @@ __export(headless_exports, {
|
|
|
964
985
|
});
|
|
965
986
|
|
|
966
987
|
// 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
|
-
}
|
|
988
|
+
import { base64urlEncode, base64urlDecode } from "@me2em/core";
|
|
986
989
|
async function deriveCacheKey(loginMaterial, salt) {
|
|
987
990
|
const base = await crypto.subtle.importKey(
|
|
988
991
|
"raw",
|
|
@@ -1020,17 +1023,17 @@ async function encryptContext(key, data) {
|
|
|
1020
1023
|
);
|
|
1021
1024
|
return {
|
|
1022
1025
|
v: 1,
|
|
1023
|
-
salt:
|
|
1024
|
-
iv:
|
|
1025
|
-
ct:
|
|
1026
|
+
salt: base64urlEncode(salt),
|
|
1027
|
+
iv: base64urlEncode(iv),
|
|
1028
|
+
ct: base64urlEncode(new Uint8Array(ct))
|
|
1026
1029
|
};
|
|
1027
1030
|
}
|
|
1028
1031
|
async function decryptContext(key, envelope) {
|
|
1029
1032
|
if (envelope.v !== 1) throw new Error("Unsupported envelope version");
|
|
1030
1033
|
const pt = await crypto.subtle.decrypt(
|
|
1031
|
-
{ name: "AES-GCM", iv:
|
|
1034
|
+
{ name: "AES-GCM", iv: base64urlDecode(envelope.iv) },
|
|
1032
1035
|
key,
|
|
1033
|
-
|
|
1036
|
+
base64urlDecode(envelope.ct)
|
|
1034
1037
|
);
|
|
1035
1038
|
return JSON.parse(new TextDecoder().decode(pt));
|
|
1036
1039
|
}
|
|
@@ -1189,7 +1192,7 @@ var IdentityContextIDBStorage = class {
|
|
|
1189
1192
|
if (!db) {
|
|
1190
1193
|
return null;
|
|
1191
1194
|
}
|
|
1192
|
-
return new Promise((resolve
|
|
1195
|
+
return new Promise((resolve) => {
|
|
1193
1196
|
const tx = db.transaction(KEYS_STORE_NAME, "readonly");
|
|
1194
1197
|
const store = tx.objectStore(KEYS_STORE_NAME);
|
|
1195
1198
|
const request = store.get("cache");
|