feat: initial scaffold of uniweb — unibus web frontend (SPA + gateway)

Extracted from unibus v0.13.0: the chat SPA (web/, React+Mantine, per-user
BIP39 wallet) and the web gateway (cmd/webgw, REST+SSE) that acts as a bus
peer for the browser. Consumes unibus as a Go module via replace => ../unibus,
keeping its own replace fn-registry for the cybersecurity primitives.

go build/vet/test and pnpm build green in the new location.
This commit is contained in:
agent
2026-06-13 21:23:10 +02:00
commit e8e37d77fe
42 changed files with 5439 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
// High-level wallet account operations shared by the join, recover and login
// flows. These compose the low-level primitives (derive / crypto / store) with
// the gateway API so the page components stay thin.
import { api } from "../api";
import type { MeInfo, User } from "../types";
import { decryptJSON, encryptJSON } from "./crypto";
import type { WalletIdentity } from "./derive";
import { getIdentity, putIdentity, type StoredIdentity } from "./store";
function toUser(me: MeInfo): User {
return { id: me.endpoint, handle: me.handle || me.endpoint.slice(0, 8) };
}
// saveAndOpen encrypts the identity under `password`, stores it on this device,
// and opens a gateway session as that user. Used by join (new identity) and
// recover (re-derived identity): both end with a locally-encrypted key plus a
// live per-user session. The mnemonic/seed is NOT touched here — only the derived
// keypair is persisted (encrypted).
export async function saveAndOpen(
identity: WalletIdentity,
handle: string,
password: string,
): Promise<User> {
const enc = await encryptJSON(identity, password);
await putIdentity({
handle,
signPub: identity.signPub,
kexPub: identity.kexPub,
enc,
createdAt: Date.now(),
});
const me = await api.session(identity, handle);
return toUser(me);
}
// unlockAndOpen reads this device's stored identity, decrypts the private key with
// `password`, and opens a gateway session. Throws WrongPasswordError on a bad
// password (GCM auth failure) and NoLocalIdentityError if the device has none.
export async function unlockAndOpen(password: string): Promise<User> {
const stored = await getIdentity();
if (!stored) throw new NoLocalIdentityError();
const identity = await decryptJSON<WalletIdentity>(stored.enc, password);
const me = await api.session(identity, stored.handle);
return toUser(me);
}
// localIdentity returns the device's stored identity record (or null), for the
// router to decide between the password-unlock screen and the welcome screen, and
// to greet the user by handle before unlocking.
export async function localIdentity(): Promise<StoredIdentity | null> {
return getIdentity();
}
export class NoLocalIdentityError extends Error {
constructor() {
super("no local identity on this device");
this.name = "NoLocalIdentityError";
}
}