mirror of
https://github.com/PR0M3TH3AN/bitlogin.git
synced 2026-11-05 21:11:21 +00:00
Reading "who is signed in" on a page required mounting the whole widget
(bitlogin.js + crypto worker + wasm, ~1.6MB) because that was the only
thing that restored the per-origin session. Multi-page sites paid the
full load on every page just to render a "Signed in as …" header.
Write a cleartext, public-only record ({ pubkey, npub, name? } — never
key material) beside the encrypted session in IndexedDB, in lockstep with
it: the worker writes it at the single persistSession() choke point,
self-heals it on restoreSession (so sessions cached before this feature
upgrade with no re-login), and clears it on logout, so a reported session
can never outlive the real one.
Add @bitlogin/widget/session -> bitlogin-session.js: a self-contained
~1KB gzip reader (zero imports, no worker/wasm) exposing
readCachedSession(): Promise<{ pubkey, npub, name? } | null>. Mirror the
same call on window.bitlogin.readCachedSession() for pages that already
load the full widget.
The private key stays in the AES-GCM record in sessionCache.ts, exactly
as protected as before. Reader falls through to null for every failure
mode (DB/store absent, IndexedDB blocked), which reads as "not signed in".
Addresses the top item of the Tessera integration feedback (docs/TODO.md).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BXQCvJPA6sVSQ5j8uBd9bB
81 lines
3.0 KiB
TypeScript
81 lines
3.0 KiB
TypeScript
/**
|
|
* Cleartext, public-only session identity — the companion to the AES-GCM
|
|
* secret session in `worker/sessionCache.ts`.
|
|
*
|
|
* It holds ONLY data that is already public: the account's Nostr public key
|
|
* (x-only hex), its npub, and an optional display name. None of it is secret —
|
|
* the same pubkey appears on every event the user signs and on every relay —
|
|
* so it is stored unencrypted, on purpose. That lets a read-only page answer
|
|
* "who, if anyone, is signed in?" WITHOUT loading the crypto worker, wasm, or
|
|
* the full widget (see `session.ts` for the lightweight reader).
|
|
*
|
|
* The everyday private key is NOT here and never will be: it stays in the
|
|
* encrypted record in `sessionCache.ts`, exactly as protected as before. This
|
|
* record is written and cleared in lockstep with that one (crypto worker), so
|
|
* "signed in" here can never outlive the real session.
|
|
*/
|
|
import type { KeyValueStore } from "@bitlogin/core/storage";
|
|
|
|
/** IndexedDB key for the cleartext public-session record (sibling of
|
|
* `bitlogin:session:v1`, which stays encrypted). */
|
|
export const PUBLIC_SESSION_KEY = "bitlogin:session-public:v1";
|
|
|
|
export interface PublicSessionInfo {
|
|
/** The account's Nostr public key, x-only, lowercase hex. */
|
|
pubkey: string;
|
|
/** NIP-19 npub encoding of the same key. */
|
|
npub: string;
|
|
/** Display name, when the widget knows one. Absent otherwise. */
|
|
name?: string;
|
|
}
|
|
|
|
/** Validates and narrows a raw stored string. Returns null for anything that
|
|
* isn't a well-formed record, so a corrupt entry reads as "no session," never
|
|
* an error. Shared by the store reader and the standalone reader so both agree
|
|
* on the shape. */
|
|
export function parsePublicSession(raw: string | null | undefined): PublicSessionInfo | null {
|
|
if (!raw) return null;
|
|
try {
|
|
const parsed = JSON.parse(raw) as Partial<PublicSessionInfo>;
|
|
if (typeof parsed.pubkey !== "string" || typeof parsed.npub !== "string") return null;
|
|
return {
|
|
pubkey: parsed.pubkey,
|
|
npub: parsed.npub,
|
|
...(typeof parsed.name === "string" && parsed.name ? { name: parsed.name } : {})
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Best-effort write, mirroring `saveCachedSession`: a full disk or a browser
|
|
* blocking IndexedDB must never break the login/rotate call that triggered it. */
|
|
export async function savePublicSession(store: KeyValueStore, info: PublicSessionInfo): Promise<void> {
|
|
try {
|
|
const record: PublicSessionInfo = {
|
|
pubkey: info.pubkey,
|
|
npub: info.npub,
|
|
...(info.name ? { name: info.name } : {})
|
|
};
|
|
await store.set(PUBLIC_SESSION_KEY, JSON.stringify(record));
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
export async function loadPublicSession(store: KeyValueStore): Promise<PublicSessionInfo | null> {
|
|
try {
|
|
return parsePublicSession(await store.get(PUBLIC_SESSION_KEY));
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function clearPublicSession(store: KeyValueStore): Promise<void> {
|
|
try {
|
|
await store.delete(PUBLIC_SESSION_KEY);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|