mirror of
https://github.com/PR0M3TH3AN/bitlogin.git
synced 2026-11-06 05:21:31 +00:00
Tier 1 hardening: no more silent hangs, no writes before consent
Reliability, which is where most of these reach users: - The worker RPC had no message validation, no error/messageerror listener, no timeout, and terminate() left every pending promise unsettled. A malformed frame threw inside the listener (posting no response at all) and a dead worker stranded its callers -- the documented innerHTML-remount hazard manifested as a permanent freeze rather than an error. Both sides now validate the envelope, calls time out at 60s, and worker death rejects everything outstanding. - requestNwcConnection fired no pending event, so a host using the RECOMMENDED hidden-mount pattern navigated a hidden element to a consent screen nobody could see and waited forever. Now symmetric with bitlogin-offer-pending. Consent integrity: - vaultOfferCheck published an origin rebinding to relays -- a durable mutation of the user's account -- from a plain host API call before any consent screen existed. It is read-only now, and a wallet stored under a DIFFERENT origin no longer reports as already-saved, so the binding can only change through the consented path. - offerNwcConnection's guard sat above three awaits; two concurrent calls both passed it and the second dropped the first's promise. The slot is claimed synchronously. Vault reads fail closed: - listConnections threw rather than returning a relay-shaped subset when quorum is not met -- a short listing is indistinguishable from "you have no connections", and one relay should not get to say that. - The 500-record page limit is reported (truncated) instead of silently deciding which connections exist. Supply chain: - All eight crypto dependencies pinned to exact versions (they held users' keys behind caret ranges), and Bitcoin Connect with them. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1
This commit is contained in:
1 parent
b414cc53d2
commit
d040c2f5af
7 files changed
+141
-36
No files matched your search
@@ -123,6 +123,8 @@ export class BitLoginAuthElement extends HTMLElement {
|
||||
private vaultConnections: VaultConnectionSummary[] | null = null;
|
||||
/** offerNwcConnection state: the app already holds this URI; the only
|
||||
* question on screen is whether a copy enters the user's vault. */
|
||||
/** Claimed synchronously by offerNwcConnection before any await (see there). */
|
||||
private offerInFlight = false;
|
||||
private vaultOffer: {
|
||||
uri: string;
|
||||
appName: string;
|
||||
@@ -239,6 +241,10 @@ export class BitLoginAuthElement extends HTMLElement {
|
||||
const appName = options.appName?.trim() || window.location.hostname || "This app";
|
||||
return new Promise<string | null>((resolve) => {
|
||||
this.vaultRequest = { appName, reason: options.reason, origin, stage: "auth", resolve };
|
||||
// Symmetric with bitlogin-offer-pending. Without it, a host following
|
||||
// the recommended hidden-mount pattern navigated a HIDDEN element to a
|
||||
// consent screen nobody could see, and the promise never settled.
|
||||
this.dispatchEvent(new CustomEvent("bitlogin-request-pending"));
|
||||
void (async () => {
|
||||
try {
|
||||
const status = await this.worker.getSessionStatus();
|
||||
@@ -272,12 +278,26 @@ export class BitLoginAuthElement extends HTMLElement {
|
||||
uri: string,
|
||||
options: { appName?: string; label?: string } = {}
|
||||
): Promise<"saved" | "declined" | "already-saved" | "unavailable"> {
|
||||
if (this.vaultOffer || this.vaultRequest) return "unavailable";
|
||||
// The slot is claimed SYNCHRONOUSLY. The guard used to sit above three
|
||||
// awaits, so two concurrent calls both passed it and the second
|
||||
// overwrote the first's resolve -- dropping a promise that never settled.
|
||||
if (this.vaultOffer || this.vaultRequest || this.offerInFlight) return "unavailable";
|
||||
this.offerInFlight = true;
|
||||
try {
|
||||
return await this.runOfferNwcConnection(uri, options);
|
||||
} finally {
|
||||
this.offerInFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async runOfferNwcConnection(
|
||||
uri: string,
|
||||
options: { appName?: string; label?: string }
|
||||
): Promise<"saved" | "declined" | "already-saved" | "unavailable"> {
|
||||
const status = await this.worker.getSessionStatus().catch(() => ({ unlocked: false }));
|
||||
if (!status.unlocked) return "unavailable";
|
||||
const vaultStatus = await this.worker.vaultStatus();
|
||||
if (!vaultStatus.enabled) return "unavailable";
|
||||
const origin = window.location.origin;
|
||||
const check = await this.worker.vaultOfferCheck({ uri });
|
||||
if (check.duplicate) return "already-saved";
|
||||
|
||||
|
||||
Reference in new issue
Block a user