- Define missing dict_factory (orphaned fragment left app unable to
return DB rows; every endpoint crashed on first query)
- Skip backup when PINVAULT_NAS_BACKUP_DIR is empty (os.makedirs('')
raised in bootstrap and killed gunicorn at import; compose default)
- Generate PINs with secrets.randbelow instead of random.randint
- Per-row random HMAC salts for PINs and recovery codes (single shared
hardcoded salt defeated the precomputation protection); constant-time
comparisons; legacy rows keep the historic salt
- Atomic backup restore (temp file + os.replace) to avoid torn DB
- Validate lock_days type, guard backup loop, move re import to module
level
- Replace weak unittest stubs with 34-test pytest suite (sqlcipher
shimmed with sqlite3 so tests run without the native lib)
- Add README, LICENSE, pyproject, .env.example; ruff clean
Require Bearer token on all API endpoints (PINVAULT_API_KEY env).
Rate limit PIN access to 5 attempts per 15min lockout per PIN.
Make NAS_BACKUP_DIR configurable via PINVAULT_NAS_BACKUP_DIR.
Replace bootstrap() sys.exit(1) with graceful False return.
Add tests for rate limiting and auth.