mirror of
https://github.com/PR0M3TH3AN/bitlogin.git
synced 2026-11-03 03:51:22 +00:00
window.bitlogin.activeMethod()/activeSession(): session state for every method
Hosts had no method-agnostic session check: isActiveSigner() answers who backs window.nostr, which is deliberately false during a NIP-07 session (the extension IS the signer and rightly owns the slot) even though the user signed in through BitLogin. Sites using it as a login check mis-read extension sessions. A page-level registry (globalSession.ts, public data only -- method, pubkey, npub) is written at the single dispatchLogin point and cleared on logout and DOM removal, owner-scoped so one element's teardown can't clear another's session. window.bitlogin gains activeMethod() and activeSession(); the integration guide and llms.txt now spell out the two-questions distinction and lead with the one-login-surface pattern. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
1 parent
14d993d11a
commit
a7cf55ea84
6 files changed
+171
-27
No files matched your search
@@ -19,6 +19,7 @@ import { detectForeignNip07Provider, Nip07Signer } from "./signers/nip07.js";
|
||||
import { Nip46Signer } from "./signers/nip46.js";
|
||||
import type { Signer, SignerCapabilities, SignerMethod } from "./signers/types.js";
|
||||
import { renderQrSvg } from "./qr.js";
|
||||
import { setActiveSession, clearActiveSession } from "./globalSession.js";
|
||||
|
||||
type Screen =
|
||||
| "welcome"
|
||||
@@ -215,6 +216,7 @@ export class BitLoginAuthElement extends HTMLElement {
|
||||
|
||||
disconnectedCallback(): void {
|
||||
if (this.successDismissTimer !== null) clearTimeout(this.successDismissTimer);
|
||||
clearActiveSession(this);
|
||||
this.releaseSigner();
|
||||
this.worker.terminate();
|
||||
}
|
||||
@@ -269,6 +271,7 @@ export class BitLoginAuthElement extends HTMLElement {
|
||||
if (this.altSigner.method === "nip46") void this.worker.nip46Disconnect();
|
||||
this.altSigner = null;
|
||||
this.session = null;
|
||||
clearActiveSession(this);
|
||||
this.releaseSigner();
|
||||
this.dispatchEvent(new CustomEvent("bitlogin-logout"));
|
||||
this.goto("welcome");
|
||||
@@ -276,6 +279,7 @@ export class BitLoginAuthElement extends HTMLElement {
|
||||
}
|
||||
await this.worker.logout();
|
||||
this.session = null;
|
||||
clearActiveSession(this);
|
||||
this.releaseSigner();
|
||||
this.dispatchEvent(new CustomEvent("bitlogin-logout"));
|
||||
this.goto("welcome");
|
||||
@@ -591,6 +595,15 @@ export class BitLoginAuthElement extends HTMLElement {
|
||||
nip04: true,
|
||||
getRelays: true
|
||||
};
|
||||
if (this.session) {
|
||||
// Keep window.bitlogin.activeMethod()/activeSession() in step with the
|
||||
// bitlogin-login event -- hosts may use either.
|
||||
setActiveSession(this, {
|
||||
method: this.session.method,
|
||||
publicKey: this.session.publicKey,
|
||||
npub: this.session.npub
|
||||
});
|
||||
}
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("bitlogin-login", {
|
||||
detail: {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { clearActiveSession, getActiveSession, setActiveSession } from "./globalSession.js";
|
||||
|
||||
const INFO = { method: "nip07" as const, publicKey: "ab".repeat(32), npub: "npub1example" };
|
||||
|
||||
describe("global session registry", () => {
|
||||
beforeEach(() => {
|
||||
// Reset by clearing with whatever owner is current -- the module keeps a
|
||||
// single slot, so clearing under a fresh owner after set covers it.
|
||||
setActiveSession(globalThis, INFO);
|
||||
clearActiveSession(globalThis);
|
||||
});
|
||||
|
||||
it("returns null when nobody is signed in", () => {
|
||||
expect(getActiveSession()).toBeNull();
|
||||
});
|
||||
|
||||
it("records and returns the session, as a copy", () => {
|
||||
const owner = {};
|
||||
setActiveSession(owner, INFO);
|
||||
const first = getActiveSession()!;
|
||||
expect(first).toEqual(INFO);
|
||||
first.publicKey = "tampered";
|
||||
expect(getActiveSession()!.publicKey).toBe(INFO.publicKey);
|
||||
});
|
||||
|
||||
it("only the owning element can clear its session", () => {
|
||||
const elementA = {};
|
||||
const elementB = {};
|
||||
setActiveSession(elementA, INFO);
|
||||
clearActiveSession(elementB); // a different element tearing down
|
||||
expect(getActiveSession()).toEqual(INFO);
|
||||
clearActiveSession(elementA);
|
||||
expect(getActiveSession()).toBeNull();
|
||||
});
|
||||
|
||||
it("the most recent sign-in wins across elements", () => {
|
||||
const elementA = {};
|
||||
const elementB = {};
|
||||
setActiveSession(elementA, INFO);
|
||||
setActiveSession(elementB, { ...INFO, method: "nip46" });
|
||||
expect(getActiveSession()!.method).toBe("nip46");
|
||||
// A's later logout must not clear B's session.
|
||||
clearActiveSession(elementA);
|
||||
expect(getActiveSession()!.method).toBe("nip46");
|
||||
});
|
||||
});
|
||||
|
||||
describe("element wiring", () => {
|
||||
const widgetSource = readFileSync(new URL("./element.ts", import.meta.url), "utf8");
|
||||
|
||||
it("every login dispatch records the session; every teardown clears it", () => {
|
||||
// dispatchLogin is the single sign-in dispatch point, so one setActiveSession
|
||||
// there covers password, extension, and remote-signer sessions alike.
|
||||
const dispatchBody = widgetSource.slice(
|
||||
widgetSource.indexOf("private dispatchLogin"),
|
||||
widgetSource.indexOf("private noteSignerClaim"),
|
||||
);
|
||||
expect(dispatchBody).toContain("setActiveSession(this");
|
||||
// Both logout branches and DOM removal clear it.
|
||||
const clearCount = widgetSource.split("clearActiveSession(this)").length - 1;
|
||||
expect(clearCount).toBe(3);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
/** Page-level record of the widget session, backing window.bitlogin.activeMethod()
|
||||
* and activeSession() (docs/login-methods.md §LM3).
|
||||
*
|
||||
* window.nostr ownership (isActiveSigner) and session state are DIFFERENT
|
||||
* questions with different answers: during a NIP-07 session the extension owns
|
||||
* the slot -- correctly -- yet the user very much signed in through BitLogin.
|
||||
* Hosts that used isActiveSigner() as a session check got `false` there. This
|
||||
* registry answers the session question directly, for every method.
|
||||
*
|
||||
* Public data only (method, public key, npub); never key material. Owner-scoped
|
||||
* so one element's teardown cannot clear a session another element established.
|
||||
*/
|
||||
import type { SignerMethod } from "./signers/types.js";
|
||||
|
||||
export interface ActiveSessionInfo {
|
||||
method: SignerMethod;
|
||||
publicKey: string;
|
||||
npub: string;
|
||||
}
|
||||
|
||||
let current: { owner: object; info: ActiveSessionInfo } | null = null;
|
||||
|
||||
export function setActiveSession(owner: object, info: ActiveSessionInfo): void {
|
||||
current = { owner, info };
|
||||
}
|
||||
|
||||
export function clearActiveSession(owner: object): void {
|
||||
if (current?.owner === owner) current = null;
|
||||
}
|
||||
|
||||
export function getActiveSession(): ActiveSessionInfo | null {
|
||||
return current ? { ...current.info } : null;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
/** Entry point: any static site includes this file and drops in <bitlogin-auth>. */
|
||||
import { BitLoginAuthElement } from "./element.js";
|
||||
import { getActiveSession, type ActiveSessionInfo } from "./globalSession.js";
|
||||
import type { SignerMethod } from "./signers/types.js";
|
||||
|
||||
if (!customElements.get("bitlogin-auth")) {
|
||||
customElements.define("bitlogin-auth", BitLoginAuthElement);
|
||||
@@ -24,6 +26,19 @@ export interface BitLoginGlobal {
|
||||
* holds several widget instances. Returns whether it actually released anything.
|
||||
*/
|
||||
releaseSigner(): boolean;
|
||||
/**
|
||||
* How the current widget session was established -- 'bitlogin' (password
|
||||
* account), 'nip07' (extension), 'nip46' (remote signer) -- or null when
|
||||
* nobody is signed in. THE session check for hosts. Deliberately distinct
|
||||
* from isActiveSigner(): during a NIP-07 session the extension rightly owns
|
||||
* window.nostr, so isActiveSigner() is false while activeMethod() is
|
||||
* 'nip07'. With several <bitlogin-auth> elements, reflects the most recent
|
||||
* sign-in.
|
||||
*/
|
||||
activeMethod(): SignerMethod | null;
|
||||
/** The current session's public identity ({ method, publicKey, npub }), or
|
||||
* null. Public data only -- never key material. */
|
||||
activeSession(): ActiveSessionInfo | null;
|
||||
}
|
||||
|
||||
declare global {
|
||||
@@ -56,6 +71,12 @@ window.bitlogin = {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
activeMethod(): SignerMethod | null {
|
||||
return getActiveSession()?.method ?? null;
|
||||
},
|
||||
activeSession(): ActiveSessionInfo | null {
|
||||
return getActiveSession();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user