All files / providers CryptoProvider.tsx

92% Statements 23/25
43.75% Branches 7/16
83.33% Functions 5/6
90.47% Lines 19/21

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130                                                3x                               6x 6x 6x           3x   3x 3x   3x   3x                                               3x               6x       6x     3x                                   4x 4x   4x         4x     4x       4x    
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
 
/**
 * Interface for the crypto module functions exposed by WASM
 */
export interface CryptoModule {
  /** Derive a Key ID from a public key: base64url(SHA-256(pubkey)[0:16]) */
  derive_kid: (publicKey: Uint8Array) => string;
  /** Encode bytes as base64url (RFC 4648) without padding */
  encode_base64url: (bytes: Uint8Array) => string;
  /** Decode a base64url string to bytes */
  decode_base64url: (encoded: string) => Uint8Array;
}
 
interface CryptoContextValue {
  /** The loaded crypto module, or null if still loading */
  crypto: CryptoModule | null;
  /** Whether the WASM module is currently loading */
  isLoading: boolean;
  /** Error if WASM failed to load */
  error: Error | null;
}
 
const CryptoContext = createContext<CryptoContextValue>({
  crypto: null,
  isLoading: true,
  error: null,
});
 
interface CryptoProviderProps {
  children: ReactNode;
}
 
/**
 * Provider that loads the tc-crypto WASM module and makes it available
 * to child components via the useCrypto hook.
 *
 * The WASM module is loaded asynchronously on mount. Children will not
 * render until the module is loaded (renders null during loading).
 */
export function CryptoProvider({ children }: CryptoProviderProps) {
  const [state, setState] = useState<CryptoContextValue>({
    crypto: null,
    isLoading: true,
    error: null,
  });
 
  useEffect(() => {
    let mounted = true;
 
    async function loadWasm() {
      try {
        // Dynamic import for code splitting - WASM loads separately from main bundle
        const wasm = await import('@/wasm/tc-crypto/tc_crypto.js');
        // Initialize the WASM module (required before using exported functions)
        await wasm.default();
 
        if (mounted) {
          setState({
            crypto: {
              derive_kid: wasm.derive_kid,
              encode_base64url: wasm.encode_base64url,
              decode_base64url: wasm.decode_base64url,
            },
            isLoading: false,
            error: null,
          });
        }
      } catch (err) {
        if (mounted) {
          setState({
            crypto: null,
            isLoading: false,
            error: err instanceof Error ? err : new Error('Failed to load crypto WASM module'),
          });
        }
      }
    }
 
    void loadWasm();
    return () => {
      mounted = false;
    };
  }, []);
 
  // Don't render children until WASM is loaded
  // This ensures crypto is always available when useCryptoRequired is called
  if (state.isLoading) {
    return null;
  }
 
  if (state.error) {
    // Let ErrorBoundary handle it or show inline error
    throw state.error;
  }
 
  return <CryptoContext.Provider value={state}>{children}</CryptoContext.Provider>;
}
 
/**
 * Hook to access the crypto module state.
 * Returns { crypto, isLoading, error } - check isLoading/error before using crypto.
 */
export function useCrypto(): CryptoContextValue {
  return useContext(CryptoContext);
}
 
/**
 * Hook to access the crypto module directly.
 * Throws if the crypto module is not loaded yet.
 * Only use this inside components that are descendants of CryptoProvider.
 */
export function useCryptoRequired(): CryptoModule {
  const { crypto, isLoading, error } = useContext(CryptoContext);
 
  if (isLoading) {
    throw new Error(
      'Crypto module is still loading. Ensure this component is inside CryptoProvider.'
    );
  }
  if (error) {
    throw error;
  }
  if (!crypto) {
    throw new Error('Crypto module not available. Ensure this component is inside CryptoProvider.');
  }
 
  return crypto;
}