mirror of
https://github.com/PR0M3TH3AN/bitlogin.git
synced 2026-11-06 13:31:21 +00:00
Implement BitLogin as a modular protocol library, embeddable widget, and demo site
Builds out the v0.3 spec as an npm workspace monorepo any static site can consume for login and Nostr signing: - packages/core: framework-free protocol implementation — Argon2id/HKDF/ ScalarExpand/AES-256-GCM/JCS crypto primitives, NIP-01 event handling and a quorum-aware relay pool, credential/recovery capsule construction and validation, and the full account lifecycle (registration, any-device login with rollback/relay-disagreement detection, phrase recovery with recovery-capsule refresh, password rotation with mandatory tombstone + NIP-09 deletion, replica repair, recovery export, NIP-19 encoding, a NIP-44 v2 implementation, and a NIP-07-shaped signer). 49 tests cover the spec's Phase 0 byte-level vectors plus full create/login/recover/rotate end-to-end scenarios against an in-memory mock relay. - packages/widget: a <bitlogin-auth> custom element with a dedicated crypto Web Worker (private keys never touch the main thread) and a window.nostr NIP-07-compatible provider, so existing Nostr web apps work against it with zero code changes. Built with Vite into two relocatable static files. - packages/demo: a plain static site (no build step of its own) consuming the widget — a branding/landing page, an integration guide, and an account-manager page covering create/login/recover, password rotation, and identity export. All flows were verified twice: protocol-level tests against a mock relay, and the actual built widget driven end-to-end in real Chromium (registration, phrase confirmation, event signing, clean-device login, window.nostr, and phrase recovery all reproduce the identical everyday pubkey). Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01GWJbXXhvTx9wo59jSz57J6
This commit is contained in:
85 files changed
+9563
-2
No files matched your search
@@ -0,0 +1,110 @@
|
||||
/** Main-thread proxy for the crypto Web Worker. Every method returns a Promise resolving to public data only. */
|
||||
import type {
|
||||
WorkerActionMap,
|
||||
WorkerAction,
|
||||
WorkerRequest,
|
||||
WorkerResponse,
|
||||
ConfigurePayload,
|
||||
RegisterPayload,
|
||||
LoginPayload,
|
||||
RecoverPayload,
|
||||
CompleteRecoveryPayload,
|
||||
ChangePasswordPayload,
|
||||
PublishProfilePayload,
|
||||
SignEventPayload,
|
||||
Nip44EncryptPayload,
|
||||
Nip44DecryptPayload
|
||||
} from "./protocol.js";
|
||||
|
||||
export class WorkerClient {
|
||||
private readonly worker: Worker;
|
||||
private readonly pending = new Map<string, { resolve: (value: unknown) => void; reject: (error: Error) => void }>();
|
||||
private counter = 0;
|
||||
|
||||
constructor() {
|
||||
// Deliberately NOT the literal `new Worker(new URL("./x.js", import.meta.url))` shape: Vite
|
||||
// statically pattern-matches that exact expression (string-literal first argument) and
|
||||
// rewrites/inlines it at build time, which breaks when this widget is served from a
|
||||
// subdirectory rather than the site root. Building the path in a variable defeats the
|
||||
// static match, leaving plain runtime URL resolution -- relative to *this* module's actual
|
||||
// URL, exactly like any other web platform relative import -- untouched by the bundler.
|
||||
const workerFileName = ["cryptoWorker", ".js"].join("");
|
||||
const workerUrl = new URL(workerFileName, import.meta.url);
|
||||
this.worker = new Worker(workerUrl, { type: "module" });
|
||||
this.worker.addEventListener("message", (event: MessageEvent<WorkerResponse>) => {
|
||||
const msg = event.data;
|
||||
const entry = this.pending.get(msg.id);
|
||||
if (!entry) return;
|
||||
this.pending.delete(msg.id);
|
||||
if (msg.ok) {
|
||||
entry.resolve(msg.result);
|
||||
} else {
|
||||
const error = new Error(msg.error);
|
||||
error.name = msg.errorName ?? "Error";
|
||||
entry.reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private call<A extends WorkerAction>(action: A, payload: WorkerActionMap[A][0]): Promise<WorkerActionMap[A][1]> {
|
||||
const id = `${Date.now().toString(36)}-${(this.counter++).toString(36)}`;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pending.set(id, { resolve: resolve as (value: unknown) => void, reject });
|
||||
const request: WorkerRequest = { id, action, payload };
|
||||
this.worker.postMessage(request);
|
||||
});
|
||||
}
|
||||
|
||||
configure(payload: ConfigurePayload) {
|
||||
return this.call("configure", payload);
|
||||
}
|
||||
register(payload: RegisterPayload) {
|
||||
return this.call("register", payload);
|
||||
}
|
||||
login(payload: LoginPayload) {
|
||||
return this.call("login", payload);
|
||||
}
|
||||
recover(payload: RecoverPayload) {
|
||||
return this.call("recover", payload);
|
||||
}
|
||||
completeRecovery(payload: CompleteRecoveryPayload) {
|
||||
return this.call("completeRecovery", payload);
|
||||
}
|
||||
changePassword(payload: ChangePasswordPayload) {
|
||||
return this.call("changePassword", payload);
|
||||
}
|
||||
publishProfileAndRelayLists(payload: PublishProfilePayload) {
|
||||
return this.call("publishProfileAndRelayLists", payload);
|
||||
}
|
||||
getPublicKey() {
|
||||
return this.call("getPublicKey", {});
|
||||
}
|
||||
signEvent(payload: SignEventPayload) {
|
||||
return this.call("signEvent", payload);
|
||||
}
|
||||
nip44Encrypt(payload: Nip44EncryptPayload) {
|
||||
return this.call("nip44Encrypt", payload);
|
||||
}
|
||||
nip44Decrypt(payload: Nip44DecryptPayload) {
|
||||
return this.call("nip44Decrypt", payload);
|
||||
}
|
||||
exportIdentity() {
|
||||
return this.call("exportIdentity", {});
|
||||
}
|
||||
buildRecoveryExport() {
|
||||
return this.call("buildRecoveryExport", {});
|
||||
}
|
||||
repairReplicas() {
|
||||
return this.call("repairReplicas", {});
|
||||
}
|
||||
getSessionStatus() {
|
||||
return this.call("getSessionStatus", {});
|
||||
}
|
||||
logout() {
|
||||
return this.call("logout", {});
|
||||
}
|
||||
|
||||
terminate(): void {
|
||||
this.worker.terminate();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user