mirror of
https://github.com/PR0M3TH3AN/bitlogin.git
synced 2026-10-27 08:31:21 +00:00
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
111 lines
3.6 KiB
TypeScript
111 lines
3.6 KiB
TypeScript
/** 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();
|
|
}
|
|
}
|