mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-29 17:02:28 +00:00
A persisted nsec session restores the pubkey + UI on reload but NOT the in-memory signer (the private key is only passphrase-encrypted — TODO #36). So after a refresh, adding/editing a hashtag on an nsec account dead-ended with "Connect a Nostr signer that supports encryption before managing hashtag preferences" — even though the user looks logged in. Same class blocks subscriptions/blocks/DMs/reactions. New reusable gate app.ensureEncryptionCapableSigner({ pubkey, need, promptMessage }): checks the active signer's capabilities; if it can't encrypt/sign AND a MATCHING locked nsec key exists for the account, prompts once for the passphrase and re-unlocks the signer app-wide via unlockStoredSessionActor. One unlock re-enables every signing flow until the next reload. Never unlocks a different account's key (pubkey-mismatch guard); "no-stored-key" leaves non-nsec cases (e.g. an unresponsive NIP-07 extension) to the caller's own error. Branching lives in js/nostr/ensureSignerDecision.js (pure, unit-testable without the app shell). Wired hashtag add/edit through it (ProfileHashtagController.persistHashtagPreferences): a cancelled prompt aborts silently, a bad passphrase shows its own toast, and publish never runs until the signer is confirmed. Exposed to the modal as services.ensureEncryptionCapableSigner. This is the shared machinery for the whole reloaded-nsec cluster (#51/#54/#56/#57); subscriptions/blocks/DM-send/reactions are mechanical fast-follows against the same gate (tracked in TODO #57). Tests: tests/ensure-signer-decision.test.mjs (9 — capability + recoverable/fail branching, never-wrong-key), + 2 wiring tests (gate blocks publish until ok) in tests/profile-modal-controller.test.mjs. Lint + build green. Co-Authored-By: Claude Opus 4.8 <[email protected]>
57 lines
1.9 KiB
JavaScript
57 lines
1.9 KiB
JavaScript
// Pure decision for `app.ensureEncryptionCapableSigner`. Given the active signer's
|
|
// current capabilities and whether a locked (passphrase-encrypted) nsec key exists
|
|
// for the active account, decide whether to proceed, fail, or prompt the user to
|
|
// re-unlock their key. Kept side-effect-free so the branching is unit-testable
|
|
// without the browser app shell (TODO #51/#54/#56/#57).
|
|
//
|
|
// Returns one of:
|
|
// { action: "ok" } — signer already capable
|
|
// { action: "prompt" } — recoverable: prompt + unlock nsec
|
|
// { action: "fail", reason: <string> } — not recoverable here
|
|
|
|
const defaultNormalize = (value) =>
|
|
typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
|
|
export function isSignerCapable(capabilities, need = "encrypt") {
|
|
const caps =
|
|
capabilities && typeof capabilities === "object" ? capabilities : {};
|
|
if (!caps.sign) {
|
|
return false;
|
|
}
|
|
return need === "sign" ? true : Boolean(caps.nip44) || Boolean(caps.nip04);
|
|
}
|
|
|
|
export function decideSignerEnsure({
|
|
capabilities,
|
|
need = "encrypt",
|
|
normalizedPubkey,
|
|
storedKeyMeta,
|
|
normalizePubkey = defaultNormalize,
|
|
} = {}) {
|
|
if (isSignerCapable(capabilities, need)) {
|
|
return { action: "ok" };
|
|
}
|
|
|
|
if (!normalizedPubkey) {
|
|
return { action: "fail", reason: "no-signer" };
|
|
}
|
|
|
|
// Only an nsec-after-reload situation is recoverable here: a saved, locked key
|
|
// for this exact account we can decrypt with the passphrase. Anything else
|
|
// (e.g. an unresponsive NIP-07 extension) is the caller's own error to surface.
|
|
if (!storedKeyMeta || storedKeyMeta.hasEncryptedKey !== true) {
|
|
return { action: "fail", reason: "no-stored-key" };
|
|
}
|
|
|
|
if (
|
|
storedKeyMeta.pubkey &&
|
|
normalizePubkey(storedKeyMeta.pubkey) !== normalizedPubkey
|
|
) {
|
|
return { action: "fail", reason: "pubkey-mismatch" };
|
|
}
|
|
|
|
return { action: "prompt" };
|
|
}
|
|
|
|
export default decideSignerEnsure;
|