⚠️ Breaking Changes
This release changes how credential store errors are reported. Success and "not found" results are unchanged, but provider failures that were previously silenced now throw (sync API) or reject (async API).
1. Reads: getPassword() / getSecret()
Previously, any failure reading the credential store — a locked keychain, denied access, an OS error — was swallowed and returned as null / undefined, indistinguishable from "no credential stored".
Now only a genuinely missing credential (NoEntry) returns the absent result. Every other error throws or rejects:
| Scenario | v1.x | v2.0.0 |
|---|---|---|
| Credential exists | value | value (unchanged) |
| Credential missing | null / undefined |
null / undefined (unchanged) |
| Store locked / inaccessible / OS error | null / undefined |
throws / rejects |
2. Deletes: deleteCredential() / deletePassword()
Previously, any delete failure returned false, so a failed delete looked identical to "credential was already gone" — leaving callers unable to tell whether the secret was actually removed.
Now false only means the credential did not exist (NoEntry). A failed delete throws or rejects:
| Scenario | v1.x | v2.0.0 |
|---|---|---|
| Credential deleted | true |
true (unchanged) |
| No credential to delete | false |
false (unchanged) |
| Delete failed (locked store, OS error) | false |
throws / rejects |
A false result now guarantees the credential is absent from the store.
3. TypeScript: async deletePassword() return type
The async deletePassword() alias is now correctly declared as Promise<boolean> instead of Promise<unknown>. This is a narrowing and is source-compatible for typical usage, but code that treated the result as unknown may need a small type adjustment.
These changes apply to both Entry (sync) and AsyncEntry (async), including the deletePassword() aliases.
🔧 Migration Guide
If you only check for absent credentials — no change needed
// Still works exactly as before: null means "not stored"
const password = await entry.getPassword()
if (password === null) {
// no credential stored
}If you call reads/deletes without error handling — add it
Code that previously "worked" against a locked or inaccessible store will now surface the error. This is the main thing to audit when upgrading:
// v1.x: silently got null on a locked keychain
// v2.0.0: must handle the rejection
try {
const secret = await entry.getSecret()
if (secret === null) {
// credential not stored
}
} catch (err) {
// store is locked, access denied, or OS-level failure
// previously this path returned null — decide how to handle it
}// Deletes: distinguish "was absent" from "failed to delete"
try {
const deleted = await entry.deleteCredential()
// deleted === true → credential was removed
// deleted === false → credential did not exist (guaranteed absent)
} catch (err) {
// deletion failed — the credential may still be stored
}Sync API
The same applies to the sync Entry methods, which now throw on provider errors instead of returning null / false:
try {
const password = entry.getPassword() // null only if not stored
} catch (err) {
// store error
}Why this change
The new behavior matches the documented node-keytar compatibility contract: missing credentials resolve as null, while native failures reject. Previously .ok() / .is_ok() conversions at the N-API boundary erased that distinction, so a locked keychain looked like an empty one. See #136 and #138 for details.
What's Changed
- fix: preserve non-missing password read errors by @skvark in #136
- fix: propagate credential store errors instead of erasing them into false/absent by @acoliver in #138
- chore: dependency and CI updates in #123–#140 (Yarn 4.18, TypeScript 7, chalk 6, ava 8, npm-run-all2 9, lint-staged 17, GitHub Actions bumps)
Full Changelog: v1.3.0...v2.0.0