Files
bitlogin/packages/widget/src/worker/protocol.ts
T
thePR0M3TH3ANandClaude Fable 5 c3e55add16 Widget: requestNwcConnection() -- vault authorization layer, reveal mode
The Phase D surface from vault-ux.md, scoped to NWC and honest about its
boundary: an embedded same-origin widget cannot broker (§CV12.3), so the
API reveals, says so in its consent copy, and leans on the wallet-side
budget as the real security boundary.

One public call owns the whole ceremony. If nobody is signed in, the
sign-in flow runs first and the request resumes via the goto("dashboard")
hook exactly once; if the account has a connection bound to the
requesting origin, the user gets the one-tap approval screen the vault
exists for; otherwise a guided import -- Bitcoin Connect's wallet
chooser or a pasted URI. The BC integration lifts Satisfied's
production discipline verbatim: chooser only, lazy chunks (never
downloaded unless the flow opens), no numeric budget in
authorizationUrlOptions (the unit is not interoperable), disconnect and
clear bc:config so no plaintext copy outlives the ceremony.

The worker holds the vault root (never the sudo key -- wiped on
arrival) and gains vaultStatus/List/SaveNwc/FindForOrigin/RevealNwc/
SetBinding/Delete actions; the root rides the encrypted session cache
so restored sessions keep working, with a vaultEnabled marker so a
stale cache says "sign in again" instead of falsely claiming the
account has no vault. Publishing failures are loud -- a connection that
reached no relay is not saved anywhere and says so. The dashboard gains
Wallet connections management with "revoke app access" and "remove
from BitLogin" honestly separated from wallet-side revocation.
VaultSession gains buildDeletionRequest so the NIP-09 courtesy ask is
signed without exposing the signing key.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1
2026-07-30 13:10:39 -04:00

254 lines
7.4 KiB
TypeScript

/** Request/response envelope for the main-thread <-> crypto-worker RPC channel. */
import type { NostrEvent, NostrTag } from "@bitlogin/core/nostr";
import type { RecoveryExportFile } from "@bitlogin/core/account";
export interface WorkerRequest<TAction extends string = string, TPayload = unknown> {
id: string;
action: TAction;
payload: TPayload;
}
export type WorkerResponse<TResult = unknown> =
| { id: string; ok: true; result: TResult }
| { id: string; ok: false; error: string; errorName?: string };
export interface ConfigurePayload {
vaultRelayUrls?: string[];
discoveryRelayUrls?: string[];
}
export interface RegisterPayload {
loginName: string;
password: string;
/** When present, import this existing identity (nsec or 64-char hex) instead of generating one (§SF10). */
importKey?: string;
}
export interface RegisterResult {
recoveryPhrase: string;
everydayPublicKey: string;
recoveryPublicKey: string;
accountId: string;
imported: boolean;
credentialEventId: string;
recoveryEventId: string;
}
export interface PreviewImportKeyPayload {
nsecOrHex: string;
}
export interface PreviewImportKeyResult {
everydayPublicKey: string;
npub: string;
}
export interface LoginPayload {
loginName: string;
password: string;
/** See `LoginParams.acknowledgeRollback` in `@bitlogin/core/account` (§16.2 step 6). */
acknowledgeRollback?: boolean;
}
export interface LoginResult {
everydayPublicKey: string;
accountId: string;
generation: number;
rollbackWarning?: string;
relayDisagreementWarning?: string;
}
export interface RecoverPayload {
phrase: string;
/**
* A previously downloaded recovery export file (§19.5), parsed on the main thread and
* passed through as-is. Used as a fallback alongside the live relay read when every
* configured relay is unreachable or has lost the capsule -- never a replacement for the
* phrase, which the file never contains.
*/
offlineExportFile?: RecoveryExportFile;
}
export interface RecoverResult {
everydayPublicKey: string;
accountId: string;
generalRelays: string[];
dmRelays: string[];
chainWarning?: string;
}
export interface CompleteRecoveryPayload {
newLoginName: string;
newPassword: string;
}
export interface CompleteRecoveryResult {
locatorPublicKey: string;
credentialEventId: string;
refreshedRecoveryEventId: string;
}
export interface ChangePasswordPayload {
loginName: string;
oldPassword: string;
newPassword: string;
/** See `ChangePasswordParams.acknowledgeRollback` in `@bitlogin/core/account` (§16.2 step 6). */
acknowledgeRollback?: boolean;
}
export interface ChangePasswordResult {
newLocatorPublicKey: string;
newGeneration: number;
tombstoneAcknowledgedCount: number;
deletionAcknowledgedCount: number;
}
export interface PublishProfilePayload {
name?: string;
about?: string;
picture?: string;
generalRelays: string[];
dmRelays: string[];
}
export interface PublishProfileResult {
profilePublished: boolean;
relayListAcknowledgedCount: number;
dmRelayListAcknowledgedCount: number;
/** True when an existing kind:0 profile was already found for this identity, so BitLogin left it untouched (§28.1). */
profileSkippedExisting: boolean;
/** True when an existing NIP-65 relay list was already found and left untouched (§28.1). */
relayListSkippedExisting: boolean;
/** True when an existing NIP-17 DM relay list was already found and left untouched (§28.1). */
dmRelayListSkippedExisting: boolean;
}
export interface SignEventPayload {
kind: number;
tags?: NostrTag[];
content: string;
created_at?: number;
}
export interface Nip44EncryptPayload {
peerPublicKey: string;
plaintext: string;
}
export interface Nip44DecryptPayload {
peerPublicKey: string;
payload: string;
}
export interface Nip04EncryptPayload {
peerPublicKey: string;
plaintext: string;
}
export interface Nip04DecryptPayload {
peerPublicKey: string;
payload: string;
}
export interface ExportIdentityResult {
nsec: string;
npub: string;
}
export interface RepairReplicasResult {
credentialAcknowledgedCount: number;
recoveryAcknowledgedCount: number;
relaysTried: number;
}
export interface SessionStatusResult {
unlocked: boolean;
everydayPublicKey?: string;
}
export interface RestoreSessionResult {
restored: boolean;
everydayPublicKey?: string;
accountId?: string;
}
// ---- Connection Vault (connection-vault.md §12, phase D; reveal mode) ----
export interface VaultStatusResult {
/** True when this session holds the vault root and can use connections. */
enabled: boolean;
/** Set when enabled=false: the account predates the vault ("no-vault") or
* the root simply wasn't in the restored cache and a fresh sign-in would
* load it ("stale-cache"). */
reason?: "no-vault" | "stale-cache";
vaultPublicKey?: string;
}
/** Everything the UI needs and nothing an app must not see: no secrets. */
export interface VaultConnectionSummary {
connectionId: string;
connectionType: string;
label: string;
state: string;
origin: string | null;
createdAt: number;
updatedAt: number;
/** NWC display metadata (non-secret). */
walletPubkey?: string;
relayCount?: number;
}
export interface VaultListResult {
connections: VaultConnectionSummary[];
rollbackWarnings: string[];
}
export interface VaultSaveNwcPayload {
uri: string;
label: string;
origin: string | null;
}
export interface VaultFindForOriginPayload {
origin: string;
}
export interface VaultRevealNwcPayload {
connectionId: string;
}
export interface VaultRevealNwcResult {
uri: string;
}
export interface VaultSetBindingPayload {
connectionId: string;
origin: string | null;
}
export interface VaultDeletePayload {
connectionId: string;
}
export type WorkerActionMap = {
configure: [ConfigurePayload, Record<string, never>];
register: [RegisterPayload, RegisterResult];
previewImportKey: [PreviewImportKeyPayload, PreviewImportKeyResult];
login: [LoginPayload, LoginResult];
recover: [RecoverPayload, RecoverResult];
completeRecovery: [CompleteRecoveryPayload, CompleteRecoveryResult];
changePassword: [ChangePasswordPayload, ChangePasswordResult];
publishProfileAndRelayLists: [PublishProfilePayload, PublishProfileResult];
getPublicKey: [Record<string, never>, { publicKey: string }];
signEvent: [SignEventPayload, NostrEvent];
nip44Encrypt: [Nip44EncryptPayload, { ciphertext: string }];
nip44Decrypt: [Nip44DecryptPayload, { plaintext: string }];
nip04Encrypt: [Nip04EncryptPayload, { ciphertext: string }];
nip04Decrypt: [Nip04DecryptPayload, { plaintext: string }];
exportIdentity: [Record<string, never>, ExportIdentityResult];
buildRecoveryExport: [Record<string, never>, unknown];
repairReplicas: [Record<string, never>, RepairReplicasResult];
getSessionStatus: [Record<string, never>, SessionStatusResult];
restoreSession: [Record<string, never>, RestoreSessionResult];
logout: [Record<string, never>, Record<string, never>];
vaultStatus: [Record<string, never>, VaultStatusResult];
vaultList: [Record<string, never>, VaultListResult];
vaultSaveNwc: [VaultSaveNwcPayload, VaultConnectionSummary];
vaultFindForOrigin: [VaultFindForOriginPayload, { connection: VaultConnectionSummary | null }];
vaultRevealNwc: [VaultRevealNwcPayload, VaultRevealNwcResult];
vaultSetBinding: [VaultSetBindingPayload, VaultConnectionSummary];
vaultDelete: [VaultDeletePayload, Record<string, never>];
};
export type WorkerAction = keyof WorkerActionMap;