Files

69 lines
1.7 KiB
Go

// Package zxcvbnjs wraps @zxcvbn-ts/core via goja (ExecJS-style) for exact
// parity between frontend and backend password strength scoring.
//
// The bundled JS contains the same @zxcvbn-ts/core library used in the
// SvelteKit frontend, so scores are identical for any given password.
package zxcvbnjs
import (
"embed"
"fmt"
"log"
"sync"
"github.com/dop251/goja"
)
//go:embed bundle.js
var bundleFS embed.FS
var (
vm *goja.Runtime
vmOnce sync.Once
vmErr error
)
func getVM() (*goja.Runtime, error) {
vmOnce.Do(func() {
vm = goja.New()
bundleData, err := bundleFS.ReadFile("bundle.js")
if err != nil {
vmErr = fmt.Errorf("failed to read zxcvbn bundle: %w", err)
return
}
_, err = vm.RunScript("zxcvbn-bundle.js", string(bundleData))
if err != nil {
vmErr = fmt.Errorf("failed to evaluate zxcvbn bundle: %w", err)
return
}
})
return vm, vmErr
}
// Score returns the zxcvbn-ts score (0-4) for the given password.
// It uses the exact same @zxcvbn-ts/core library as the frontend,
// guaranteeing identical results for the same input.
func Score(password string) (int, error) {
runtime, err := getVM()
if err != nil {
return 0, fmt.Errorf("zxcvbnjs init error: %w", err)
}
zxcvbnScore, ok := goja.AssertFunction(runtime.Get("zxcvbnScore"))
if !ok {
return 0, fmt.Errorf("zxcvbnScore not found in JS runtime")
}
result, err := zxcvbnScore(goja.Undefined(), runtime.ToValue(password))
if err != nil {
log.Printf("zxcvbnjs: eval error: %v", err)
return 0, fmt.Errorf("zxcvbnScore eval failed: %w", err)
}
score := int(result.ToInteger())
if score < 0 || score > 4 {
return 0, fmt.Errorf("unexpected zxcvbn score: %d", score)
}
return score, nil
}