@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 CHANGED
@@ -1,12 +1,10 @@
1
- # @me2em/react
1
+ [![npm version](https://img.shields.io/npm/v/@me2em/react.svg)](https://www.npmjs.com/package/@me2em/react)
2
+ [![License](https://img.shields.io/npm/l/@me2em/react.svg)](./LICENSE)
3
+ [![Docs](https://img.shields.io/badge/docs-react.me2em.com-blue)](https://react.me2em.com)
2
4
 
3
- **Me2em never persists your seed.** The seed phrase exists in two
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 Me2em protocol: identity lifecycle, seed phrase UX, and hierarchical handle derivation.
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 { Me2emProvider, useCreateIdentity } from '@me2em/react';
27
+ import { Me2eMProvider, useCreateIdentity } from '@me2em/react';
23
28
 
24
29
  function App() {
25
30
  return (
26
- <Me2emProvider>
31
+ <Me2eMProvider>
27
32
  <CreateIdentityScreen />
28
- </Me2emProvider>
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: @me2em/crypto protocol mechanisms — planned
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
- get32ByteSeedFromMnemonic
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
- get32ByteSeedFromMnemonic(words).then((derivedSeed) => {
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
- get32ByteSeedFromMnemonic as get32ByteSeedFromMnemonic2
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 get32ByteSeedFromMnemonic2(words, passphrase ?? "");
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 [copyFailed, setCopyFailed] = useState4(false);
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
- setCopyFailed(false);
393
+ setCopyError(null);
391
394
  onCopyAllowed?.();
392
395
  } catch {
393
- setCopyFailed(true);
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
- copyFailed && /* @__PURE__ */ jsx2("div", { "data-testid": "copy-error", children: "Copy failed \u2014 try again" }),
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 useCallback6 } from "react";
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 = useCallback6(
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 useState7, useCallback as useCallback7, useEffect as useEffect5, useRef as useRef3 } from "react";
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] = useState7(null);
825
- const [isLoaded, setIsLoaded] = useState7(false);
826
- const [error, setError] = useState7(null);
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 = useCallback7(async (data) => {
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 = useCallback7(async () => {
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 = useCallback7(async () => {
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 useState8, useEffect as useEffect6, useCallback as useCallback8, useRef as useRef4 } from "react";
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 { identity } = useMe2emContext();
878
- const [session, setSession] = useState8(null);
879
- const [isLoaded, setIsLoaded] = useState8(false);
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
- optionsRef.current = options;
885
- const clearTimer = useCallback8(() => {
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 = useCallback8(() => {
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
- 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
- }
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: toB64(salt),
1024
- iv: toB64(iv),
1025
- ct: toB64(new Uint8Array(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: fromB64(envelope.iv) },
1034
+ { name: "AES-GCM", iv: base64urlDecode(envelope.iv) },
1032
1035
  key,
1033
- fromB64(envelope.ct)
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, reject) => {
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");