Files
bitvid/js/services/authProviders/nsec.js
T
thePR0M3TH3ANandClaude Opus 4.8 8692dac671 [nostr-core] Per-account nsec key storage for multi-account switching (#48)
Move the encrypted-key store from a single slot to a per-pubkey map so
several saved nsec accounts each keep their own key on the device and the
profile switcher can move between them. Previously bitvid:sessionActor:v1
held one key (last-saved wins), so switching among saved nsec accounts
silently failed.

- sessionActor.js: new bitvid:sessionActors:v2 map keyed by pubkey (follows
  the existing bitvid:<thing>:<npub> convention). readStoredSessionActorEntry,
  persistSessionActor, clearStoredSessionActor take an optional target pubkey;
  legacy v1 slot is kept in sync as the last-saved default and migrated into
  the map on first read. IndexedDB mirror keyed per-pubkey. Adds
  listStoredSessionActorPubkeys().
- client.js: getStoredSessionActorMetadata / unlockStoredSessionActor accept a
  target pubkey; access-denial cleanup forgets only the blocked account.
- nsec provider threads expectPubkey into the unlock so a switch unlocks the
  requested account, with the expectPubkey guard as a backstop.
- authSessionCoordinator: look up the target account's stored-key metadata.

NIP-46 multi-account (single slot bitvid:nip46:session:v1) remains as a
follow-up. Tests: tests/session-actor-multi-account.test.mjs (7).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 22:33:03 -04:00

171 lines
5.0 KiB
JavaScript

import { accessControl } from "../../accessControl.js";
const PROVIDER_ID = "nsec";
const PROVIDER_LABEL = "nsec or seed (direct private key)";
const PROVIDER_DESCRIPTION =
"Paste an nsec or hex private key. Keep it encrypted on this device if you choose.";
const PROVIDER_CAPABILITIES = Object.freeze([
Object.freeze({
id: "signing",
label: "Signs locally",
variant: "warning",
}),
Object.freeze({
id: "session",
label: "Encrypted backup optional",
variant: "neutral",
}),
]);
const PROVIDER_BUTTON = Object.freeze({
variant: "ghost",
});
const PROVIDER_MESSAGES = Object.freeze({
loading: "Unlocking direct key…",
slow: "Still deriving your key…",
error: "Failed to unlock the direct key. Check your secret and try again.",
});
function normalizePubkey(result) {
if (typeof result === "string") {
return result;
}
if (!result || typeof result !== "object") {
return "";
}
if (typeof result.pubkey === "string") {
return result.pubkey;
}
if (typeof result.publicKey === "string") {
return result.publicKey;
}
return "";
}
function normalizeOptions(options) {
if (!options || typeof options !== "object") {
return {
secret: "",
persist: false,
passphrase: "",
unlockStored: false,
};
}
return {
secret: typeof options.secret === "string" ? options.secret : "",
persist: options.persist === true,
passphrase: typeof options.passphrase === "string" ? options.passphrase : "",
unlockStored: options.unlockStored === true,
// Set when switching to a specific saved account, so the stored-key unlock
// targets that account rather than the last-saved default.
expectPubkey:
typeof options.expectPubkey === "string" ? options.expectPubkey.trim() : "",
};
}
export default {
id: PROVIDER_ID,
label: PROVIDER_LABEL,
description: PROVIDER_DESCRIPTION,
eyebrow: "Advanced",
tone: "warning",
badgeVariant: "warning",
capabilities: PROVIDER_CAPABILITIES,
button: PROVIDER_BUTTON,
messages: PROVIDER_MESSAGES,
errorMessage: PROVIDER_MESSAGES.error,
async login({ nostrClient, options } = {}) {
if (!nostrClient || typeof nostrClient !== "object") {
const error = new Error("Direct key login is not available.");
error.code = "provider-unavailable";
throw error;
}
if (
typeof nostrClient.derivePrivateKeyFromSecret !== "function" ||
typeof nostrClient.registerPrivateKeySigner !== "function"
) {
const error = new Error("Direct key login is not supported in this environment.");
error.code = "provider-unavailable";
throw error;
}
const normalized = normalizeOptions(options);
// Access gate enforced before any signer is activated, for both fresh
// logins and stored-key unlocks.
const validator = (pubkey) => {
if (!accessControl.canAccess(pubkey)) {
if (accessControl.isBlacklisted(pubkey)) {
throw new Error("Your account has been blocked on this platform.");
}
throw new Error("Access restricted to admins and moderators users only.");
}
return true;
};
if (normalized.unlockStored) {
if (typeof nostrClient.unlockStoredSessionActor !== "function") {
const error = new Error("Stored key unlock is not supported.");
error.code = "unlock-unavailable";
throw error;
}
const suppliedPassphrase = normalized.passphrase;
if (typeof suppliedPassphrase !== "string" || !suppliedPassphrase.trim()) {
const error = new Error("A passphrase is required to unlock the saved key.");
error.code = "passphrase-required";
throw error;
}
const unlockResult = await nostrClient.unlockStoredSessionActor(
suppliedPassphrase,
{ validator, pubkey: normalized.expectPubkey || undefined },
);
const unlockedPubkey = normalizePubkey(unlockResult);
return {
authType: PROVIDER_ID,
pubkey: unlockedPubkey,
};
}
const trimmedSecret = normalized.secret.trim();
if (!trimmedSecret) {
const error = new Error("A private key is required to continue.");
error.code = "secret-required";
throw error;
}
const { privateKey, pubkey } = await nostrClient.derivePrivateKeyFromSecret(trimmedSecret);
const shouldPersist = normalized.persist === true;
const passphrase = shouldPersist ? normalized.passphrase : "";
if (shouldPersist && (!passphrase || !passphrase.trim())) {
const error = new Error("A passphrase is required to remember this key.");
error.code = "passphrase-required";
throw error;
}
const registrationResult = await nostrClient.registerPrivateKeySigner({
privateKey,
pubkey,
persist: shouldPersist,
passphrase: shouldPersist ? passphrase : undefined,
validator,
});
const registeredPubkey = normalizePubkey(registrationResult) || pubkey;
return {
authType: PROVIDER_ID,
pubkey: registeredPubkey,
};
},
};