mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
The login modal now shows <bitlogin-auth> -- a portable Nostr identity behind a login name and password -- as the default sign-in path, styled with bitvid's own brand tokens (accent-strong, surfaces, radii). The existing NIP-07 extension, nsec, and NIP-46 remote-signer options still work exactly as before, just collapsed under a new "Other login options" fold instead of a flat "Available methods" grid; nip07 no longer carries the stale "Recommended" eyebrow now that BitLogin has that role. BitLogin isn't a single button-click-then-await-a-promise flow like the other providers -- it's a whole multi-screen sign-in/create/recover UI -- so it gets a permanent, statically-declared widget in components/login-modal.html (js/ui/bitloginModalIntegration.js relocates it, never destroys and recreates it, since removal tears down its crypto Web Worker) instead of a grid button. It still goes through the exact same session-establishment path as every other signer: a new "bitlogin" provider (js/services/authProviders/bitlogin.js) lets AuthService resolve and activate it via the normal requestLogin -> handleAuthLogin path, while providersForModal excludes it from the grid so it isn't rendered twice. Two things this needed beyond the wiring itself: - js/app.js is one line from its lint/file-size ceiling, so the only change there is renaming an import binding (providers -> providersForModal), a net-zero line change. The actual widget/event wiring lives in a new, unconstrained module instead. - The login modal's HTML (including the permanent widget element) loads asynchronously after bootstrap runs, so wireBitloginLogin() waits via MutationObserver rather than assuming a specific call order. Vendored via scripts/build-bitlogin-widget.mjs (mirrors the existing Bitcoin Connect/Blossom vendor steps): BitLogin isn't published to npm, so the widget build is copied verbatim from the sibling ../bitlogin checkout into committed vendor/bitlogin/, then lazy-imported (like Bitcoin Connect) so its ~350KB never weighs on visitors who never open the login modal. Gated behind FEATURE_BITLOGIN (config/instance-config.js), default on for this unstable-branch experiment per AGENTS.md §1 -- reset to false before this reaches main until verified end-to-end against production relays. Verified: npm run lint (clean), the full auth/login test suite (43/43, including new coverage for the provider + adapter), and a manual Chrome pass against the built dist/ -- widget renders themed correctly, "Other login options" still shows the original four methods unchanged.
147 lines
4.8 KiB
JavaScript
147 lines
4.8 KiB
JavaScript
import "./test-helpers/setup-localstorage.mjs";
|
|
import assert from "node:assert/strict";
|
|
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
|
|
import {
|
|
providers,
|
|
providersForModal,
|
|
} from "../js/services/authProviders/index.js";
|
|
import bitloginProvider, {
|
|
setPendingBitloginResult,
|
|
} from "../js/services/authProviders/bitlogin.js";
|
|
import { createBitloginAdapter } from "../js/nostr/adapters/bitloginAdapter.js";
|
|
import { accessControl } from "../js/accessControl.js";
|
|
|
|
const HEX_PUBKEY = "a".repeat(64);
|
|
|
|
describe("BitLogin auth provider", () => {
|
|
const originalCanAccess = accessControl.canAccess;
|
|
const originalIsBlacklisted = accessControl.isBlacklisted;
|
|
|
|
afterEach(() => {
|
|
setPendingBitloginResult(null);
|
|
accessControl.canAccess = originalCanAccess;
|
|
accessControl.isBlacklisted = originalIsBlacklisted;
|
|
});
|
|
|
|
it("registers in the resolvable provider registry but not the modal grid", () => {
|
|
// AuthService.resolveAuthProvider() must be able to find "bitlogin" ...
|
|
assert.equal(providers.bitlogin, bitloginProvider);
|
|
// ... but the login modal's grid (LoginModalController) must never render
|
|
// a redundant button for it -- BitLogin gets its own permanent widget in
|
|
// components/login-modal.html instead.
|
|
assert.equal(providersForModal.bitlogin, undefined);
|
|
assert.equal(providersForModal.nip07, providers.nip07);
|
|
});
|
|
|
|
it("refuses to log in before the widget has actually produced a pubkey", async () => {
|
|
accessControl.canAccess = () => true;
|
|
setPendingBitloginResult(null);
|
|
|
|
await assert.rejects(
|
|
() => bitloginProvider.login({ nostrClient: {} }),
|
|
/hasn't completed yet/i,
|
|
);
|
|
});
|
|
|
|
it("activates the signer and resolves once the widget's result is pending", async () => {
|
|
accessControl.canAccess = () => true;
|
|
let activated = null;
|
|
const fakeSigner = { type: "bitlogin", pubkey: HEX_PUBKEY };
|
|
const fakeNostrClient = {
|
|
signerManager: {
|
|
setActiveSigner: (signer) => {
|
|
activated = signer;
|
|
},
|
|
},
|
|
};
|
|
|
|
setPendingBitloginResult({ pubkey: HEX_PUBKEY, signer: fakeSigner });
|
|
|
|
const result = await bitloginProvider.login({ nostrClient: fakeNostrClient });
|
|
|
|
assert.equal(result.authType, "bitlogin");
|
|
assert.equal(result.pubkey, HEX_PUBKEY);
|
|
assert.equal(result.signer, fakeSigner);
|
|
assert.equal(activated, fakeSigner);
|
|
});
|
|
|
|
it("consumes the pending result exactly once (no replay across calls)", async () => {
|
|
accessControl.canAccess = () => true;
|
|
const fakeNostrClient = { signerManager: { setActiveSigner: () => {} } };
|
|
setPendingBitloginResult({ pubkey: HEX_PUBKEY, signer: {} });
|
|
|
|
await bitloginProvider.login({ nostrClient: fakeNostrClient });
|
|
|
|
await assert.rejects(
|
|
() => bitloginProvider.login({ nostrClient: fakeNostrClient }),
|
|
/hasn't completed yet/i,
|
|
);
|
|
});
|
|
|
|
it("never activates a signer for a blacklisted pubkey", async () => {
|
|
accessControl.canAccess = () => false;
|
|
accessControl.isBlacklisted = () => true;
|
|
let activated = null;
|
|
const fakeNostrClient = {
|
|
signerManager: {
|
|
setActiveSigner: (signer) => {
|
|
activated = signer;
|
|
},
|
|
},
|
|
};
|
|
|
|
setPendingBitloginResult({ pubkey: HEX_PUBKEY, signer: { type: "bitlogin" } });
|
|
|
|
await assert.rejects(
|
|
() => bitloginProvider.login({ nostrClient: fakeNostrClient }),
|
|
/blocked/i,
|
|
);
|
|
assert.equal(activated, null);
|
|
});
|
|
});
|
|
|
|
describe("createBitloginAdapter", () => {
|
|
it("delegates signing and NIP-44 calls to the live widget element, never window.nostr", async () => {
|
|
const calls = [];
|
|
const fakeWidget = {
|
|
signEvent: async (event) => {
|
|
calls.push(["signEvent", event]);
|
|
return { ...event, id: "signed" };
|
|
},
|
|
nip44Encrypt: async (peer, plaintext) => {
|
|
calls.push(["nip44Encrypt", peer, plaintext]);
|
|
return "ciphertext";
|
|
},
|
|
nip44Decrypt: async (peer, payload) => {
|
|
calls.push(["nip44Decrypt", peer, payload]);
|
|
return "plaintext";
|
|
},
|
|
logout: async () => {
|
|
calls.push(["logout"]);
|
|
},
|
|
};
|
|
|
|
const adapter = createBitloginAdapter(fakeWidget, HEX_PUBKEY);
|
|
|
|
assert.equal(adapter.type, "bitlogin");
|
|
assert.equal(adapter.pubkey, HEX_PUBKEY);
|
|
assert.equal(adapter.canSign(), true);
|
|
assert.deepEqual(adapter.capabilities, { sign: true, nip44: true, nip04: false });
|
|
|
|
const signed = await adapter.signEvent({ kind: 1, content: "hi" });
|
|
assert.equal(signed.id, "signed");
|
|
|
|
await adapter.nip44Encrypt("peer-pubkey", "hello");
|
|
await adapter.nip44Decrypt("peer-pubkey", "ciphertext");
|
|
await adapter.destroy();
|
|
|
|
assert.deepEqual(calls, [
|
|
["signEvent", { kind: 1, content: "hi" }],
|
|
["nip44Encrypt", "peer-pubkey", "hello"],
|
|
["nip44Decrypt", "peer-pubkey", "ciphertext"],
|
|
["logout"],
|
|
]);
|
|
});
|
|
});
|