feat: add per-profile signer modes with persisted NIP-46 connections
Introduce three coexisting signing modes: - Embedded (INTERNAL): vault-held nsec, decrypted in Rust, signs locally. - Nip46Client (EXTERNAL): Keynctr is the NIP-46 CLIENT; the key never touches this machine. - Nip46Bunker: legacy inverted mode (Keynctr as signer serving others). Data model: - StoredProfile gains signer_mode (serde-defaults to Embedded for legacy profiles); SignerMode moves from app.rs to vault.rs to break a circular dependency; app.rs re-exports it. - Vault gains nip46_connections (profile-owned) and bumps VAULT_VERSION to 3; migrate_vault_signer_modes() normalises on load (idempotent). - Nip46Connection gains profile_npub ownership, parsed permissions, expires_at, and revoked_at. Signer abstraction (src/signer): - Signer trait gains pubkey_for() identity validation, a Signing enum (Local vs External) that re-verifies the returned event, and a permission surface (permissions/can_*/is_connection_valid) with safe defaults. - permissions.rs: NIP-46 per-connection permission model (parse, validate, deny-by-default, no-broadening checks) with 52 unit tests. - Nip46ClientSigner parses perms from nostrconnect:// URIs, enforces permissions on every gated request, persists/revokes connections in the vault, and audits permission denials via the app's audit log. - App gains audit_log and a nip46_bunker_signer handle; default mode is Nip46Client (most secure). Frontend: SignerModeScreen redesigned for the three modes with a nostr-tools-based SignerManager client, new IPC allowlist entries, and signer-mode styling. Verified: cargo test --release 186 passed; clippy/fmt clean; frontend tsc clean, vitest 110 passed.
This commit is contained in:
parent
caed722b06
commit
2c61830390
17 changed files with 2801 additions and 319 deletions
|
|
@ -38,12 +38,12 @@ protocol.registerSchemesAsPrivileged([
|
|||
* (HMR websocket included).
|
||||
*/
|
||||
const CSP_PROD =
|
||||
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; " +
|
||||
"connect-src 'self'; img-src 'self' data: https:; object-src 'none'; " +
|
||||
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " +
|
||||
"connect-src 'self'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; object-src 'none'; " +
|
||||
"base-uri 'none'; form-action 'none'";
|
||||
const CSP_DEV =
|
||||
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; " +
|
||||
"connect-src 'self' ws://localhost:* http://localhost:*; img-src 'self' data: https:; " +
|
||||
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " +
|
||||
"connect-src 'self' ws://localhost:* http://localhost:*; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; " +
|
||||
"object-src 'none'; base-uri 'none'; form-action 'none'";
|
||||
|
||||
/** The CSP for a URL this window may load, or `null` for anywhere else. */
|
||||
|
|
@ -218,10 +218,26 @@ const RENDERER_METHODS: ReadonlySet<string> = new Set([
|
|||
'lock_vault',
|
||||
'remove_vault_password',
|
||||
'reveal_secret_key',
|
||||
'export_secret_key',
|
||||
// Legacy bunker
|
||||
'signer_connect',
|
||||
'signer_disconnect',
|
||||
'signer_status',
|
||||
'signer_approve',
|
||||
// New signer modes (default: nip46_client most secure)
|
||||
'signer_mode_get',
|
||||
'signer_mode_set',
|
||||
'embedded_signer_status',
|
||||
'embedded_signer_approve',
|
||||
'nip46_connect',
|
||||
'nip46_disconnect',
|
||||
'nip46_status',
|
||||
'nip46_approve',
|
||||
// Sidecar (local isolated signer, planned)
|
||||
'sidecar_connect',
|
||||
'sidecar_disconnect',
|
||||
'sidecar_status',
|
||||
'sidecar_approve',
|
||||
]);
|
||||
|
||||
/** True when `method` may be dispatched. Unknown methods never reach the backend. */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue