mirror of
https://github.com/PR0M3TH3AN/bitlogin.git
synced 2026-11-07 05:51:25 +00:00
Widget: offerNwcConnection() -- consent-gated offer-to-save
The inverse of requestNwcConnection: an app that obtained an NWC URI by its own means (its own wallet chooser, its own paste box) offers the user a portable copy. The credential is already in the app's hands, so the only question on screen is whether a copy enters the user's vault -- and that write happens in BitLogin's own chrome or not at all, never silently, because a page that can pump records into someone's account without a tap is a spam vector wearing a feature's clothes. Dedupe by connection identity (wallet pubkey + secret; relays and labels may drift without making a new connection -- sameNwcCredential in core): a duplicate resolves 'already-saved' with a silent origin-binding refresh and no UI, which is what makes the call safe to fire after every successful connect. 'unavailable' (no session, no vault root) is a quiet no-op by contract. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1
This commit is contained in:
1 parent
6841d647c2
commit
6b48de6168
9 files changed
+191
-3
No files matched your search
@@ -24,6 +24,7 @@ import {
|
||||
VaultSession,
|
||||
parseNwcUri,
|
||||
toNwcUri,
|
||||
sameNwcCredential,
|
||||
validateNwcCredential,
|
||||
type DecryptedConnectionRecord,
|
||||
type NwcCredential
|
||||
@@ -570,6 +571,39 @@ async function handle(action: string, payload: unknown): Promise<unknown> {
|
||||
return {};
|
||||
}
|
||||
|
||||
case "vaultOfferCheck": {
|
||||
// The offer-to-save flow's dedupe: the same wallet + secret already
|
||||
// stored means nothing NEW would enter the vault, so no consent screen
|
||||
// is warranted. The origin binding is refreshed silently — the offering
|
||||
// origin already HOLDS the credential (it supplied it), so binding is
|
||||
// bookkeeping for later one-tap approvals, not a new grant.
|
||||
const p = payload as { uri: string; origin: string | null };
|
||||
const credential = parseNwcUri(p.uri);
|
||||
const { vault, connections } = await listLiveConnections();
|
||||
const existing = connections.find(
|
||||
(c) =>
|
||||
c.record.connection_type === "nwc" &&
|
||||
sameNwcCredential(c.record.credential as unknown as NwcCredential, credential)
|
||||
);
|
||||
if (!existing) return { duplicate: false };
|
||||
if (p.origin !== null && existing.record.application_binding.origin !== p.origin) {
|
||||
const { record, event } = await vault.updateConnection(existing, {
|
||||
application_binding: { origin: p.origin, app_pubkey: null }
|
||||
});
|
||||
const publish = await vault.publish({
|
||||
event,
|
||||
connectionId: record.connection_id,
|
||||
relayUrls: vaultRelayUrls,
|
||||
store
|
||||
});
|
||||
if (publish.success) {
|
||||
const decrypted = await vault.decryptEvent(event);
|
||||
return { duplicate: true, connection: summarize(decrypted!) };
|
||||
}
|
||||
}
|
||||
return { duplicate: true, connection: summarize(existing) };
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown worker action: ${action}`);
|
||||
}
|
||||
|
||||
@@ -220,6 +220,18 @@ export interface VaultDeletePayload {
|
||||
connectionId: string;
|
||||
}
|
||||
|
||||
export interface VaultOfferCheckPayload {
|
||||
uri: string;
|
||||
origin: string | null;
|
||||
}
|
||||
/** duplicate=true means the same wallet+secret is already stored; the worker
|
||||
* has silently refreshed its origin binding, and no consent UI is needed —
|
||||
* nothing new entered the vault. */
|
||||
export interface VaultOfferCheckResult {
|
||||
duplicate: boolean;
|
||||
connection?: VaultConnectionSummary;
|
||||
}
|
||||
|
||||
export type WorkerActionMap = {
|
||||
configure: [ConfigurePayload, Record<string, never>];
|
||||
register: [RegisterPayload, RegisterResult];
|
||||
@@ -248,6 +260,7 @@ export type WorkerActionMap = {
|
||||
vaultRevealNwc: [VaultRevealNwcPayload, VaultRevealNwcResult];
|
||||
vaultSetBinding: [VaultSetBindingPayload, VaultConnectionSummary];
|
||||
vaultDelete: [VaultDeletePayload, Record<string, never>];
|
||||
vaultOfferCheck: [VaultOfferCheckPayload, VaultOfferCheckResult];
|
||||
};
|
||||
|
||||
export type WorkerAction = keyof WorkerActionMap;
|
||||
@@ -21,7 +21,8 @@ import type {
|
||||
VaultFindForOriginPayload,
|
||||
VaultRevealNwcPayload,
|
||||
VaultSetBindingPayload,
|
||||
VaultDeletePayload
|
||||
VaultDeletePayload,
|
||||
VaultOfferCheckPayload
|
||||
} from "./protocol.js";
|
||||
|
||||
export class WorkerClient {
|
||||
@@ -141,6 +142,9 @@ export class WorkerClient {
|
||||
vaultDelete(payload: VaultDeletePayload) {
|
||||
return this.call("vaultDelete", payload);
|
||||
}
|
||||
vaultOfferCheck(payload: VaultOfferCheckPayload) {
|
||||
return this.call("vaultOfferCheck", payload);
|
||||
}
|
||||
logout() {
|
||||
return this.call("logout", {});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user