@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/react/Me2emProvider.tsx","../src/react/context.ts","../src/react/useCreateIdentity.ts","../src/headless/identity-flow.ts","../src/react/useImportSeed.ts","../src/react/useHandle.ts","../src/react/useSubHandle.ts","../src/react/SeedPhraseDisplay.tsx","../src/headless/display-flow.ts","../src/react/SeedPhraseVerify.tsx","../src/headless/seed-words.ts","../src/headless/verify-flow.ts","../src/react/SeedPhraseImport.tsx","../src/headless/import-flow.ts","../src/react/PassphraseInput.tsx","../src/react/useIdentityContext.ts","../src/react/useSession.ts","../src/headless/index.ts","../src/headless/context-crypto.ts","../src/headless/identity-context-storage.ts"],"sourcesContent":["import {\n useState,\n useCallback,\n useMemo,\n type ReactNode,\n} from 'react';\nimport { Identity } from '@me2em/core';\nimport { Me2emContext, type Me2emContextValue } from './context.js';\n\nexport function Me2emProvider({ children }: { children: ReactNode }) {\n const [identity, setIdentity] = useState<Identity | null>(null);\n\n const activateIdentity = useCallback(async (seed: Uint8Array): Promise<boolean> => {\n try {\n const id = await Identity.fromSeed(seed);\n setIdentity(id);\n return true;\n } catch {\n return false;\n }\n }, []);\n\n const clearIdentity = useCallback(() => {\n setIdentity(null);\n }, []);\n\n const value = useMemo<Me2emContextValue>(\n () => ({ identity, hasIdentity: identity !== null, activateIdentity, clearIdentity }),\n [identity, activateIdentity, clearIdentity]\n );\n\n return <Me2emContext.Provider value={value}>{children}</Me2emContext.Provider>;\n}\n","import { createContext, useContext } from 'react';\nimport type { Identity } from '@me2em/core';\n\nexport interface Me2emContextValue {\n identity: Identity | null;\n hasIdentity: boolean;\n activateIdentity: (seed: Uint8Array) => Promise<boolean>;\n clearIdentity: () => void;\n}\n\nexport const Me2emContext = createContext<Me2emContextValue | null>(null);\n\nexport function useMe2emContext(): Me2emContextValue {\n const ctx = useContext(Me2emContext);\n if (!ctx) {\n throw new Error('Me2em hooks must be used inside <Me2emProvider>');\n }\n return ctx;\n}\n","// packages/react/src/react/useCreateIdentity.ts\nimport {\n useReducer,\n useCallback,\n type Reducer,\n} from 'react';\nimport {\n generateSeedPhrase,\n get32ByteSeedFromMnemonic,\n type SeedStrength,\n type Identity,\n} from '@me2em/core';\nimport {\n identityFlowReducer,\n initialIdentityFlowState,\n} from '../headless/identity-flow.js';\nimport type { IdentityFlowState, IdentityFlowEvent, IdentityFlowStatus } from '../headless/types.js';\nimport { useMe2emContext } from './context.js';\n\nexport interface UseCreateIdentityResult {\n status: IdentityFlowStatus;\n seedWords: string[] | null;\n identity: Identity | null;\n error: string | null;\n generate: (opts?: { strength?: SeedStrength }) => void;\n confirmWords: () => Promise<void>;\n reset: () => void;\n fail: (message: string) => void;\n}\n\nexport function useCreateIdentity(): UseCreateIdentityResult {\n const { activateIdentity, clearIdentity, identity } = useMe2emContext();\n const [state, dispatch] = useReducer<\n Reducer<IdentityFlowState, IdentityFlowEvent>\n >(identityFlowReducer, initialIdentityFlowState);\n\n const generate = useCallback(\n (opts?: { strength?: SeedStrength }) => {\n const words = generateSeedPhrase(opts?.strength ?? 128);\n get32ByteSeedFromMnemonic(words).then((derivedSeed) => {\n dispatch({ type: 'GENERATE', words, seed: derivedSeed });\n }).catch(() => {\n dispatch({ type: 'FAIL', message: 'Failed to generate seed' });\n });\n },\n []\n );\n\n const confirmWords = useCallback(async () => {\n if (state.status !== 'seed-generated' || !state.identitySeed) return;\n dispatch({ type: 'CONFIRM_WORDS' });\n const ok = await activateIdentity(state.identitySeed);\n if (!ok) {\n dispatch({ type: 'FAIL', message: 'Identity activation failed' });\n }\n }, [state.status, state.identitySeed, activateIdentity]);\n\n const reset = useCallback(() => {\n dispatch({ type: 'RESET' });\n clearIdentity();\n }, [clearIdentity]);\n\n const fail = useCallback((message: string) => {\n dispatch({ type: 'FAIL', message });\n }, []);\n\n return {\n status: state.status,\n seedWords: state.seedWords,\n identity,\n error: state.error,\n generate,\n confirmWords,\n reset,\n fail,\n };\n}\n","import type { IdentityFlowState, IdentityFlowEvent } from './types.js';\n\nexport const initialIdentityFlowState: IdentityFlowState = {\n status: 'idle',\n seedWords: null,\n identitySeed: null,\n error: null,\n};\n\nconst UNEXPECTED_EVENTS: Record<string, string> = {\n CONFIRM_WORDS: 'CONFIRM_WORDS called from idle — no seed to confirm',\n RESET: 'RESET called from idle — nothing to reset',\n};\n\nexport function identityFlowReducer(\n state: IdentityFlowState,\n event: IdentityFlowEvent\n): IdentityFlowState {\n const { type } = event;\n\n switch (state.status) {\n case 'idle': {\n if (type === 'GENERATE') {\n return {\n status: 'seed-generated',\n seedWords: event.words,\n identitySeed: event.seed,\n error: null,\n };\n }\n if (type === 'IMPORT') {\n return {\n status: 'imported',\n seedWords: null,\n identitySeed: event.seed,\n error: null,\n };\n }\n if (type === 'FAIL') {\n return {\n status: 'error',\n seedWords: null,\n identitySeed: null,\n error: event.message,\n };\n }\n if (UNEXPECTED_EVENTS[type]) {\n console.warn(UNEXPECTED_EVENTS[type]);\n return state;\n }\n return state;\n }\n\n case 'seed-generated': {\n if (type === 'CONFIRM_WORDS') {\n return {\n status: 'verified',\n seedWords: null,\n identitySeed: state.identitySeed,\n error: null,\n };\n }\n if (type === 'FAIL') {\n return {\n status: 'error',\n seedWords: null,\n identitySeed: null,\n error: event.message,\n };\n }\n if (type === 'RESET') {\n return initialIdentityFlowState;\n }\n console.warn(`Unexpected event ${type} in seed-generated`);\n return state;\n }\n\n case 'verified':\n case 'imported': {\n if (type === 'RESET') {\n return initialIdentityFlowState;\n }\n if (type === 'FAIL') {\n return {\n status: 'error',\n seedWords: null,\n identitySeed: null,\n error: event.message,\n };\n }\n console.warn(`Unexpected event ${type} in ${state.status}`);\n return state;\n }\n\n case 'error': {\n if (type === 'RESET') {\n return initialIdentityFlowState;\n }\n console.warn(`Unexpected event ${type} in error`);\n return state;\n }\n\n default:\n return state;\n }\n}\n","// packages/react/src/react/useImportSeed.ts\nimport { useReducer, useCallback, type Reducer } from 'react';\nimport {\n validateSeedPhrase,\n get32ByteSeedFromMnemonic,\n type Identity,\n} from '@me2em/core';\nimport {\n identityFlowReducer,\n initialIdentityFlowState,\n} from '../headless/identity-flow.js';\nimport type { IdentityFlowState, IdentityFlowEvent, IdentityFlowStatus } from '../headless/types.js';\nimport { useMe2emContext } from './context.js';\n\nexport interface UseImportSeedResult {\n status: IdentityFlowStatus;\n identity: Identity | null;\n error: string | null;\n importWords: (words: string[], passphrase?: string) => Promise<void>;\n reset: () => void;\n}\n\nexport function useImportSeed(): UseImportSeedResult {\n const { activateIdentity, clearIdentity, identity } = useMe2emContext();\n const [state, dispatch] = useReducer<\n Reducer<IdentityFlowState, IdentityFlowEvent>\n >(identityFlowReducer, initialIdentityFlowState);\n\n const importWords = useCallback(\n async (words: string[], passphrase?: string) => {\n const validation = validateSeedPhrase(words);\n if (!validation.isValid) {\n dispatch({ type: 'FAIL', message: validation.error ?? 'Invalid seed phrase' });\n return;\n }\n try {\n const seed = await get32ByteSeedFromMnemonic(words, passphrase ?? '');\n dispatch({ type: 'IMPORT', seed });\n await activateIdentity(seed);\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Import failed';\n dispatch({ type: 'FAIL', message });\n }\n },\n [activateIdentity]\n );\n\n const reset = useCallback(() => {\n dispatch({ type: 'RESET' });\n clearIdentity();\n }, [clearIdentity]);\n\n return {\n status: state.status,\n identity,\n error: state.error,\n importWords,\n reset,\n };\n}\n","// packages/react/src/react/useHandle.ts\nimport { useState, useEffect } from 'react';\nimport { type Handle } from '@me2em/core';\nimport { useMe2emContext } from './context.js';\n\nexport function useHandle(name: string): Handle | null {\n const { identity } = useMe2emContext();\n const [handle, setHandle] = useState<Handle | null>(null);\n\n useEffect(() => {\n let cancelled = false;\n if (!identity) {\n setHandle(null);\n return;\n }\n // Core performs full name canonicalization (NFKC, lowercase,\n // trim) and validation — no local normalization here.\n identity.deriveHandle(name).then((h) => {\n if (!cancelled) setHandle(h);\n }).catch((err) => {\n if (!cancelled) console.error('useHandle derivation failed', err);\n });\n return () => { cancelled = true; };\n }, [identity, name]);\n\n return handle;\n}\n","import { useState, useEffect } from 'react';\nimport { type SubHandle } from '@me2em/core';\nimport { useMe2emContext } from './context.js';\n\nexport function useSubHandle(\n handleName: string,\n subName: string\n): SubHandle | null {\n const { identity } = useMe2emContext();\n const [subHandle, setSubHandle] = useState<SubHandle | null>(null);\n\n useEffect(() => {\n let cancelled = false;\n if (!identity) {\n setSubHandle(null);\n return;\n }\n identity\n .deriveSubHandle(handleName, subName)\n .then((h) => {\n if (!cancelled) setSubHandle(h);\n })\n .catch((err) => {\n if (!cancelled) console.error('useSubHandle derivation failed', err);\n });\n return () => { cancelled = true; };\n }, [identity, handleName, subName]);\n\n return subHandle;\n}\n","// packages/react/src/react/SeedPhraseDisplay.tsx\nimport { useReducer, useState } from 'react';\nimport {\n displayFlowReducer,\n initialDisplayState,\n} from '../headless/display-flow.js';\n\nexport interface SeedPhraseDisplayProps {\n words: string[];\n onDismiss?: () => void;\n onCopyAllowed?: () => void;\n}\n\nexport function SeedPhraseDisplay({\n words,\n onDismiss,\n onCopyAllowed,\n}: SeedPhraseDisplayProps) {\n const [state, dispatch] = useReducer(displayFlowReducer, initialDisplayState);\n const [copyFailed, setCopyFailed] = useState(false);\n\n const { status, copyCount } = state;\n\n const handleReveal = () => {\n dispatch({ type: 'REVEAL', at: Date.now() });\n };\n\n const handleCopy = async () => {\n if (status === 'revealed' && copyCount === 0) {\n try {\n await (navigator as any).clipboard.writeText(words.join(' '));\n dispatch({ type: 'COPY', at: Date.now() });\n setCopyFailed(false);\n onCopyAllowed?.();\n } catch {\n setCopyFailed(true);\n }\n }\n };\n\n const handleDismiss = () => {\n dispatch({ type: 'DISMISS' });\n onDismiss?.();\n };\n\n if (status === 'hidden') {\n return (\n <button data-testid=\"reveal\" onClick={handleReveal}>\n Reveal\n </button>\n );\n }\n\n if (status === 'dismissed') {\n return (\n <div data-testid=\"dismissed\">\n Phrase dismissed\n </div>\n );\n }\n\n return (\n <div>\n <ol data-testid=\"words\">\n {words.map((word, i) => (\n <li\n key={i}\n data-testid={`word-${i}`}\n data-position={i}\n >\n {word}\n </li>\n ))}\n </ol>\n {copyFailed && (\n <div data-testid=\"copy-error\">\n Copy failed — try again\n </div>\n )}\n <button\n data-testid=\"copy-all\"\n onClick={handleCopy}\n disabled={copyCount === 1}\n >\n {copyCount === 1 ? 'Copied' : 'Copy all'}\n </button>\n <button data-testid=\"dismiss\" onClick={handleDismiss}>\n Hide\n </button>\n </div>\n );\n}\n","// packages/react/src/headless/display-flow.ts\n\nexport type DisplayStatus =\n | 'hidden'\n | 'revealed'\n | 'copied'\n | 'dismissed';\n\nexport interface DisplayState {\n status: DisplayStatus;\n copyCount: number;\n revealedAt: number | null;\n}\n\nexport type DisplayEvent =\n | { type: 'REVEAL'; at: number }\n | { type: 'COPY'; at: number }\n | { type: 'DISMISS' }\n | { type: 'RESET' };\n\nexport const initialDisplayState: DisplayState = {\n status: 'hidden',\n copyCount: 0,\n revealedAt: null,\n};\n\nexport function displayFlowReducer(\n state: DisplayState,\n event: DisplayEvent\n): DisplayState {\n const { type } = event;\n\n switch (state.status) {\n case 'hidden': {\n if (type === 'REVEAL') {\n return {\n status: 'revealed',\n copyCount: 0,\n revealedAt: event.at,\n };\n }\n if (type === 'COPY') {\n console.warn('Cannot copy while hidden');\n return state;\n }\n if (type === 'RESET') {\n return initialDisplayState;\n }\n if (type === 'DISMISS') {\n console.warn('Cannot dismiss while hidden');\n return state;\n }\n console.warn(`Unexpected event ${type} in hidden`);\n return state;\n }\n\n case 'revealed': {\n if (type === 'COPY') {\n if (state.copyCount === 0) {\n return {\n status: 'copied',\n copyCount: 1,\n revealedAt: state.revealedAt,\n };\n }\n console.warn('Already copied — duplicate copy blocked');\n return state;\n }\n if (type === 'DISMISS') {\n return {\n status: 'dismissed',\n copyCount: state.copyCount,\n revealedAt: state.revealedAt,\n };\n }\n if (type === 'RESET') {\n return initialDisplayState;\n }\n console.warn(`Unexpected event ${type} in revealed`);\n return state;\n }\n\n case 'copied': {\n if (type === 'COPY') {\n console.warn('Already copied — duplicate copy blocked');\n return state;\n }\n if (type === 'DISMISS') {\n return {\n status: 'dismissed',\n copyCount: 1,\n revealedAt: state.revealedAt,\n };\n }\n if (type === 'RESET') {\n return initialDisplayState;\n }\n console.warn(`Unexpected event ${type} in copied`);\n return state;\n }\n\n case 'dismissed': {\n if (type === 'RESET') {\n return initialDisplayState;\n }\n console.warn(`Unexpected event ${type} in dismissed`);\n return state;\n }\n\n default:\n return state;\n }\n}\n","// packages/react/src/react/SeedPhraseVerify.tsx\nimport { useReducer, useCallback, useState, useEffect, useRef } from 'react';\nimport {\n verifyFlowReducer,\n initVerifyState,\n type VerifyState,\n} from '../headless/verify-flow.js';\nimport { wordlist } from '@me2em/core';\n\nexport interface SeedPhraseVerifyProps {\n realWords: string[];\n gridSize?: number;\n wordlist?: readonly string[];\n onVerified: () => void;\n onFailed?: (attempts: number) => void;\n}\n\nconst DEFAULT_GRID_SIZE_OFFSET = 8;\n\nexport function SeedPhraseVerify({\n realWords,\n gridSize,\n wordlist: propWordlist,\n onVerified,\n onFailed,\n}: SeedPhraseVerifyProps) {\n const grid = gridSize ?? realWords.length + DEFAULT_GRID_SIZE_OFFSET;\n const allWords = propWordlist ?? wordlist;\n const baseSeed = 42;\n\n const [state, dispatch] = useReducer(\n verifyFlowReducer,\n {\n realWords,\n gridSize: grid,\n randomSeed: baseSeed,\n allWords,\n } as const,\n (params) => initVerifyState(params.realWords, params.gridSize, params.randomSeed, params.allWords)\n );\n\n const [retryCount, setRetryCount] = useState(0);\n\n const { status, grid: gridData, selected } = state;\n\n // Guard against re-entrant callbacks after status change\n const notifiedRef = useRef(false);\n\n const handleSelect = useCallback(\n (index: number) => {\n if (status === 'correct' || status === 'incorrect') return;\n if (selected.length >= gridData.correctIndices.length) return;\n dispatch({ type: 'SELECT', index });\n },\n [status, selected.length, gridData.correctIndices.length]\n );\n\n // Single effect: auto-check when enough words selected, then notify result\n useEffect(() => {\n if (notifiedRef.current) return;\n\n // Auto-check when enough words selected\n if (\n selected.length === gridData.correctIndices.length &&\n selected.length > 0 &&\n status === 'collecting'\n ) {\n dispatch({ type: 'CHECK' });\n // After dispatch, status will change synchronously in the reducer\n // but the new state won't be reflected until next render.\n // We need to check the pending result in the next effect cycle.\n return;\n }\n\n // Handle result after status update\n if (status === 'correct') {\n notifiedRef.current = true;\n onVerified();\n } else if (status === 'incorrect') {\n notifiedRef.current = true;\n onFailed?.(state.attempts);\n }\n }, [selected.length, gridData.correctIndices.length, status, state.attempts, onVerified, onFailed]);\n\n const handleRetry = () => {\n notifiedRef.current = false;\n setRetryCount((c) => c + 1);\n dispatch({\n type: 'RESET',\n realWords,\n gridSize: grid,\n randomSeed: baseSeed + retryCount + 1,\n allWords,\n });\n };\n\n return (\n <div>\n <div data-testid=\"progress\">\n Select word {selected.length + 1} of {realWords.length}\n </div>\n <ol data-testid=\"grid\">\n {gridData.words.map((word, i) => {\n const isSelected = selected.includes(i);\n const isDisabled =\n isSelected || status === 'correct' || status === 'incorrect';\n return (\n <li key={i}>\n <button\n data-testid={`cell-${i}`}\n data-word={word}\n disabled={isDisabled}\n onClick={() => handleSelect(i)}\n >\n {word}\n </button>\n </li>\n );\n })}\n </ol>\n {status === 'incorrect' && (\n <button data-testid=\"retry\" onClick={handleRetry}>\n Try again\n </button>\n )}\n </div>\n );\n}\n","// packages/react/src/headless/seed-words.ts\n\nexport function shuffleWithSeed<T>(items: T[], seed: number): T[] {\n const arr = [...items];\n let currentSeed = seed;\n function nextRand(): number {\n currentSeed = (currentSeed * 1664525 + 1013904223) >>> 0;\n return currentSeed;\n }\n for (let i = arr.length - 1; i > 0; i--) {\n const j = nextRand() % (i + 1);\n [arr[i], arr[j]] = [arr[j], arr[i]];\n }\n return arr;\n}\n\nexport function buildVerificationGrid(\n realWords: string[],\n gridSize: number,\n randomSeed: number,\n allWords: readonly string[]\n): { words: string[]; correctIndices: number[] } {\n if (gridSize < realWords.length) {\n throw new Error(\n `gridSize (${gridSize}) must be >= realWords.length (${realWords.length})`\n );\n }\n\n const shuffled = shuffleWithSeed(realWords, randomSeed);\n const realSet = new Set(realWords);\n const available = allWords.filter((w) => !realSet.has(w));\n const shuffledDecoys = shuffleWithSeed(available, randomSeed + 1);\n\n const needed = gridSize - realWords.length;\n const decoys = shuffledDecoys.slice(0, needed);\n\n const words = [...shuffled, ...decoys];\n if (words.length !== gridSize) {\n throw new Error(\n `Cannot fill grid: wordlist too small (got ${words.length}, need ${gridSize})`\n );\n }\n\n // Map each word occurrence to a DISTINCT grid position, so\n // repeated words in the phrase get their own cells.\n const positions = new Map<string, number[]>();\n words.forEach((w, i) => {\n const list = positions.get(w);\n if (list) list.push(i);\n else positions.set(w, [i]);\n });\n\n const correctIndices = realWords.map((w) => {\n const list = positions.get(w);\n if (!list || list.length === 0) {\n throw new Error(`Internal error: word \"${w}\" missing from grid`);\n }\n return list.shift() as number;\n });\n\n return { words, correctIndices };\n}\n\n/**\n * Selection is compared IN ORDER: selected[i] must equal\n * correctIndices[i]. The i-th click must be the i-th word of the\n * phrase. Repeated phrase words map to distinct grid cells.\n */\nexport function isGridSelectionCorrect(\n grid: { words: string[]; correctIndices: number[] },\n selected: number[]\n): boolean {\n if (selected.length !== grid.correctIndices.length) return false;\n return selected.every((v, i) => v === grid.correctIndices[i]);\n}\n","// packages/react/src/headless/verify-flow.ts\nimport {\n buildVerificationGrid,\n isGridSelectionCorrect,\n} from './seed-words.js';\n\nexport type VerifyStatus = 'collecting' | 'correct' | 'incorrect';\n\nexport interface VerifyState {\n status: VerifyStatus;\n grid: { words: string[]; correctIndices: number[] };\n selected: number[];\n attempts: number;\n}\n\nexport type VerifyEvent =\n | { type: 'SELECT'; index: number }\n | { type: 'DESELECT'; index: number }\n | { type: 'CHECK' }\n | {\n type: 'RESET';\n realWords: string[];\n gridSize: number;\n randomSeed: number;\n allWords: readonly string[];\n };\n\nexport function initVerifyState(\n realWords: string[],\n gridSize: number,\n randomSeed: number,\n allWords: readonly string[]\n): VerifyState {\n const grid = buildVerificationGrid(realWords, gridSize, randomSeed, allWords);\n return {\n status: 'collecting',\n grid,\n selected: [],\n attempts: 0,\n };\n}\n\nexport function verifyFlowReducer(\n state: VerifyState,\n event: VerifyEvent\n): VerifyState {\n const { type } = event;\n\n if (type === 'RESET') {\n return initVerifyState(\n event.realWords,\n event.gridSize,\n event.randomSeed,\n event.allWords\n );\n }\n\n // Terminal states: only RESET allowed\n if (state.status === 'correct' || state.status === 'incorrect') {\n console.warn(\n `Cannot ${type} in ${state.status} state — use RESET to retry`\n );\n return state;\n }\n\n // collecting state\n if (type === 'SELECT') {\n const { index } = event;\n const { grid, selected } = state;\n if (selected.includes(index)) {\n console.warn(`Index ${index} already selected — use DESELECT`);\n return state;\n }\n if (index < 0 || index >= grid.words.length) {\n console.warn(`Index ${index} out of grid bounds`);\n return state;\n }\n return { ...state, selected: [...selected, index] };\n }\n\n if (type === 'DESELECT') {\n const { index } = event;\n const idx = state.selected.indexOf(index);\n if (idx === -1) {\n console.warn(`Index ${index} not selected`);\n return state;\n }\n return {\n ...state,\n selected: state.selected.filter((_, i) => i !== idx),\n };\n }\n\n if (type === 'CHECK') {\n const { grid, selected, attempts } = state;\n if (selected.length !== grid.correctIndices.length) {\n console.warn(\n `Cannot check: selected ${selected.length}, need ${grid.correctIndices.length}`\n );\n return state;\n }\n const correct = isGridSelectionCorrect(grid, selected);\n return {\n ...state,\n status: correct ? 'correct' : 'incorrect',\n attempts: attempts + 1,\n };\n }\n\n console.warn(`Unexpected event ${type} in collecting`);\n return state;\n}\n","// packages/react/src/react/SeedPhraseImport.tsx\nimport { useReducer, useCallback, useEffect, useRef } from 'react';\nimport {\n importFlowReducer,\n initImportState,\n type ImportState,\n} from '../headless/import-flow.js';\nimport { wordlist } from '@me2em/core';\n\nexport interface SeedPhraseImportProps {\n expectedCount?: 12 | 24;\n wordlist?: readonly string[];\n onImported: (words: string[]) => void;\n}\n\nexport function SeedPhraseImport({\n expectedCount: propCount,\n wordlist: propWordlist,\n onImported,\n}: SeedPhraseImportProps) {\n const allWords = propWordlist ?? wordlist;\n\n const [state, dispatch] = useReducer(\n importFlowReducer,\n { expectedCount: propCount ?? 12 } as const,\n (params) => initImportState(params.expectedCount)\n );\n\n const count = state.expectedCount;\n\n const prevStatusRef = useRef<ImportState['status']>(state.status);\n\n const lastImportedRef = useRef<string | null>(null);\n\n // Auto-check on every word change\n useEffect(() => {\n dispatch({ type: 'CHECK' });\n }, [state.words]);\n\n // Call onImported once when transitioning to valid\n useEffect(() => {\n const prevStatus = prevStatusRef.current;\n prevStatusRef.current = state.status;\n\n if (state.status === 'valid' && prevStatus !== 'valid') {\n const key = (state.words as string[]).join('\\0');\n if (lastImportedRef.current !== key) {\n lastImportedRef.current = key;\n onImported(state.words as string[]);\n }\n }\n }, [state.status, state.words, onImported]);\n\n const handleCountChange = (n: 12 | 24) => {\n dispatch({ type: 'INIT', expectedCount: n });\n };\n\n const handleWordChange = (position: number, value: string) => {\n dispatch({ type: 'SET_WORD', position, word: value });\n };\n\n const isWordInvalid = (position: number): boolean => {\n if (state.status !== 'invalid' && state.status !== 'valid') return false;\n const word = state.words[position];\n if (word === null) return false;\n return !allWords.includes(word);\n };\n\n const isImportEnabled = state.status === 'valid';\n\n return (\n <div>\n <div>\n <button\n data-testid=\"count-12\"\n onClick={() => handleCountChange(12)}\n disabled={count === 12}\n >\n 12 words\n </button>\n <button\n data-testid=\"count-24\"\n onClick={() => handleCountChange(24)}\n disabled={count === 24}\n >\n 24 words\n </button>\n </div>\n <div data-testid=\"inputs\">\n {Array.from({ length: count }, (_, i) => (\n <input\n key={i}\n data-testid={`word-${i}`}\n aria-label={`Word ${i + 1}`}\n value={state.words[i] ?? ''}\n onChange={(e) => handleWordChange(i, e.target.value)}\n data-invalid={isWordInvalid(i) ? 'true' : undefined}\n />\n ))}\n </div>\n <button\n data-testid=\"import\"\n disabled={!isImportEnabled}\n onClick={() => dispatch({ type: 'CHECK' })}\n >\n Import\n </button>\n </div>\n );\n}\n","// packages/react/src/headless/import-flow.ts\nimport { validateSeedPhrase, type SeedStrength } from '@me2em/core';\n\nexport type ImportStatus = 'entering' | 'valid' | 'invalid';\n\nexport interface ImportState {\n status: ImportStatus;\n words: (string | null)[];\n expectedCount: 12 | 24;\n}\n\nexport type ImportEvent =\n | { type: 'INIT'; expectedCount: 12 | 24 }\n | { type: 'SET_WORD'; position: number; word: string }\n | { type: 'CLEAR_WORD'; position: number }\n | { type: 'CHECK' }\n | { type: 'RESET' };\n\nexport function initImportState(expectedCount: 12 | 24): ImportState {\n return {\n status: 'entering',\n words: Array(expectedCount).fill(null),\n expectedCount,\n };\n}\n\nexport function importFlowReducer(\n state: ImportState,\n event: ImportEvent\n): ImportState {\n const { type } = event;\n\n if (type === 'INIT') {\n return initImportState(event.expectedCount);\n }\n\n if (type === 'RESET') {\n return initImportState(state.expectedCount);\n }\n\n if (type === 'SET_WORD') {\n const { position, word } = event;\n if (position < 0 || position >= state.expectedCount) {\n console.warn(`SET_WORD: position ${position} out of bounds`);\n return state;\n }\n const normalized = word.toLowerCase().trim();\n const newWords = [...state.words] as (string | null)[];\n newWords[position] = normalized;\n return { ...state, words: newWords, status: 'entering' };\n }\n\n if (type === 'CLEAR_WORD') {\n const { position } = event;\n if (position < 0 || position >= state.expectedCount) {\n console.warn(`CLEAR_WORD: position ${position} out of bounds`);\n return state;\n }\n const newWords = [...state.words] as (string | null)[];\n newWords[position] = null;\n return { ...state, words: newWords, status: 'entering' };\n }\n\n if (type === 'CHECK') {\n const { words } = state;\n const nullIdx = words.findIndex((w) => w === null);\n if (nullIdx !== -1) {\n return {\n ...state,\n status: 'invalid',\n };\n }\n const result = validateSeedPhrase(words as string[]);\n return {\n ...state,\n status: result.isValid ? 'valid' : 'invalid',\n };\n }\n\n console.warn(`Unexpected event ${type} in import-flow`);\n return state;\n}\n","// packages/react/src/react/PassphraseInput.tsx\nimport { useState, useCallback } from 'react';\n\nexport interface PassphraseInputProps {\n onPassphraseChange: (p: string) => void;\n showWarnings?: boolean;\n}\n\nexport function PassphraseInput({\n onPassphraseChange,\n showWarnings = true,\n}: PassphraseInputProps) {\n const handleChange = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n const value = e.target.value.normalize('NFKC');\n onPassphraseChange(value);\n },\n [onPassphraseChange]\n );\n\n return (\n <div>\n <input\n type=\"password\"\n data-testid=\"passphrase\"\n aria-label=\"BIP39 passphrase\"\n onChange={handleChange}\n />\n {showWarnings && (\n <div data-testid=\"warning\">\n Passphrase is case-sensitive and NOT recoverable. A different\n passphrase silently derives a different wallet.\n </div>\n )}\n </div>\n );\n}\n","// packages/react/src/react/useIdentityContext.ts\nimport { useState, useCallback, useEffect, useRef } from 'react';\nimport type { IdentityContextStorage, IdentityContextData } from '../headless/identity-context-types.js';\nimport { useMe2emContext } from './context.js';\n\nfunction publicKeyToId(publicKey: Uint8Array): string {\n return Array.from(publicKey)\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n}\n\nexport interface UseIdentityContextResult {\n context: IdentityContextData | null;\n isLoaded: boolean;\n error: string | null;\n save: (data: IdentityContextData) => Promise<void>;\n clear: () => Promise<void>;\n /** Reload the cache for the current identity (after sync). */\n refresh: () => Promise<void>;\n}\n\nexport function useIdentityContext(\n storage: IdentityContextStorage\n): UseIdentityContextResult {\n const { identity } = useMe2emContext();\n const [context, setContext] = useState<IdentityContextData | null>(null);\n const [isLoaded, setIsLoaded] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const prevIdentityIdRef = useRef<string | null>(null);\n\n useEffect(() => {\n let cancelled = false;\n\n if (!identity) {\n setContext(null);\n setIsLoaded(true);\n setError(null);\n return;\n }\n\n const identityId = publicKeyToId(identity.getPublicKey());\n\n // If identity changed, close old storage and open new\n if (prevIdentityIdRef.current !== null && prevIdentityIdRef.current !== identityId) {\n storage.close().catch(() => { /* ignore close errors */ });\n }\n\n prevIdentityIdRef.current = identityId;\n\n // Open storage for this identity and load cached context (atomic)\n storage.openAndLoad(identityId)\n .then((data) => {\n if (cancelled) return;\n setContext(data);\n setIsLoaded(true);\n setError(null);\n })\n .catch((err) => {\n if (cancelled) return;\n setError(err instanceof Error ? err.message : String(err));\n setIsLoaded(true);\n });\n\n return () => {\n cancelled = true;\n };\n }, [identity, storage]);\n\n const save = useCallback(async (data: IdentityContextData): Promise<void> => {\n await storage.save(data);\n setContext(data);\n }, [storage]);\n\n const clear = useCallback(async (): Promise<void> => {\n await storage.clear();\n setContext(null);\n }, [storage]);\n\n const refresh = useCallback(async (): Promise<void> => {\n if (!identity) return;\n const identityId = publicKeyToId(identity.getPublicKey());\n const data = await storage.openAndLoad(identityId);\n setContext(data);\n }, [identity, storage]);\n\n return { context, isLoaded, error, save, clear, refresh };\n}\n","// packages/react/src/react/useSession.ts\nimport { useState, useEffect, useCallback, useRef } from 'react';\nimport type { Handle } from '@me2em/core';\nimport { Session, type SessionOptions, type SessionPayload } from '@me2em/core';\nimport { useMe2emContext } from './context.js';\n\n/**\n * Options for useSession, memoized via deep comparison of\n * `handle` and `options` refs on each render.\n *\n * @category Types\n */\nexport interface UseSessionOptions {\n /** Intended audience (recipient) of this session. */\n audience: string;\n /** Permission scopes granted in this session. */\n scopes: string[];\n /** Time-to-live in seconds. */\n ttl: number;\n}\n\n/**\n * Result of the useSession hook.\n *\n * @category Types\n */\nexport interface UseSessionResult {\n /** The active session, or null if no handle or session creation failed. */\n session: Session | null;\n /** Whether the session (or its absence) has been determined. */\n isLoaded: boolean;\n /** Error string if session creation or renewal failed. */\n error: string | null;\n /** Whether the current session has expired. */\n isExpired: boolean;\n /** Manually renew the session (creates a new one with a fresh jti). */\n renew: () => void;\n}\n\n/**\n * Hook that creates and manages a signed session token for a Handle.\n *\n * When `handle` is provided, creates a Session with the given options.\n * Supports auto-renewal: when `autoRenew` is true (default), a timer\n * fires at half the session TTL to create a fresh session with a new\n * jti (the previous token remains valid until its own expiry, but the\n * lineage identity is preserved through `handle`).\n *\n * On unmount, all timers are cleaned up to prevent state updates after\n * the component is gone.\n *\n * Dependencies: `[handle, renew]` — the options object is read through\n * a ref, so passing a fresh object literal on every render does NOT\n * re-trigger session creation. Only changes to `handle` (or to the\n * renewal function identity, which follows `handle`) do.\n *\n * @param handle - The Handle to sign the session, or null.\n * @param options - Session options (audience, scopes, ttl).\n * @param autoRenew - Whether to auto-renew at half-life (default: true).\n * @returns Session state with manual `renew` control.\n *\n * @example\n * ```ts\n * const handle = useHandle('my-handle');\n * const { session, isExpired, renew } = useSession(handle, {\n * audience: 'app.example.com',\n * scopes: ['read', 'write'],\n * ttl: 3600,\n * });\n * ```\n */\nexport function useSession(\n handle: Handle | null,\n options: UseSessionOptions,\n autoRenew: boolean = true\n): UseSessionResult {\n const { identity } = useMe2emContext();\n const [session, setSession] = useState<Session | null>(null);\n const [isLoaded, setIsLoaded] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const optionsRef = useRef(options);\n const mountedRef = useRef(true);\n\n // Update options ref without triggering re-renders\n optionsRef.current = options;\n\n const clearTimer = useCallback(() => {\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n }, []);\n\n // Cleanup on unmount\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n clearTimer();\n };\n }, [clearTimer]);\n\n const renew = useCallback(() => {\n clearTimer();\n const opts = optionsRef.current;\n\n if (!handle) {\n if (!mountedRef.current) return;\n setSession(null);\n setIsLoaded(true);\n setError(null);\n return;\n }\n\n Session.create(handle, {\n audience: opts.audience,\n scopes: opts.scopes,\n ttl: opts.ttl,\n }).then((s) => {\n if (!mountedRef.current) return;\n setSession(s);\n setIsLoaded(true);\n setError(null);\n\n // Schedule auto-renewal at half-life\n if (autoRenew) {\n const halfLife = Math.floor((s.expiresAt * 1000 - Date.now()) / 2);\n if (halfLife > 0) {\n timerRef.current = setTimeout(() => {\n renew();\n }, Math.max(halfLife, 1000));\n }\n }\n }).catch((err) => {\n if (!mountedRef.current) return;\n setSession(null);\n setIsLoaded(true);\n setError(err instanceof Error ? err.message : String(err));\n });\n }, [handle, autoRenew, clearTimer]);\n\n // Create session when handle or options change\n useEffect(() => {\n if (mountedRef.current) {\n renew();\n }\n }, [handle, renew]);\n\n return {\n session,\n isLoaded,\n error,\n isExpired: session ? session.isExpired() : false,\n renew,\n };\n}\n","export { identityFlowReducer, initialIdentityFlowState } from './identity-flow.js';\nexport type {\n IdentityFlowState,\n IdentityFlowStatus,\n IdentityFlowEvent,\n} from './types.js';\nexport {\n buildVerificationGrid,\n isGridSelectionCorrect,\n shuffleWithSeed,\n} from './seed-words.js';\nexport {\n displayFlowReducer,\n initialDisplayState,\n} from './display-flow.js';\nexport type {\n DisplayState,\n DisplayStatus,\n DisplayEvent,\n} from './display-flow.js';\nexport {\n verifyFlowReducer,\n initVerifyState,\n} from './verify-flow.js';\nexport type {\n VerifyState,\n VerifyStatus,\n VerifyEvent,\n} from './verify-flow.js';\nexport {\n importFlowReducer,\n initImportState,\n} from './import-flow.js';\nexport type {\n ImportState,\n ImportStatus,\n ImportEvent,\n} from './import-flow.js';\nexport {\n IdentityContextIDBStorage,\n} from './identity-context-storage.js';\nexport type {\n IdentityContextData,\n IdentityContextStorage,\n} from './identity-context-types.js';\nexport {\n deriveCacheKey,\n encryptContext,\n decryptContext,\n} from './context-crypto.js';\nexport type {\n ContextEnvelope,\n} from './context-crypto.js';\n","// packages/react/src/headless/context-crypto.ts\nimport type { IdentityContextData } from './identity-context-types.js';\n\nfunction toB64(bytes: Uint8Array): string {\n let binary = '';\n for (const byte of bytes) {\n binary += String.fromCharCode(byte);\n }\n return btoa(binary)\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '');\n}\n\nfunction fromB64(s: string): Uint8Array {\n let base64 = s\n .replace(/-/g, '+')\n .replace(/_/g, '/');\n while (base64.length % 4 !== 0) {\n base64 += '=';\n }\n const binary = atob(base64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n}\n\nexport interface ContextEnvelope {\n v: 1;\n /** fresh HKDF salt этого логина (b64). Нужен только при\n * первичной деривации; persisted CryptoKey переживает\n * перезапуск без него. */\n salt: string;\n iv: string; // b64, 12B\n ct: string; // b64, ciphertext || GCM tag\n}\n\n/**\n * Derives a fresh, per-login cache key from login-time secret\n * material and wraps it as a NON-EXTRACTABLE persisted CryptoKey.\n * Raw key bytes exist only transiently during importKey.\n *\n * @param loginMaterial - high-entropy bytes available at login\n * (derived from the seed while it is still in memory).\n * @param salt - FRESH random bytes for this login. Never reuse a\n * salt across logins: the whole point is that a compromised\n * cacheKey from session N is useless in session N+1.\n */\nexport async function deriveCacheKey(\n loginMaterial: Uint8Array,\n salt: Uint8Array\n): Promise<CryptoKey> {\n const base = await crypto.subtle.importKey(\n 'raw', loginMaterial as BufferSource, 'HKDF', false, ['deriveBits']\n );\n const bits = await crypto.subtle.deriveBits(\n { name: 'HKDF', hash: 'SHA-256',\n salt: salt as BufferSource,\n info: new TextEncoder().encode('me2em/cache-wrap/v1') },\n base, 256\n );\n return crypto.subtle.importKey(\n 'raw', bits, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']\n );\n}\n\nexport async function encryptContext(\n key: CryptoKey, data: IdentityContextData\n): Promise<ContextEnvelope> {\n const iv = crypto.getRandomValues(new Uint8Array(12));\n const salt = crypto.getRandomValues(new Uint8Array(32));\n const pt = new TextEncoder().encode(JSON.stringify(data));\n const ct = await crypto.subtle.encrypt(\n { name: 'AES-GCM', iv: iv as BufferSource }, key, pt\n );\n return { v: 1, salt: toB64(salt), iv: toB64(iv),\n ct: toB64(new Uint8Array(ct)) };\n}\n\nexport async function decryptContext(\n key: CryptoKey, envelope: ContextEnvelope\n): Promise<IdentityContextData> {\n if (envelope.v !== 1) throw new Error('Unsupported envelope version');\n const pt = await crypto.subtle.decrypt(\n { name: 'AES-GCM', iv: fromB64(envelope.iv) as BufferSource },\n key, fromB64(envelope.ct) as BufferSource\n );\n return JSON.parse(new TextDecoder().decode(pt)) as IdentityContextData;\n}\n","// packages/react/src/headless/identity-context-storage.ts\nimport { encryptContext, decryptContext, type ContextEnvelope } from './context-crypto.js';\nimport type { IdentityContextData, IdentityContextStorage } from './identity-context-types.js';\nexport type { IdentityContextData };\n\nconst DB_NAME_PREFIX = 'me2em_';\nconst STORE_NAME = 'context';\nconst KEYS_STORE_NAME = 'cryptoKeys';\nconst CACHE_KEY = 'cache';\n\nfunction hashToHex(data: Uint8Array): string {\n const hexParts: string[] = [];\n for (const byte of data) {\n hexParts.push(byte.toString(16).padStart(2, '0'));\n }\n return hexParts.join('');\n}\n\nasync function sha256Hex(data: Uint8Array): Promise<string> {\n const buffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer;\n const digest: ArrayBuffer = await crypto.subtle.digest('SHA-256', buffer);\n return hashToHex(new Uint8Array(digest));\n}\n\nasync function dbNameForIdentity(identityId: string): Promise<string> {\n const hash = await sha256Hex(new TextEncoder().encode(identityId));\n return DB_NAME_PREFIX + hash.slice(0, 16);\n}\n\nexport class IdentityContextIDBStorage implements IdentityContextStorage {\n private db: IDBDatabase | null = null;\n private currentIdentityId: string | null = null;\n private cacheKey: CryptoKey | null = null;\n\n private async getDb(): Promise<IDBDatabase> {\n if (!this.db) {\n throw new Error('Storage not opened — call open() first');\n }\n return this.db;\n }\n\n async open(identityId: string): Promise<void> {\n if (this.currentIdentityId !== null && this.currentIdentityId !== identityId) {\n await this.close();\n }\n\n this.currentIdentityId = identityId;\n const name = await dbNameForIdentity(identityId);\n\n return new Promise<void>((resolve, reject) => {\n const request = indexedDB.open(name, 1);\n\n request.onupgradeneeded = () => {\n const db = request.result;\n if (!db.objectStoreNames.contains(STORE_NAME)) {\n db.createObjectStore(STORE_NAME, { keyPath: 'key' });\n }\n if (!db.objectStoreNames.contains(KEYS_STORE_NAME)) {\n db.createObjectStore(KEYS_STORE_NAME, { keyPath: 'key' });\n }\n };\n\n request.onsuccess = () => {\n this.db = request.result;\n resolve();\n };\n\n request.onerror = () => {\n reject(new Error('IndexedDB open failed: ' + request.error?.message));\n };\n });\n }\n\n async openAndLoad(identityId: string): Promise<IdentityContextData | null> {\n await this.open(identityId);\n const key = this.cacheKey ?? await this.loadPersistedKey();\n if (!key) {\n return null;\n }\n this.cacheKey = key;\n return this.load();\n }\n\n async save(data: IdentityContextData): Promise<void> {\n const key = this.cacheKey;\n if (!key) {\n throw new Error('No cache key set — call setCacheKey before save');\n }\n const envelope = await encryptContext(key, data);\n const db = await this.getDb();\n return new Promise<void>((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, 'readwrite');\n const store = tx.objectStore(STORE_NAME);\n store.put({ key: CACHE_KEY, envelope });\n\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(new Error('Save failed: ' + tx.error?.message));\n });\n }\n\n async load(): Promise<IdentityContextData | null> {\n const db = await this.getDb();\n const key = this.cacheKey;\n if (!key) {\n return null;\n }\n return new Promise<IdentityContextData | null>((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, 'readonly');\n const store = tx.objectStore(STORE_NAME);\n const request = store.get(CACHE_KEY);\n\n request.onsuccess = () => {\n const record = request.result;\n if (!record || !record.envelope) {\n resolve(null);\n return;\n }\n decryptContext(key, record.envelope)\n .then((data) => {\n if (data.identityId !== this.currentIdentityId) {\n resolve(null);\n return;\n }\n resolve(data);\n })\n .catch(() => {\n resolve(null);\n });\n };\n\n request.onerror = () => reject(new Error('Load failed: ' + request.error?.message));\n });\n }\n\n async clear(): Promise<void> {\n const db = await this.getDb();\n return new Promise<void>((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, 'readwrite');\n const store = tx.objectStore(STORE_NAME);\n store.delete(CACHE_KEY);\n\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(new Error('Clear failed: ' + tx.error?.message));\n });\n }\n\n async close(): Promise<void> {\n const db = this.db;\n this.db = null;\n this.currentIdentityId = null;\n this.cacheKey = null;\n if (db) {\n db.close();\n }\n }\n\n setCacheKey(key: CryptoKey): void {\n this.cacheKey = key;\n }\n\n getCacheKey(): CryptoKey | null {\n return this.cacheKey;\n }\n\n async persistCacheKey(): Promise<void> {\n const key = this.cacheKey;\n if (!key) {\n throw new Error('No cache key to persist — call setCacheKey first');\n }\n const db = await this.getDb();\n return new Promise<void>((resolve, reject) => {\n const tx = db.transaction(KEYS_STORE_NAME, 'readwrite');\n const store = tx.objectStore(KEYS_STORE_NAME);\n store.put({ key: 'cache', cryptoKey: key });\n\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(new Error('persistCacheKey failed: ' + tx.error?.message));\n });\n }\n\n async loadPersistedKey(): Promise<CryptoKey | null> {\n const db = this.db;\n if (!db) {\n return null;\n }\n return new Promise<CryptoKey | null>((resolve, reject) => {\n const tx = db.transaction(KEYS_STORE_NAME, 'readonly');\n const store = tx.objectStore(KEYS_STORE_NAME);\n const request = store.get('cache');\n\n request.onsuccess = () => {\n const record = request.result;\n if (!record || !record.cryptoKey) {\n resolve(null);\n return;\n }\n resolve(record.cryptoKey);\n };\n\n request.onerror = () => {\n resolve(null);\n };\n });\n }\n}\n"],"mappings":"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","names":["useCallback","useCallback","useReducer","useCallback","get32ByteSeedFromMnemonic","useReducer","useCallback","get32ByteSeedFromMnemonic","useState","useState","useState","useEffect","useState","useEffect","useReducer","useState","jsx","useReducer","useState","useReducer","useCallback","useState","useEffect","jsx","jsxs","useReducer","useState","useCallback","useEffect","useReducer","useEffect","useRef","validateSeedPhrase","wordlist","jsx","jsxs","useReducer","useRef","useEffect","useCallback","jsx","jsxs","useState","useCallback","useEffect","useRef","useState","useRef","useEffect","useCallback","useState","useEffect","useCallback","useRef","useState","useRef","useCallback","useEffect"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@me2em/react",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"description": "React bindings for the Me2em protocol: identity lifecycle, seed phrase UX, hierarchical handles",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"license": "Apache-2.0",
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@me2em/core": "0.7.0-alpha.1"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"react": ">=18.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@testing-library/dom": "^10.0.0",
|
|
32
|
+
"@testing-library/jest-dom": "^6.4.0",
|
|
33
|
+
"@testing-library/react": "^16.0.0",
|
|
34
|
+
"@types/react": "^18.3.0",
|
|
35
|
+
"@types/react-dom": "^18.3.0",
|
|
36
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
37
|
+
"jsdom": "^25.0.0",
|
|
38
|
+
"react": "^18.3.0",
|
|
39
|
+
"react-dom": "^18.3.0",
|
|
40
|
+
"tsup": "^8.0.0",
|
|
41
|
+
"typescript": "^5.6.0",
|
|
42
|
+
"fake-indexeddb": "^6.0.0",
|
|
43
|
+
"vitest": "^4.1.7"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsup",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"lint": "eslint src",
|
|
49
|
+
"typecheck": "tsc --noEmit"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// packages/react/src/headless/context-crypto.ts
|
|
2
|
+
import type { IdentityContextData } from './identity-context-types.js';
|
|
3
|
+
|
|
4
|
+
function toB64(bytes: Uint8Array): string {
|
|
5
|
+
let binary = '';
|
|
6
|
+
for (const byte of bytes) {
|
|
7
|
+
binary += String.fromCharCode(byte);
|
|
8
|
+
}
|
|
9
|
+
return btoa(binary)
|
|
10
|
+
.replace(/\+/g, '-')
|
|
11
|
+
.replace(/\//g, '_')
|
|
12
|
+
.replace(/=+$/, '');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function fromB64(s: string): Uint8Array {
|
|
16
|
+
let base64 = s
|
|
17
|
+
.replace(/-/g, '+')
|
|
18
|
+
.replace(/_/g, '/');
|
|
19
|
+
while (base64.length % 4 !== 0) {
|
|
20
|
+
base64 += '=';
|
|
21
|
+
}
|
|
22
|
+
const binary = atob(base64);
|
|
23
|
+
const bytes = new Uint8Array(binary.length);
|
|
24
|
+
for (let i = 0; i < binary.length; i++) {
|
|
25
|
+
bytes[i] = binary.charCodeAt(i);
|
|
26
|
+
}
|
|
27
|
+
return bytes;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ContextEnvelope {
|
|
31
|
+
v: 1;
|
|
32
|
+
/** fresh HKDF salt этого логина (b64). Нужен только при
|
|
33
|
+
* первичной деривации; persisted CryptoKey переживает
|
|
34
|
+
* перезапуск без него. */
|
|
35
|
+
salt: string;
|
|
36
|
+
iv: string; // b64, 12B
|
|
37
|
+
ct: string; // b64, ciphertext || GCM tag
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Derives a fresh, per-login cache key from login-time secret
|
|
42
|
+
* material and wraps it as a NON-EXTRACTABLE persisted CryptoKey.
|
|
43
|
+
* Raw key bytes exist only transiently during importKey.
|
|
44
|
+
*
|
|
45
|
+
* @param loginMaterial - high-entropy bytes available at login
|
|
46
|
+
* (derived from the seed while it is still in memory).
|
|
47
|
+
* @param salt - FRESH random bytes for this login. Never reuse a
|
|
48
|
+
* salt across logins: the whole point is that a compromised
|
|
49
|
+
* cacheKey from session N is useless in session N+1.
|
|
50
|
+
*/
|
|
51
|
+
export async function deriveCacheKey(
|
|
52
|
+
loginMaterial: Uint8Array,
|
|
53
|
+
salt: Uint8Array
|
|
54
|
+
): Promise<CryptoKey> {
|
|
55
|
+
const base = await crypto.subtle.importKey(
|
|
56
|
+
'raw', loginMaterial as BufferSource, 'HKDF', false, ['deriveBits']
|
|
57
|
+
);
|
|
58
|
+
const bits = await crypto.subtle.deriveBits(
|
|
59
|
+
{ name: 'HKDF', hash: 'SHA-256',
|
|
60
|
+
salt: salt as BufferSource,
|
|
61
|
+
info: new TextEncoder().encode('me2em/cache-wrap/v1') },
|
|
62
|
+
base, 256
|
|
63
|
+
);
|
|
64
|
+
return crypto.subtle.importKey(
|
|
65
|
+
'raw', bits, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function encryptContext(
|
|
70
|
+
key: CryptoKey, data: IdentityContextData
|
|
71
|
+
): Promise<ContextEnvelope> {
|
|
72
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
73
|
+
const salt = crypto.getRandomValues(new Uint8Array(32));
|
|
74
|
+
const pt = new TextEncoder().encode(JSON.stringify(data));
|
|
75
|
+
const ct = await crypto.subtle.encrypt(
|
|
76
|
+
{ name: 'AES-GCM', iv: iv as BufferSource }, key, pt
|
|
77
|
+
);
|
|
78
|
+
return { v: 1, salt: toB64(salt), iv: toB64(iv),
|
|
79
|
+
ct: toB64(new Uint8Array(ct)) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function decryptContext(
|
|
83
|
+
key: CryptoKey, envelope: ContextEnvelope
|
|
84
|
+
): Promise<IdentityContextData> {
|
|
85
|
+
if (envelope.v !== 1) throw new Error('Unsupported envelope version');
|
|
86
|
+
const pt = await crypto.subtle.decrypt(
|
|
87
|
+
{ name: 'AES-GCM', iv: fromB64(envelope.iv) as BufferSource },
|
|
88
|
+
key, fromB64(envelope.ct) as BufferSource
|
|
89
|
+
);
|
|
90
|
+
return JSON.parse(new TextDecoder().decode(pt)) as IdentityContextData;
|
|
91
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// packages/react/src/headless/context-key-store.ts
|
|
2
|
+
import type { IdentityContextData } from './identity-context-storage.js';
|
|
3
|
+
|
|
4
|
+
const KEYS_PREFIX = '__keys:';
|
|
5
|
+
|
|
6
|
+
export interface ContextKeyStore {
|
|
7
|
+
/** Save a persisted CryptoKey for later use. */
|
|
8
|
+
setCacheKey(key: CryptoKey): Promise<void>;
|
|
9
|
+
/** Retrieve the persisted CryptoKey, or null if not found. */
|
|
10
|
+
getCacheKey(): Promise<CryptoKey | null>;
|
|
11
|
+
/** Delete the persisted CryptoKey (called on logout). */
|
|
12
|
+
deleteCacheKey(): Promise<void>;
|
|
13
|
+
/** Persist the CryptoKey to IndexedDB for warm return after browser restart. */
|
|
14
|
+
persistCacheKey(): Promise<void>;
|
|
15
|
+
/** Load a previously persisted CryptoKey from IndexedDB. */
|
|
16
|
+
loadPersistedKey(): Promise<CryptoKey | null>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* IndexedDB-backed key store. CryptoKey objects are structured-cloned
|
|
21
|
+
* into an object store 'cryptoKeys' so they survive browser restarts.
|
|
22
|
+
*/
|
|
23
|
+
export class InMemoryKeyStore implements ContextKeyStore {
|
|
24
|
+
private db: IDBDatabase | null = null;
|
|
25
|
+
|
|
26
|
+
private getDbName(identityId: string): string {
|
|
27
|
+
return `${KEYS_PREFIX}${identityId}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private async openOrCreate(): Promise<IDBDatabase> {
|
|
31
|
+
if (this.db) return this.db;
|
|
32
|
+
|
|
33
|
+
const name = this.getDbName('default');
|
|
34
|
+
return new Promise<IDBDatabase>((resolve, reject) => {
|
|
35
|
+
const request = indexedDB.open(name, 1);
|
|
36
|
+
|
|
37
|
+
request.onupgradeneeded = () => {
|
|
38
|
+
const db = request.result;
|
|
39
|
+
if (!db.objectStoreNames.contains('keys')) {
|
|
40
|
+
db.createObjectStore('keys', { keyPath: 'key' });
|
|
41
|
+
}
|
|
42
|
+
if (!db.objectStoreNames.contains('cryptoKeys')) {
|
|
43
|
+
db.createObjectStore('cryptoKeys', { keyPath: 'key' });
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
request.onsuccess = () => {
|
|
48
|
+
this.db = request.result;
|
|
49
|
+
resolve(request.result);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
request.onerror = () => reject(new Error('Keys DB open failed'));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async setCacheKey(key: CryptoKey): Promise<void> {
|
|
57
|
+
this.cacheKey = key;
|
|
58
|
+
const db = await this.openOrCreate();
|
|
59
|
+
return new Promise<void>((resolve, reject) => {
|
|
60
|
+
const tx = db.transaction('cryptoKeys', 'readwrite');
|
|
61
|
+
const store = tx.objectStore('cryptoKeys');
|
|
62
|
+
store.put({ key: 'cacheKey', cryptoKey: key });
|
|
63
|
+
|
|
64
|
+
tx.oncomplete = () => resolve();
|
|
65
|
+
tx.onerror = () => reject(new Error('setCacheKey failed: ' + tx.error?.message));
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private cacheKey: CryptoKey | null = null;
|
|
70
|
+
|
|
71
|
+
async getCacheKey(): Promise<CryptoKey | null> {
|
|
72
|
+
if (this.cacheKey) return this.cacheKey;
|
|
73
|
+
return this.loadPersistedKey();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async deleteCacheKey(): Promise<void> {
|
|
77
|
+
this.cacheKey = null;
|
|
78
|
+
const db = await this.openOrCreate();
|
|
79
|
+
return new Promise<void>((resolve, reject) => {
|
|
80
|
+
const tx = db.transaction('cryptoKeys', 'readwrite');
|
|
81
|
+
const store = tx.objectStore('cryptoKeys');
|
|
82
|
+
store.delete('cacheKey');
|
|
83
|
+
|
|
84
|
+
tx.oncomplete = () => resolve();
|
|
85
|
+
tx.onerror = () => reject(new Error('deleteCacheKey failed: ' + tx.error?.message));
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async persistCacheKey(): Promise<void> {
|
|
90
|
+
const key = this.cacheKey;
|
|
91
|
+
if (!key) {
|
|
92
|
+
throw new Error('No cache key to persist');
|
|
93
|
+
}
|
|
94
|
+
const db = await this.openOrCreate();
|
|
95
|
+
return new Promise<void>((resolve, reject) => {
|
|
96
|
+
const tx = db.transaction('cryptoKeys', 'readwrite');
|
|
97
|
+
const store = tx.objectStore('cryptoKeys');
|
|
98
|
+
store.put({ key: 'cacheKey', cryptoKey: key });
|
|
99
|
+
|
|
100
|
+
tx.oncomplete = () => resolve();
|
|
101
|
+
tx.onerror = () => reject(new Error('persistCacheKey failed: ' + tx.error?.message));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async loadPersistedKey(): Promise<CryptoKey | null> {
|
|
106
|
+
const db = await this.openOrCreate();
|
|
107
|
+
return new Promise<CryptoKey | null>((resolve, reject) => {
|
|
108
|
+
const tx = db.transaction('cryptoKeys', 'readonly');
|
|
109
|
+
const store = tx.objectStore('cryptoKeys');
|
|
110
|
+
const request = store.get('cacheKey');
|
|
111
|
+
|
|
112
|
+
request.onsuccess = () => {
|
|
113
|
+
const record = request.result;
|
|
114
|
+
if (!record || !record.cryptoKey) {
|
|
115
|
+
resolve(null);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
resolve(record.cryptoKey);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
request.onerror = () => resolve(null);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// packages/react/src/headless/display-flow.ts
|
|
2
|
+
|
|
3
|
+
export type DisplayStatus =
|
|
4
|
+
| 'hidden'
|
|
5
|
+
| 'revealed'
|
|
6
|
+
| 'copied'
|
|
7
|
+
| 'dismissed';
|
|
8
|
+
|
|
9
|
+
export interface DisplayState {
|
|
10
|
+
status: DisplayStatus;
|
|
11
|
+
copyCount: number;
|
|
12
|
+
revealedAt: number | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type DisplayEvent =
|
|
16
|
+
| { type: 'REVEAL'; at: number }
|
|
17
|
+
| { type: 'COPY'; at: number }
|
|
18
|
+
| { type: 'DISMISS' }
|
|
19
|
+
| { type: 'RESET' };
|
|
20
|
+
|
|
21
|
+
export const initialDisplayState: DisplayState = {
|
|
22
|
+
status: 'hidden',
|
|
23
|
+
copyCount: 0,
|
|
24
|
+
revealedAt: null,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function displayFlowReducer(
|
|
28
|
+
state: DisplayState,
|
|
29
|
+
event: DisplayEvent
|
|
30
|
+
): DisplayState {
|
|
31
|
+
const { type } = event;
|
|
32
|
+
|
|
33
|
+
switch (state.status) {
|
|
34
|
+
case 'hidden': {
|
|
35
|
+
if (type === 'REVEAL') {
|
|
36
|
+
return {
|
|
37
|
+
status: 'revealed',
|
|
38
|
+
copyCount: 0,
|
|
39
|
+
revealedAt: event.at,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (type === 'COPY') {
|
|
43
|
+
console.warn('Cannot copy while hidden');
|
|
44
|
+
return state;
|
|
45
|
+
}
|
|
46
|
+
if (type === 'RESET') {
|
|
47
|
+
return initialDisplayState;
|
|
48
|
+
}
|
|
49
|
+
if (type === 'DISMISS') {
|
|
50
|
+
console.warn('Cannot dismiss while hidden');
|
|
51
|
+
return state;
|
|
52
|
+
}
|
|
53
|
+
console.warn(`Unexpected event ${type} in hidden`);
|
|
54
|
+
return state;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
case 'revealed': {
|
|
58
|
+
if (type === 'COPY') {
|
|
59
|
+
if (state.copyCount === 0) {
|
|
60
|
+
return {
|
|
61
|
+
status: 'copied',
|
|
62
|
+
copyCount: 1,
|
|
63
|
+
revealedAt: state.revealedAt,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
console.warn('Already copied — duplicate copy blocked');
|
|
67
|
+
return state;
|
|
68
|
+
}
|
|
69
|
+
if (type === 'DISMISS') {
|
|
70
|
+
return {
|
|
71
|
+
status: 'dismissed',
|
|
72
|
+
copyCount: state.copyCount,
|
|
73
|
+
revealedAt: state.revealedAt,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (type === 'RESET') {
|
|
77
|
+
return initialDisplayState;
|
|
78
|
+
}
|
|
79
|
+
console.warn(`Unexpected event ${type} in revealed`);
|
|
80
|
+
return state;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
case 'copied': {
|
|
84
|
+
if (type === 'COPY') {
|
|
85
|
+
console.warn('Already copied — duplicate copy blocked');
|
|
86
|
+
return state;
|
|
87
|
+
}
|
|
88
|
+
if (type === 'DISMISS') {
|
|
89
|
+
return {
|
|
90
|
+
status: 'dismissed',
|
|
91
|
+
copyCount: 1,
|
|
92
|
+
revealedAt: state.revealedAt,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (type === 'RESET') {
|
|
96
|
+
return initialDisplayState;
|
|
97
|
+
}
|
|
98
|
+
console.warn(`Unexpected event ${type} in copied`);
|
|
99
|
+
return state;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
case 'dismissed': {
|
|
103
|
+
if (type === 'RESET') {
|
|
104
|
+
return initialDisplayState;
|
|
105
|
+
}
|
|
106
|
+
console.warn(`Unexpected event ${type} in dismissed`);
|
|
107
|
+
return state;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
default:
|
|
111
|
+
return state;
|
|
112
|
+
}
|
|
113
|
+
}
|