feat(signer): accept bunker:// URIs, async signer permissions, NIP-46 e2e test
- SignerManager/SignerModeScreen parse both nostrconnect:// and bunker:// (Amber presents bunker://; signer pubkey extracted before '@') - Signer permission surface made async (permissions, can_*, is_connection_valid) - tests/nip46_e2e.rs: full client handshake against fake Amber over a local relay — NIP-44 round-trip, get_public_key identity, signed-event verification, vault persistence asserting no secret material for remote profiles - prettier formatting of touched frontend files
This commit is contained in:
parent
84f11e615d
commit
85756df081
11 changed files with 654 additions and 116 deletions
|
|
@ -89,9 +89,13 @@ export function ExportSecretKeyModal({ open, onClose, profile }: ExportSecretKey
|
|||
} else if (code === 'profile_not_found') {
|
||||
setFatal({ message: 'That profile is not stored on this computer.' });
|
||||
setPhase('error');
|
||||
} else if (code === 'external_signer_not_connected' || code === 'external_signer_identity_mismatch') {
|
||||
} else if (
|
||||
code === 'external_signer_not_connected' ||
|
||||
code === 'external_signer_identity_mismatch'
|
||||
) {
|
||||
setFatal({
|
||||
message: 'This profile uses an external signer. Secret key export is not possible for externally managed accounts.',
|
||||
message:
|
||||
'This profile uses an external signer. Secret key export is not possible for externally managed accounts.',
|
||||
});
|
||||
setPhase('error');
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -322,14 +322,21 @@ export class SignerManager {
|
|||
|
||||
// ==================== CLIENT MODE (connect TO external signer) ====================
|
||||
|
||||
/** Parse nostrconnect:// URI from external signer (Amber, Nostr Connect, etc.) */
|
||||
/** Parse nostrconnect:// or bunker:// URI from external signer (Amber, Nostr Connect, etc.) */
|
||||
parseExternalSignerURI(uri: string): ExternalSignerConnection {
|
||||
if (!uri.startsWith('nostrconnect://')) {
|
||||
throw new SignerError('INVALID_NOSTRCONNECT_URI', 'URI must start with nostrconnect://');
|
||||
const isBunker = uri.startsWith('bunker://');
|
||||
if (!uri.startsWith('nostrconnect://') && !isBunker) {
|
||||
throw new SignerError(
|
||||
'INVALID_NOSTRCONNECT_URI',
|
||||
'URI must start with nostrconnect:// or bunker://',
|
||||
);
|
||||
}
|
||||
|
||||
const [authority, queryString] = uri.slice('nostrconnect://'.length).split('?');
|
||||
const signerPubkey = authority;
|
||||
const withoutScheme = uri.slice(isBunker ? 'bunker://'.length : 'nostrconnect://'.length);
|
||||
const [authorityRaw, queryString] = withoutScheme.split('?');
|
||||
// bunker://<key>@<primary-relay>?relay=… carries a display relay in the
|
||||
// authority; the key is what precedes the '@'.
|
||||
const signerPubkey = authorityRaw.split('@')[0];
|
||||
const params = new URLSearchParams(queryString || '');
|
||||
const relays = params.getAll('relay');
|
||||
const secret = params.get('secret') || undefined;
|
||||
|
|
|
|||
|
|
@ -33,11 +33,10 @@ export function SignerModeScreen() {
|
|||
|
||||
// Single source of truth: backend state (defaults to most secure)
|
||||
const mode = (state?.signer_mode ?? 'nip46_client') as SignerMode;
|
||||
const isNip46Active = (mode === 'nip46_client' || mode === 'nip46_bunker') && !!nip46StatusState?.connected;
|
||||
const isNip46Active =
|
||||
(mode === 'nip46_client' || mode === 'nip46_bunker') && !!nip46StatusState?.connected;
|
||||
const isEmbeddedActive = mode === 'embedded' && !!embeddedStatus?.available;
|
||||
|
||||
|
||||
|
||||
const refreshStatus = useCallback(async () => {
|
||||
try {
|
||||
// Mode comes from AppProvider state, just refresh signer statuses
|
||||
|
|
@ -53,14 +52,24 @@ export function SignerModeScreen() {
|
|||
const status = await embeddedSignerStatus();
|
||||
setEmbeddedStatus(status);
|
||||
} catch {
|
||||
setEmbeddedStatus({ type: 'embedded', available: false, pending_count: 0, pending: [] } as any);
|
||||
setEmbeddedStatus({
|
||||
type: 'embedded',
|
||||
available: false,
|
||||
pending_count: 0,
|
||||
pending: [],
|
||||
} as any);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const status = await nip46Status();
|
||||
setNip46StatusState(status);
|
||||
} catch {
|
||||
setNip46StatusState({ connected: false, relays: [], connected_relays: [], pending_approvals: [] } as any);
|
||||
setNip46StatusState({
|
||||
connected: false,
|
||||
relays: [],
|
||||
connected_relays: [],
|
||||
pending_approvals: [],
|
||||
} as any);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
@ -96,12 +105,18 @@ export function SignerModeScreen() {
|
|||
await refresh();
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
if (msg.includes('No keypair') || msg.includes('No active profile') || msg.includes('no active profile')) {
|
||||
if (
|
||||
msg.includes('No keypair') ||
|
||||
msg.includes('No active profile') ||
|
||||
msg.includes('no active profile')
|
||||
) {
|
||||
setError('No keypair found: Please import a key first.');
|
||||
} else if (msg.includes('vault_locked') || msg.toLowerCase().includes('vault locked')) {
|
||||
setError('Vault locked: Please unlock to switch modes.');
|
||||
} else if (msg.includes('ACTIVE_SESSION') || msg.toLowerCase().includes('active session')) {
|
||||
setError('Invalid mode transition: Cannot switch while active session exists. Disconnect first.');
|
||||
setError(
|
||||
'Invalid mode transition: Cannot switch while active session exists. Disconnect first.',
|
||||
);
|
||||
} else {
|
||||
setError(msg || 'That operation is not permitted.');
|
||||
}
|
||||
|
|
@ -112,8 +127,12 @@ export function SignerModeScreen() {
|
|||
|
||||
const handleNip46Connect = useCallback(async () => {
|
||||
const trimmed = uri.trim();
|
||||
if (!trimmed.startsWith('nostrconnect://')) {
|
||||
setError('Paste a nostrconnect:// link from Amber, Nostr Connect, or your bunker.');
|
||||
// Amber and self-hosted bunkers show a bunker:// link; Nostr Connect
|
||||
// apps use nostrconnect://. Both are accepted by the backend parser.
|
||||
if (!trimmed.startsWith('nostrconnect://') && !trimmed.startsWith('bunker://')) {
|
||||
setError(
|
||||
'Paste a bunker:// or nostrconnect:// link from Amber, Nostr Connect, or your bunker.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
|
|
@ -183,12 +202,16 @@ export function SignerModeScreen() {
|
|||
return isEmbeddedActive ? (
|
||||
<Badge tone="success">Embedded (Least Secure)</Badge>
|
||||
) : (
|
||||
<Badge tone={vaultLocked ? 'warning' : 'neutral'}>Embedded {vaultLocked ? '(Vault Locked)' : ''}</Badge>
|
||||
<Badge tone={vaultLocked ? 'warning' : 'neutral'}>
|
||||
Embedded {vaultLocked ? '(Vault Locked)' : ''}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
const handleImportKey = useCallback(async () => {
|
||||
const nsec = prompt('Enter your nsec (npub will be derived) or leave blank to generate a new key:');
|
||||
const nsec = prompt(
|
||||
'Enter your nsec (npub will be derived) or leave blank to generate a new key:',
|
||||
);
|
||||
if (nsec === null) return;
|
||||
setError(null);
|
||||
try {
|
||||
|
|
@ -237,11 +260,15 @@ export function SignerModeScreen() {
|
|||
<div className="status-grid">
|
||||
<div className={`status-item ${hasProfile ? 'ok' : 'missing'}`}>
|
||||
<span className="status-label">Keypair</span>
|
||||
<span className="status-value">{hasProfile ? `${state?.active_profile?.npub.slice(0, 16)}…` : 'Not imported'}</span>
|
||||
<span className="status-value">
|
||||
{hasProfile ? `${state?.active_profile?.npub.slice(0, 16)}…` : 'Not imported'}
|
||||
</span>
|
||||
</div>
|
||||
<div className={`status-item ${!vaultLocked ? 'ok' : 'locked'}`}>
|
||||
<span className="status-label">Vault</span>
|
||||
<span className="status-value">{vaultLocked ? 'Locked' : 'Unlocked / No password'}</span>
|
||||
<span className="status-value">
|
||||
{vaultLocked ? 'Locked' : 'Unlocked / No password'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="status-item">
|
||||
<span className="status-label">Current Mode</span>
|
||||
|
|
@ -249,12 +276,20 @@ export function SignerModeScreen() {
|
|||
</div>
|
||||
</div>
|
||||
{!hasProfile && (
|
||||
<Button variant="primary" onClick={() => void handleImportKey()} style={{ marginTop: 12 }}>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => void handleImportKey()}
|
||||
style={{ marginTop: 12 }}
|
||||
>
|
||||
<Icon name="key" size={16} /> Import / Generate Key
|
||||
</Button>
|
||||
)}
|
||||
{hasProfile && vaultLocked && (
|
||||
<Button variant="secondary" onClick={() => void handleUnlockVault()} style={{ marginTop: 12 }}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => void handleUnlockVault()}
|
||||
style={{ marginTop: 12 }}
|
||||
>
|
||||
<Icon name="shield" size={16} /> Unlock Vault
|
||||
</Button>
|
||||
)}
|
||||
|
|
@ -269,7 +304,9 @@ export function SignerModeScreen() {
|
|||
<div className="card-body">
|
||||
<div className="mode-options">
|
||||
{/* 1. Most Secure */}
|
||||
<label className={`mode-option${mode === 'nip46_client' ? ' active' : ''} security-most`}>
|
||||
<label
|
||||
className={`mode-option${mode === 'nip46_client' ? ' active' : ''} security-most`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="signer-mode"
|
||||
|
|
@ -282,9 +319,9 @@ export function SignerModeScreen() {
|
|||
<div className="mode-option-content">
|
||||
<h3>NIP-46 Client (Connect to External Signer)</h3>
|
||||
<p className="security-desc">
|
||||
<strong>Most Secure:</strong> Private key never touches this device. Connects to an
|
||||
external signer (Amber, Nostr Connect, hardware wallet). Every signing request is
|
||||
approved on the external device.
|
||||
<strong>Most Secure:</strong> Private key never touches this device. Connects to
|
||||
an external signer (Amber, Nostr Connect, hardware wallet). Every signing
|
||||
request is approved on the external device.
|
||||
</p>
|
||||
<ul className="mode-features">
|
||||
<li>✓ Private key NEVER on this device</li>
|
||||
|
|
@ -292,12 +329,16 @@ export function SignerModeScreen() {
|
|||
<li>✓ Every request approved externally</li>
|
||||
<li>✓ Key cannot be extracted if this app is compromised</li>
|
||||
</ul>
|
||||
{mode === 'nip46_client' && isNip46Active && <span className="mode-badge active">Connected</span>}
|
||||
{mode === 'nip46_client' && isNip46Active && (
|
||||
<span className="mode-badge active">Connected</span>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{/* 2. Moderately Secure */}
|
||||
<label className={`mode-option${mode === 'nip46_bunker' ? ' active' : ''} security-moderate`}>
|
||||
<label
|
||||
className={`mode-option${mode === 'nip46_bunker' ? ' active' : ''} security-moderate`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="signer-mode"
|
||||
|
|
@ -310,8 +351,8 @@ export function SignerModeScreen() {
|
|||
<div className="mode-option-content">
|
||||
<h3>NIP-46 Bunker (This App Signs)</h3>
|
||||
<p className="security-desc">
|
||||
<strong>Moderately Secure:</strong> This app acts as a signer for other clients. Key
|
||||
stays in this app's encrypted vault; other clients connect via{' '}
|
||||
<strong>Moderately Secure:</strong> This app acts as a signer for other clients.
|
||||
Key stays in this app's encrypted vault; other clients connect via{' '}
|
||||
<code>nostrconnect://</code>.
|
||||
</p>
|
||||
<ul className="mode-features">
|
||||
|
|
@ -320,12 +361,16 @@ export function SignerModeScreen() {
|
|||
<li>✓ Works with Amber, Nostr Connect, etc.</li>
|
||||
<li>⚠ Key in memory when vault unlocked</li>
|
||||
</ul>
|
||||
{mode === 'nip46_bunker' && isNip46Active && <span className="mode-badge active">Running</span>}
|
||||
{mode === 'nip46_bunker' && isNip46Active && (
|
||||
<span className="mode-badge active">Running</span>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{/* 3. Least Secure */}
|
||||
<label className={`mode-option${mode === 'embedded' ? ' active' : ''} security-least`}>
|
||||
<label
|
||||
className={`mode-option${mode === 'embedded' ? ' active' : ''} security-least`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="signer-mode"
|
||||
|
|
@ -338,8 +383,8 @@ export function SignerModeScreen() {
|
|||
<div className="mode-option-content">
|
||||
<h3>Embedded Signer (Local Keys)</h3>
|
||||
<p className="security-desc">
|
||||
<strong>Least Secure:</strong> Keys stored locally, signing on this device. Convenient
|
||||
but key exists in memory when vault unlocked.
|
||||
<strong>Least Secure:</strong> Keys stored locally, signing on this device.
|
||||
Convenient but key exists in memory when vault unlocked.
|
||||
</p>
|
||||
<ul className="mode-features">
|
||||
<li>✓ Keys never leave this device</li>
|
||||
|
|
@ -347,14 +392,18 @@ export function SignerModeScreen() {
|
|||
<li>✓ Encrypted vault (Argon2id + AES-256-GCM)</li>
|
||||
<li>⚠ Vulnerable to device compromise</li>
|
||||
</ul>
|
||||
{mode === 'embedded' && isEmbeddedActive && <span className="mode-badge active">Active</span>}
|
||||
{mode === 'embedded' && isEmbeddedActive && (
|
||||
<span className="mode-badge active">Active</span>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{mode === 'nip46_bunker' && !canSwitchToBunker && (
|
||||
<Alert tone="warning" title="Cannot enable bunker">
|
||||
{hasProfile ? 'Unlock vault to enable bunker mode.' : 'No keypair found: Please import a key first.'}
|
||||
{hasProfile
|
||||
? 'Unlock vault to enable bunker mode.'
|
||||
: 'No keypair found: Please import a key first.'}
|
||||
</Alert>
|
||||
)}
|
||||
{mode === 'embedded' && !canSwitchToEmbedded && hasProfile && vaultLocked && (
|
||||
|
|
@ -364,7 +413,8 @@ export function SignerModeScreen() {
|
|||
)}
|
||||
{!hasProfile && mode !== 'nip46_client' && (
|
||||
<Alert tone="warning" title="No keypair">
|
||||
No keypair found: Please import a key first. (NIP-46 Client can be selected without a local key.)
|
||||
No keypair found: Please import a key first. (NIP-46 Client can be selected without
|
||||
a local key.)
|
||||
</Alert>
|
||||
)}
|
||||
{error && <ErrorText>{error}</ErrorText>}
|
||||
|
|
@ -379,12 +429,14 @@ export function SignerModeScreen() {
|
|||
<Badge tone="warning">{embeddedStatus!.pending.length}</Badge>
|
||||
</header>
|
||||
<div className="card-body">
|
||||
{(embeddedStatus!.pending).map((req, idx) => (
|
||||
{embeddedStatus!.pending.map((req, idx) => (
|
||||
<div key={req.id} className="signer-pending-item">
|
||||
<div className="signer-pending-info">
|
||||
<code className="mono">{req.method}</code>
|
||||
<p>{req.summary}</p>
|
||||
{req.details?.is_sensitive && <span className="sensitive-badge">Sensitive</span>}
|
||||
{req.details?.is_sensitive && (
|
||||
<span className="sensitive-badge">Sensitive</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="settings-inline">
|
||||
<Button variant="primary" onClick={() => void handleEmbeddedApprove(idx, true)}>
|
||||
|
|
@ -404,16 +456,24 @@ export function SignerModeScreen() {
|
|||
{(mode === 'nip46_client' || mode === 'nip46_bunker') && (
|
||||
<section className="card">
|
||||
<header className="card-header">
|
||||
<h2>{mode === 'nip46_client' ? 'External Signer Connection' : 'Bunker Connection'}</h2>
|
||||
<h2>
|
||||
{mode === 'nip46_client' ? 'External Signer Connection' : 'Bunker Connection'}
|
||||
</h2>
|
||||
</header>
|
||||
<div className="card-body">
|
||||
{isNip46Active && nip46StatusState ? (
|
||||
<div className="signer-actions">
|
||||
<p className="hint">
|
||||
Connected to <code className="mono">{nip46StatusState.signer_pubkey?.slice(0, 16)}…</code> via{' '}
|
||||
{(nip46StatusState.connected_relays?.length ?? 0)} of {(nip46StatusState.relays?.length ?? 0)} relays
|
||||
Connected to{' '}
|
||||
<code className="mono">{nip46StatusState.signer_pubkey?.slice(0, 16)}…</code>{' '}
|
||||
via {nip46StatusState.connected_relays?.length ?? 0} of{' '}
|
||||
{nip46StatusState.relays?.length ?? 0} relays
|
||||
</p>
|
||||
{nip46StatusState.error && <Alert tone="error" title="Connection error">{nip46StatusState.error}</Alert>}
|
||||
{nip46StatusState.error && (
|
||||
<Alert tone="error" title="Connection error">
|
||||
{nip46StatusState.error}
|
||||
</Alert>
|
||||
)}
|
||||
<Button variant="danger" onClick={() => void handleNip46Disconnect()}>
|
||||
<Icon name="trash" size={16} /> Disconnect
|
||||
</Button>
|
||||
|
|
@ -427,10 +487,16 @@ export function SignerModeScreen() {
|
|||
<p>{r.summary}</p>
|
||||
</div>
|
||||
<div className="settings-inline">
|
||||
<Button variant="primary" onClick={() => void handleNip46Approve(r.id, true)}>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => void handleNip46Approve(r.id, true)}
|
||||
>
|
||||
Approve
|
||||
</Button>
|
||||
<Button variant="danger" onClick={() => void handleNip46Approve(r.id, false)}>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => void handleNip46Approve(r.id, false)}
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -444,7 +510,11 @@ export function SignerModeScreen() {
|
|||
<div className="field">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={mode === 'nip46_client' ? 'nostrconnect://… (from Amber / Nostr Connect)' : 'nostrconnect://…'}
|
||||
placeholder={
|
||||
mode === 'nip46_client'
|
||||
? 'bunker://… or nostrconnect://… (from Amber / Nostr Connect)'
|
||||
: 'nostrconnect://…'
|
||||
}
|
||||
value={uri}
|
||||
onChange={(e) => setUri(e.target.value)}
|
||||
autoComplete="off"
|
||||
|
|
@ -452,17 +522,26 @@ export function SignerModeScreen() {
|
|||
/>
|
||||
<p className="hint">
|
||||
{mode === 'nip46_client'
|
||||
? 'In Amber / Nostr Connect, choose “Connect external app” and paste the nostrconnect:// link here.'
|
||||
? 'In Amber, open Connect and copy the bunker:// link. In other Nostr Connect apps, copy the nostrconnect:// link. Then paste it here.'
|
||||
: 'Share this with client apps that want to connect to this bunker.'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Label</label>
|
||||
<input value={label} onChange={(e) => setLabel(e.target.value)} placeholder="Remote Signer" />
|
||||
<input
|
||||
value={label}
|
||||
onChange={(e) => setLabel(e.target.value)}
|
||||
placeholder="Remote Signer"
|
||||
/>
|
||||
</div>
|
||||
{error && <ErrorText>{error}</ErrorText>}
|
||||
<div className="settings-inline">
|
||||
<Button variant="primary" loading={connecting} disabled={!uri.trim()} onClick={() => void handleNip46Connect()}>
|
||||
<Button
|
||||
variant="primary"
|
||||
loading={connecting}
|
||||
disabled={!uri.trim()}
|
||||
onClick={() => void handleNip46Connect()}
|
||||
>
|
||||
<Icon name="key" size={16} /> Connect
|
||||
</Button>
|
||||
<Button variant="ghost" onClick={() => void refreshStatus()} disabled={loading}>
|
||||
|
|
@ -482,15 +561,16 @@ export function SignerModeScreen() {
|
|||
<div className="card-body">
|
||||
<ul className="security-notes">
|
||||
<li>
|
||||
<strong>NIP-46 Client (Most Secure):</strong> Private key never on this device. External
|
||||
signer (hardware wallet / Amber) holds key.
|
||||
<strong>NIP-46 Client (Most Secure):</strong> Private key never on this device.
|
||||
External signer (hardware wallet / Amber) holds key.
|
||||
</li>
|
||||
<li>
|
||||
<strong>NIP-46 Bunker (Moderate):</strong> Key in this app's vault, you approve each
|
||||
remote request.
|
||||
<strong>NIP-46 Bunker (Moderate):</strong> Key in this app's vault, you approve
|
||||
each remote request.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Embedded (Least Secure):</strong> Local signing, key in memory when unlocked.
|
||||
<strong>Embedded (Least Secure):</strong> Local signing, key in memory when
|
||||
unlocked.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -284,8 +284,7 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
|||
[applyState],
|
||||
);
|
||||
const exportSecretKey = useCallback(
|
||||
(npub: string, password: string, reason: string) =>
|
||||
api.exportSecretKey(npub, password, reason),
|
||||
(npub: string, password: string, reason: string) => api.exportSecretKey(npub, password, reason),
|
||||
[],
|
||||
);
|
||||
const pickImages = useCallback(() => api.pickImages(), []);
|
||||
|
|
|
|||
|
|
@ -146,7 +146,9 @@ describe('exporting a secret key', () => {
|
|||
// Reopen — fields should be empty
|
||||
const dialog2 = await openExport(user);
|
||||
expect((within(dialog2).getByLabelText('Vault password') as HTMLInputElement).value).toBe('');
|
||||
expect((within(dialog2).getByLabelText('Reason for export') as HTMLInputElement).value).toBe('');
|
||||
expect((within(dialog2).getByLabelText('Reason for export') as HTMLInputElement).value).toBe(
|
||||
'',
|
||||
);
|
||||
});
|
||||
|
||||
it('shows an error for an incorrect password', async () => {
|
||||
|
|
@ -185,9 +187,7 @@ describe('exporting a secret key', () => {
|
|||
await user.click(within(dialog).getByRole('button', { name: 'Export' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
within(dialog).getByText(/not stored on this computer/),
|
||||
).toBeInTheDocument();
|
||||
expect(within(dialog).getByText(/not stored on this computer/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -226,9 +226,7 @@ describe('exporting a secret key', () => {
|
|||
await user.click(within(dialog).getByRole('button', { name: 'Export' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
within(dialog).getByText(/external signer/i),
|
||||
).toBeInTheDocument();
|
||||
expect(within(dialog).getByText(/external signer/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -445,10 +445,9 @@ export function createFakeBackend(initial?: AppState): FakeBackend {
|
|||
// Check profile exists first
|
||||
const profile = state.profiles.find((p) => p.npub === npub);
|
||||
if (!profile) {
|
||||
throw Object.assign(
|
||||
new Error('That profile is not stored on this computer.'),
|
||||
{ code: 'profile_not_found' },
|
||||
);
|
||||
throw Object.assign(new Error('That profile is not stored on this computer.'), {
|
||||
code: 'profile_not_found',
|
||||
});
|
||||
}
|
||||
|
||||
// External signer profiles cannot export secret keys
|
||||
|
|
@ -461,24 +460,19 @@ export function createFakeBackend(initial?: AppState): FakeBackend {
|
|||
|
||||
if (state.encrypted_storage) {
|
||||
if (!password) {
|
||||
throw Object.assign(
|
||||
new Error('Password required to export secret key.'),
|
||||
{ code: 'wrong_password' },
|
||||
);
|
||||
throw Object.assign(new Error('Password required to export secret key.'), {
|
||||
code: 'wrong_password',
|
||||
});
|
||||
}
|
||||
// Fake password check: accept "test" or "password"
|
||||
if (password !== 'test' && password !== 'password') {
|
||||
throw Object.assign(
|
||||
new Error('Wrong password.'),
|
||||
{ code: 'wrong_password' },
|
||||
);
|
||||
throw Object.assign(new Error('Wrong password.'), { code: 'wrong_password' });
|
||||
}
|
||||
}
|
||||
if (!reason) {
|
||||
throw Object.assign(
|
||||
new Error('A reason is required for key export.'),
|
||||
{ code: 'config' },
|
||||
);
|
||||
throw Object.assign(new Error('A reason is required for key export.'), {
|
||||
code: 'config',
|
||||
});
|
||||
}
|
||||
const hex = `${npub.slice(4)}0000000000000000000000000000000000`.slice(0, 64);
|
||||
return { hex, nsec: `nsec1${npub.slice(5)}` };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue