@@ -367,13 +367,19 @@ export class AuthManager {
367367 *
368368 * better-auth defaults to `@better-auth/utils/password.node`, which calls
369369 * `node:crypto.scrypt`. WebContainer polyfills that API incompletely and
370- * signup throws `TypeError: y.run is not a function`. The pure-JS variant
371- * at `@better-auth/utils/password` uses `@noble/hashes/scrypt` with
372- * identical params (N=16384, r=16, p=1, dkLen=64) and emits the same
373- * `{salt}:{keyHex}` format, so existing hashes remain verifiable.
370+ * signup throws `TypeError: y.run is not a function`.
371+ *
372+ * We can't dynamic-import `@better-auth/utils/password` because that
373+ * package's `exports` map gates the pure-JS build behind a non-`"node"`
374+ * condition — Node-the-runtime (which WebContainer reports itself as)
375+ * always resolves to `password.node.mjs`. So we reimplement the same hash
376+ * here using `@noble/hashes/scrypt` directly, with byte-identical params
377+ * (N=16384, r=16, p=1, dkLen=64) and the same `{saltHex}:{keyHex}` storage
378+ * format. Hashes produced by either implementation verify against the
379+ * other — no migration needed.
374380 *
375381 * Returns `undefined` outside WebContainer so production deployments keep
376- * the native (fast) hasher and never load the JS fallback .
382+ * the native (fast) hasher and never load `@noble/hashes` .
377383 */
378384 private async resolvePasswordHasher ( ) : Promise <
379385 { hash : ( password : string ) => Promise < string > ; verify : ( args : { hash : string ; password : string } ) => Promise < boolean > } | undefined
@@ -385,14 +391,32 @@ export class AuthManager {
385391 Boolean ( ( globalThis as any ) . process ?. env ?. STACKBLITZ ) ) ;
386392 if ( ! isWebContainer ) return undefined ;
387393 try {
388- const mod = await import ( '@better-auth/utils/password' ) ;
394+ const { scryptAsync } = await import ( '@noble/hashes/scrypt.js' ) ;
395+ const PARAMS = { N : 16384 , r : 16 , p : 1 , dkLen : 64 , maxmem : 128 * 16384 * 16 * 2 } as const ;
396+ const toHex = ( b : Uint8Array ) : string => {
397+ let s = '' ;
398+ for ( let i = 0 ; i < b . length ; i ++ ) s += b [ i ] ! . toString ( 16 ) . padStart ( 2 , '0' ) ;
399+ return s ;
400+ } ;
401+ const generateKey = ( password : string , saltHex : string ) : Promise < Uint8Array > =>
402+ scryptAsync ( password . normalize ( 'NFKC' ) , saltHex , PARAMS ) ;
389403 return {
390- hash : ( password : string ) => mod . hashPassword ( password ) ,
391- verify : ( { hash, password } ) => mod . verifyPassword ( hash , password ) ,
404+ hash : async ( password : string ) => {
405+ const saltBytes = ( globalThis as any ) . crypto . getRandomValues ( new Uint8Array ( 16 ) ) ;
406+ const saltHex = toHex ( saltBytes ) ;
407+ const key = await generateKey ( password , saltHex ) ;
408+ return `${ saltHex } :${ toHex ( key ) } ` ;
409+ } ,
410+ verify : async ( { hash, password } ) => {
411+ const [ saltHex , keyHex ] = hash . split ( ':' ) ;
412+ if ( ! saltHex || ! keyHex ) throw new Error ( 'Invalid password hash' ) ;
413+ const target = await generateKey ( password , saltHex ) ;
414+ return toHex ( target ) === keyHex ;
415+ } ,
392416 } ;
393417 } catch ( err : any ) {
394418 console . warn (
395- `[AuthManager] WebContainer detected but pure-JS password hasher unavailable: ${ err ?. message ?? err } . Falling back to default.` ,
419+ `[AuthManager] WebContainer detected but pure-JS scrypt unavailable: ${ err ?. message ?? err } . Falling back to default.` ,
396420 ) ;
397421 return undefined ;
398422 }
0 commit comments