@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 CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## 📦 @me2em/react — React bindings
6
6
 
7
- 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.
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 { Me2emProvider, useCreateIdentity } from '@me2em/react';
27
+ import { Me2eMProvider, useCreateIdentity } from '@me2em/react';
28
28
 
29
29
  function App() {
30
30
  return (
31
- <Me2emProvider>
31
+ <Me2eMProvider>
32
32
  <CreateIdentityScreen />
33
- </Me2emProvider>
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 [root README](../../README.md).
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: @me2em/crypto protocol mechanisms — planned
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
- get32ByteSeedFromMnemonic
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
- get32ByteSeedFromMnemonic(words).then((derivedSeed) => {
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
- get32ByteSeedFromMnemonic as get32ByteSeedFromMnemonic2
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 get32ByteSeedFromMnemonic2(words, passphrase ?? "");
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
- useEffect(() => {
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 useEffect2 } from "react";
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
- useEffect2(() => {
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 [copyFailed, setCopyFailed] = useState4(false);
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
- setCopyFailed(false);
414
+ setCopyError(null);
391
415
  onCopyAllowed?.();
392
416
  } catch {
393
- setCopyFailed(true);
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
- copyFailed && /* @__PURE__ */ jsx2("div", { "data-testid": "copy-error", children: "Copy failed \u2014 try again" }),
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 useEffect3, useRef } from "react";
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 = useRef(false);
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
- useEffect3(() => {
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 useEffect4, useRef as useRef2 } from "react";
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 = useRef2(state.status);
717
- const lastImportedRef = useRef2(null);
718
- useEffect4(() => {
740
+ const prevStatusRef = useRef3(state.status);
741
+ const lastImportedRef = useRef3(null);
742
+ useEffect5(() => {
719
743
  dispatch({ type: "CHECK" });
720
744
  }, [state.words]);
721
- useEffect4(() => {
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 useCallback6 } from "react";
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 = useCallback6(
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 useState7, useCallback as useCallback7, useEffect as useEffect5, useRef as useRef3 } from "react";
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] = useState7(null);
825
- const [isLoaded, setIsLoaded] = useState7(false);
826
- const [error, setError] = useState7(null);
827
- const prevIdentityIdRef = useRef3(null);
828
- useEffect5(() => {
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 = useCallback7(async (data) => {
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 = useCallback7(async () => {
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 = useCallback7(async () => {
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 useState8, useEffect as useEffect6, useCallback as useCallback8, useRef as useRef4 } from "react";
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 { 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(() => {
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
- useEffect6(() => {
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 = useCallback8(() => {
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
- useEffect6(() => {
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
- 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
- }
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: toB64(salt),
1024
- iv: toB64(iv),
1025
- ct: toB64(new Uint8Array(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: fromB64(envelope.iv) },
1055
+ { name: "AES-GCM", iv: base64urlDecode(envelope.iv) },
1032
1056
  key,
1033
- fromB64(envelope.ct)
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, reject) => {
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");