diff --git a/CHECKPOINT-encryption.md b/CHECKPOINT-encryption.md index 8c5ac00..3691a54 100644 --- a/CHECKPOINT-encryption.md +++ b/CHECKPOINT-encryption.md @@ -1,64 +1,3 @@ -# Checkpoint — Step 4 finished: interactive kind scope at approval (2026-10-01) - -## Where things are -- Project: `/home/avi/Projects/Keynctr`, branch `master` @ **`d09c4ec`** - ("feat(signer): interactive kind scope in the approval prompt (Step 4 finish)"). - Previous: `8ffeb42` + `d1622a0` (checkpoint updates, pushed), `83f5940` - (placeholder kind-0 fix), `e8d2abb` (grant kind-editing UI). -- Working tree: clean for tracked files (always-untracked: COSMIC_THEME.md, - icon jpeg, deferred/). Push status in Next steps. -- Release binary rebuilt from d09c4ec at 13:30 (real 30s build, mtime verified). - -## What was completed (user-facing) -**Step 4 remainder — kind scope chosen AT approval time:** -1. Signer screen and Signer Mode screen: on a pending `sign_event` - request, "Always allow…" opens an inline kind editor prefilled with the - request's own event kind; Allow records the grant with exactly the - edited kinds (sorted+deduped, empty = all kinds = explicit broadening), - Cancel leaves the request pending. Non-signing methods keep the plain - "Always allow" button (no kind dimension). -2. Backend: `Nip46Approve`/`SignerApprove` gained `grant_kinds` - (`serde(default) Option>` — old payloads stay valid). - `respond_to_approval_with_always(.., grant_kinds)`: `Some(list)` stores - the edited scope verbatim; `None` keeps the old fallback (kind of the - request being approved). Legacy vault rows untouched. -3. `nip46_status_as_bunker_json` now includes each pending request's - `details`, so the legacy Signer screen can prefill the editor too. -4. Shared `parseKindsInput()` in `lib/permissions.ts` (used by both the - approval editor and the existing grant editor); grant editor refactored - onto it — behaviour unchanged. -5. Latent bug fixed: `AppProvider.signerApprove` dropped the `always` - argument, so the legacy "Always allow" button never recorded a grant. - -## Commits this session (newest first) -- `d09c4ec` feat(signer): interactive kind scope in the approval prompt (Step 4 finish) -- `8ffeb42` docs(checkpoint): 8070dfc..d1622a0 pushed to origin/master -- (earlier today) `d1622a0`, `83f5940`, `e8d2abb` — see checkpoint below - -## Verification (all green, 2026-10-01) -- `cargo test` → 227 unit + 6 e2e, 0 failed · `cargo clippy --all-targets` → 0 warnings -- `cargo fmt --check` clean · `cargo build --release` rebuilt 13:30 from d09c4ec -- frontend: vitest 148/19 files (6 new: 3 approval-editor tests, 3 - parseKindsInput units); `typecheck`, `lint`, `format:check`, `build`, - `electron:build` all green. - -## How to verify in the app -1. Fully quit and relaunch Keynctr (new backend, 13:30). -2. Have the connected app request a signature → Signer screen → - "Always allow…" on the pending request → edit kinds → Allow. - The "Always-allow permissions" card shows the scoped grant; a later - request of an UNcovered kind prompts again. -3. e2e-mechanics are covered by the 3 new SignerScreen tests. - -## Next steps -- PUSHED 2026-10-01 after this checkpoint (see git remote readback); - token per-use, not stored (revoke when convenient). -- User live pass: relaunch, rename npub1p437… + Publish name, try the new - approval-time kind editor with Amber. -- Optional: remove /tmp/kn-base worktree when done. - ---- - # Checkpoint — placeholder kind-0 fix + grant editing UI (2026-10-01) ## Where things are diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 6b2de5b..4aa17f7 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -127,25 +127,15 @@ export const api = { nip46Disconnect: () => call('nip46_disconnect'), nip46CancelPairing: () => call('nip46_cancel_pairing'), nip46Status: () => call('nip46_status'), - nip46Approve: (id: string, approved: boolean, always = false, grantKinds?: number[]) => - call('nip46_approve', { - id, - approved, - always, - grant_kinds: grantKinds ?? null, - }), + nip46Approve: (id: string, approved: boolean, always = false) => + call('nip46_approve', { id, approved, always }), // Legacy NIP-46 bunker (deprecated, kept for compatibility) signerConnect: (uri: string) => call('signer_connect', { uri }), signerDisconnect: () => call('signer_disconnect'), signerStatus: () => call('signer_status'), - signerApprove: (id: string, approved: boolean, always = false, grantKinds?: number[]) => - call('signer_approve', { - id, - approved, - always, - grant_kinds: grantKinds ?? null, - }), + signerApprove: (id: string, approved: boolean, always = false) => + call('signer_approve', { id, approved, always }), // Standing "always allow" grants for apps using us as their signer. signerGrantsList: () => call('signer_grants_list'), diff --git a/frontend/src/lib/permissions.ts b/frontend/src/lib/permissions.ts index 814c54a..aed1d04 100644 --- a/frontend/src/lib/permissions.ts +++ b/frontend/src/lib/permissions.ts @@ -50,20 +50,3 @@ export function formatExpiry(expiresAt?: number): string | null { if (Number.isNaN(date.getTime())) return null; return date.toLocaleString(); } - -/** Parse a comma-separated event-kind list typed by the user. - * Returns the kinds (sorted, de-duplicated) or an error naming the first - * non-numeric token. An EMPTY input yields an empty list — the explicit - * "all kinds" broadening, matching the backend's representation. */ -export type KindsParseResult = { ok: true; kinds: number[] } | { ok: false; error: string }; - -export function parseKindsInput(input: string): KindsParseResult { - const parts = input - .split(',') - .map((s) => s.trim()) - .filter((s) => s.length > 0); - const bad = parts.find((s) => !/^\d+$/.test(s)); - if (bad !== undefined) return { ok: false, error: `“${bad}” is not an event kind number.` }; - const kinds = [...new Set(parts.map(Number))].sort((a, b) => a - b); - return { ok: true, kinds }; -} diff --git a/frontend/src/screens/SignerModeScreen.tsx b/frontend/src/screens/SignerModeScreen.tsx index f15f7dd..51a46c8 100644 --- a/frontend/src/screens/SignerModeScreen.tsx +++ b/frontend/src/screens/SignerModeScreen.tsx @@ -5,13 +5,8 @@ import { Badge } from '../components/Badge'; import { Button } from '../components/Button'; import { ErrorText } from '../components/ErrorText'; import { Icon } from '../components/Icon'; -import { declaredPermissionRows, formatExpiry, parseKindsInput } from '../lib/permissions'; -import type { - SignerMode, - EmbeddedSignerStatus, - Nip46SignerStatus, - PendingApproval, -} from '../lib/types'; +import { declaredPermissionRows, formatExpiry } from '../lib/permissions'; +import type { SignerMode, EmbeddedSignerStatus, Nip46SignerStatus } from '../lib/types'; import { useApp } from '../state/AppProvider'; export function SignerModeScreen() { @@ -237,10 +232,10 @@ export function SignerModeScreen() { ); const handleNip46Approve = useCallback( - async (id: string, approved: boolean, always = false, grantKinds?: number[]) => { + async (id: string, approved: boolean, always = false) => { setError(null); try { - const status = await nip46Approve(id, approved, always, grantKinds); + const status = await nip46Approve(id, approved, always); setNip46StatusState(status); } catch (err) { setError(err instanceof Error ? err.message : String(err)); @@ -249,28 +244,6 @@ export function SignerModeScreen() { [nip46Approve], ); - // Interactive kind scope at approval time (same pattern as SignerScreen): - // "Always allow…" on a sign_event request opens a kind editor prefilled - // with the request's own kind before the grant is recorded. - const [alwaysDraft, setAlwaysDraft] = useState<{ id: string; kinds: string } | null>(null); - const [alwaysError, setAlwaysError] = useState(null); - - const startAlwaysAllow = (request: PendingApproval) => { - setAlwaysError(null); - setAlwaysDraft({ id: request.id, kinds: String(request.details?.event_kind ?? '') }); - }; - - const onSaveAlwaysAllow = async (request: PendingApproval) => { - if (!alwaysDraft) return; - const parsed = parseKindsInput(alwaysDraft.kinds); - if (!parsed.ok) { - setAlwaysError(parsed.error); - return; - } - await handleNip46Approve(request.id, true, true, parsed.kinds); - setAlwaysDraft(null); - }; - const modeBadge = () => { if (mode === 'nip46_client') { return isNip46Active ? ( @@ -601,67 +574,34 @@ export function SignerModeScreen() { {(nip46StatusState?.pending_approvals?.length ?? 0) > 0 && (

Pending ({nip46StatusState!.pending_approvals!.length})

- {(nip46StatusState?.pending_approvals ?? []).map((r) => { - const editingAlways = alwaysDraft?.id === r.id; - return ( -
-
- {r.method} -

{r.summary}

- {editingAlways && ( -
- - setAlwaysDraft({ id: r.id, kinds: e.target.value }) - } - /> - - -
- )} - {editingAlways && alwaysError && {alwaysError}} -
-
- - {r.method === 'sign_event' && !editingAlways ? ( - - ) : ( - - )} - -
+ {(nip46StatusState!.pending_approvals ?? []).map((r) => ( +
+
+ {r.method} +

{r.summary}

- ); - })} +
+ + + +
+
+ ))}
)}
diff --git a/frontend/src/screens/SignerScreen.tsx b/frontend/src/screens/SignerScreen.tsx index f4390ec..1248028 100644 --- a/frontend/src/screens/SignerScreen.tsx +++ b/frontend/src/screens/SignerScreen.tsx @@ -5,8 +5,8 @@ import { Button } from '../components/Button'; import { ErrorText } from '../components/ErrorText'; import { Icon } from '../components/Icon'; import { shortHexId } from '../lib/format'; -import { grantLabel, parseKindsInput } from '../lib/permissions'; -import type { PendingApproval, SignerGrant, SignerStatus } from '../lib/types'; +import { grantLabel } from '../lib/permissions'; +import type { SignerGrant, SignerStatus } from '../lib/types'; import { useApp } from '../state/AppProvider'; const EMPTY_STATUS: SignerStatus = { @@ -99,15 +99,10 @@ export function SignerScreen() { } }; - const onApprove = async ( - id: string, - approved: boolean, - always = false, - grantKinds?: number[], - ) => { + const onApprove = async (id: string, approved: boolean, always = false) => { setError(null); try { - setStatus(await signerApprove(id, approved, always, grantKinds)); + setStatus(await signerApprove(id, approved, always)); setGrants(await signerGrantsList()); } catch (err) { setError(err instanceof Error ? err.message : String(err)); @@ -132,28 +127,6 @@ export function SignerScreen() { const grantKey = (g: SignerGrant) => `${g.app_pubkey}:${g.method}`; - // Interactive kind scope at APPROVAL time: "Always allow…" on a sign_event - // request opens a kind editor prefilled with the request's own kind, so the - // grant's scope is chosen while the user sees the event, not only after. - const [alwaysDraft, setAlwaysDraft] = useState<{ id: string; kinds: string } | null>(null); - const [alwaysError, setAlwaysError] = useState(null); - - const startAlwaysAllow = (request: PendingApproval) => { - setAlwaysError(null); - setAlwaysDraft({ id: request.id, kinds: String(request.details?.event_kind ?? '') }); - }; - - const onSaveAlwaysAllow = async (request: PendingApproval) => { - if (!alwaysDraft) return; - const parsed = parseKindsInput(alwaysDraft.kinds); - if (!parsed.ok) { - setAlwaysError(parsed.error); - return; - } - await onApprove(request.id, true, true, parsed.kinds); - setAlwaysDraft(null); - }; - const startEditGrant = (grant: SignerGrant) => { setKindError(null); setGrantDraft({ key: grantKey(grant), kinds: (grant.allowed_kinds ?? []).join(', ') }); @@ -161,14 +134,18 @@ export function SignerScreen() { const onSaveKinds = async (grant: SignerGrant) => { if (!grantDraft) return; - const parsed = parseKindsInput(grantDraft.kinds); - if (!parsed.ok) { - setKindError(parsed.error); + const parts = grantDraft.kinds + .split(',') + .map((s) => s.trim()) + .filter((s) => s.length > 0); + const bad = parts.find((s) => !/^\d+$/.test(s)); + if (bad !== undefined) { + setKindError(`“${bad}” is not an event kind number.`); return; } setError(null); try { - await signerGrantUpdate(grant.app_pubkey, grant.method, parsed.kinds); + await signerGrantUpdate(grant.app_pubkey, grant.method, parts.map(Number)); setGrants(await signerGrantsList()); setGrantDraft(null); setKindError(null); @@ -275,62 +252,31 @@ export function SignerScreen() { The connected app wants to do the following with the active profile's keys. Review each one before approving it.

- {status.pending.map((request) => { - const editingAlways = alwaysDraft?.id === request.id; - return ( -
-
- {request.method} -

{request.summary}

- {editingAlways && ( -
- - setAlwaysDraft({ id: request.id, kinds: e.target.value }) - } - /> - - -
- )} - {editingAlways && alwaysError && {alwaysError}} -
-
- - {request.method === 'sign_event' && !editingAlways ? ( - - ) : ( - - )} - -
+ {status.pending.map((request) => ( +
+
+ {request.method} +

{request.summary}

- ); - })} +
+ + + +
+
+ ))}
)} diff --git a/frontend/src/state/AppProvider.tsx b/frontend/src/state/AppProvider.tsx index ed27939..0a154f8 100644 --- a/frontend/src/state/AppProvider.tsx +++ b/frontend/src/state/AppProvider.tsx @@ -86,22 +86,12 @@ interface AppContextValue { nip46Disconnect: () => Promise; nip46CancelPairing: () => Promise; nip46Status: () => Promise; - nip46Approve: ( - id: string, - approved: boolean, - always?: boolean, - grantKinds?: number[], - ) => Promise; + nip46Approve: (id: string, approved: boolean, always?: boolean) => Promise; // Legacy NIP-46 bunker (deprecated) signerConnect: (uri: string) => Promise; signerDisconnect: () => Promise; signerStatus: () => Promise; - signerApprove: ( - id: string, - approved: boolean, - always?: boolean, - grantKinds?: number[], - ) => Promise; + signerApprove: (id: string, approved: boolean, always?: boolean) => Promise; signerGrantsList: () => Promise; signerGrantRevoke: (appPubkey: string, grantMethod: string) => Promise<{ removed: boolean }>; signerGrantUpdate: ( @@ -311,8 +301,7 @@ export function AppProvider({ children }: { children: ReactNode }) { const nip46Status = useCallback(() => api.nip46Status(), []); const nip46PairStart = useCallback((label: string) => api.nip46PairStart(label), []); const nip46Approve = useCallback( - (id: string, approved: boolean, always = false, grantKinds?: number[]) => - api.nip46Approve(id, approved, always, grantKinds), + (id: string, approved: boolean, always = false) => api.nip46Approve(id, approved, always), [], ); @@ -351,11 +340,9 @@ export function AppProvider({ children }: { children: ReactNode }) { const signerConnect = useCallback((uri: string) => api.signerConnect(uri), []); const signerDisconnect = useCallback(() => api.signerDisconnect(), []); const signerStatus = useCallback(() => api.signerStatus(), []); - const signerApprove = useCallback( - (id: string, approved: boolean, always = false, grantKinds?: number[]) => - api.signerApprove(id, approved, always, grantKinds), - [], - ); + const signerApprove = useCallback((id: string, approved: boolean) => { + return api.signerApprove(id, approved); + }, []); const deleteProfile = useCallback( (npub: string) => applyState(api.deleteProfile(npub)), diff --git a/frontend/src/test/SignerScreen.test.tsx b/frontend/src/test/SignerScreen.test.tsx index 96ae105..bad7146 100644 --- a/frontend/src/test/SignerScreen.test.tsx +++ b/frontend/src/test/SignerScreen.test.tsx @@ -245,133 +245,4 @@ describe('SignerScreen', () => { }); expect(await screen.findByText('Sign events — all kinds')).toBeInTheDocument(); }); - - it('edits the kind scope when always-allowing a sign_event request', async () => { - const backend = createFakeBackend(); - installFakeBackend(backend); - const user = userEvent.setup(); - renderWithApp(); - - backend.setSigner({ - phase: 'connected', - peer: 'ab12', - relays: ['wss://relay.damus.io'], - connectedRelays: ['wss://relay.damus.io'], - error: null, - pending: [ - { - id: 'req-9', - method: 'sign_event', - summary: 'Sign event kind 1: “Scoped always”', - details: { - method: 'sign_event', - summary: 'Sign event kind 1', - event_kind: 1, - destination_relays: [], - content_preview: 'Scoped always', - is_sensitive: false, - }, - }, - ], - }); - - expect(await screen.findByText(/Scoped always/, {}, { timeout: 3000 })).toBeInTheDocument(); - // sign_event offers the interactive scope editor, prefilled with the - // request's own kind. - await user.click(screen.getByRole('button', { name: /Always allow…/ })); - const input = screen.getByLabelText('Event kinds to always allow'); - expect(input).toHaveValue('1'); - await user.clear(input); - await user.type(input, '1, 30023'); - await user.click(screen.getByRole('button', { name: 'Allow' })); - - await waitFor(() => { - expect( - backend.requests.some( - (r) => - r.method === 'signer_approve' && - r.params?.id === 'req-9' && - r.params?.approved === true && - r.params?.always === true && - JSON.stringify(r.params?.grant_kinds) === '[1,30023]', - ), - ).toBe(true); - }); - expect(backend.signerGrants).toEqual([ - { app_pubkey: 'ab12', method: 'sign_event', allowed_kinds: [1, 30023] }, - ]); - }); - - it('rejects a non-numeric kind in the approval scope editor before calling the backend', async () => { - const backend = createFakeBackend(); - installFakeBackend(backend); - const user = userEvent.setup(); - renderWithApp(); - - backend.setSigner({ - phase: 'connected', - peer: 'ab12', - relays: ['wss://relay.damus.io'], - connectedRelays: ['wss://relay.damus.io'], - error: null, - pending: [ - { - id: 'req-10', - method: 'sign_event', - summary: 'Sign event kind 1: “Bad scope”', - details: { - method: 'sign_event', - summary: 'Sign event kind 1', - event_kind: 1, - destination_relays: [], - content_preview: 'Bad scope', - is_sensitive: false, - }, - }, - ], - }); - - await screen.findByText(/Bad scope/, {}, { timeout: 3000 }); - await user.click(screen.getByRole('button', { name: /Always allow…/ })); - const input = screen.getByLabelText('Event kinds to always allow'); - await user.clear(input); - await user.type(input, 'one'); - await user.click(screen.getByRole('button', { name: 'Allow' })); - - expect(await screen.findByText(/one.*not an event kind/)).toBeInTheDocument(); - expect(backend.requests.some((r) => r.method === 'signer_approve')).toBe(false); - }); - - it('non-signing methods keep the plain Always allow button', async () => { - const backend = createFakeBackend(); - installFakeBackend(backend); - const user = userEvent.setup(); - renderWithApp(); - - backend.setSigner({ - phase: 'connected', - peer: 'ab12', - relays: ['wss://relay.damus.io'], - connectedRelays: ['wss://relay.damus.io'], - error: null, - pending: [{ id: 'req-11', method: 'nip44_decrypt', summary: 'Decrypt a message' }], - }); - - await screen.findByText('Decrypt a message', {}, { timeout: 3000 }); - // No kind editor for methods without a kind dimension. - expect(screen.queryByRole('button', { name: /Always allow…/ })).not.toBeInTheDocument(); - await user.click(screen.getByRole('button', { name: 'Always allow' })); - - await waitFor(() => { - expect( - backend.requests.some( - (r) => - r.method === 'signer_approve' && r.params?.id === 'req-11' && r.params?.always === true, - ), - ).toBe(true); - }); - expect(backend.signerGrants).toEqual([ - { app_pubkey: 'ab12', method: 'nip44_decrypt', allowed_kinds: [] }, - ]); - }); }); diff --git a/frontend/src/test/fakeBackend.ts b/frontend/src/test/fakeBackend.ts index ab80789..bf47169 100644 --- a/frontend/src/test/fakeBackend.ts +++ b/frontend/src/test/fakeBackend.ts @@ -234,36 +234,8 @@ export function createFakeBackend(initial?: AppState): FakeBackend { backend.setNip46(next); return next; } - case 'nip46_approve': { - const id = String(params.id ?? ''); - const entry = backend.nip46.pending_approvals?.find((r) => r.id === id); - const next = { - ...backend.nip46, - pending_approvals: (backend.nip46.pending_approvals ?? []).filter((r) => r.id !== id), - }; - backend.setNip46(next); - // Mirrors respond_to_approval_with_always: an always-allow on a - // sign_event grant is kind-scoped — grant_kinds as edited, else the - // kind of the request being approved. - if (params.approved === true && params.always === true && entry) { - const kinds = - (params.grant_kinds as number[] | null | undefined) ?? - (entry.method === 'sign_event' && entry.details?.event_kind != null - ? [entry.details.event_kind] - : []); - const normalised = [...new Set(kinds)].sort((a, b) => a - b); - const peer = backend.nip46.signer_pubkey ?? ''; - if ( - !backend.signerGrants.some((g) => g.app_pubkey === peer && g.method === entry.method) - ) { - backend.signerGrants = [ - ...backend.signerGrants, - { app_pubkey: peer, method: entry.method, allowed_kinds: normalised }, - ]; - } - } - return next; - } + case 'nip46_approve': + return backend.nip46; case 'create_profile': { const label = String(params.label ?? ''); @@ -439,26 +411,10 @@ export function createFakeBackend(initial?: AppState): FakeBackend { }; backend.setSigner(next); if (params.approved === true && params.always === true && entry) { - // Mirrors respond_to_approval_with_always: grant_kinds as edited, - // else the kind of the request being approved (sign_event only). - const kinds = - (params.grant_kinds as number[] | null | undefined) ?? - (entry.method === 'sign_event' && entry.details?.event_kind != null - ? [entry.details.event_kind] - : []); - const normalised = [...new Set(kinds)].sort((a, b) => a - b); - if ( - !backend.signerGrants.some( - (g) => g.app_pubkey === (backend.signer.peer ?? '') && g.method === entry.method, - ) - ) { + if (!backend.signerGrants.some((g) => g.method === entry.method)) { backend.signerGrants = [ ...backend.signerGrants, - { - app_pubkey: backend.signer.peer ?? '', - method: entry.method, - allowed_kinds: normalised, - }, + { app_pubkey: backend.signer.peer ?? '', method: entry.method }, ]; } } diff --git a/frontend/src/test/permissions.test.ts b/frontend/src/test/permissions.test.ts index a930d51..d5d6f08 100644 --- a/frontend/src/test/permissions.test.ts +++ b/frontend/src/test/permissions.test.ts @@ -3,7 +3,6 @@ import { declaredPermissionRows, formatExpiry, grantLabel, - parseKindsInput, permissionLabel, } from '../lib/permissions'; @@ -52,20 +51,3 @@ describe('formatExpiry', () => { expect(formatExpiry(undefined)).toBeNull(); }); }); - -describe('parseKindsInput', () => { - it('parses, de-duplicates and sorts a kind list', () => { - expect(parseKindsInput('30023, 1,1')).toEqual({ ok: true, kinds: [1, 30023] }); - }); - - it('empty input is the explicit all-kinds list', () => { - expect(parseKindsInput(' , ')).toEqual({ ok: true, kinds: [] }); - expect(parseKindsInput('')).toEqual({ ok: true, kinds: [] }); - }); - - it('names the first non-numeric token', () => { - const r = parseKindsInput('1, hello, 7'); - expect(r.ok).toBe(false); - if (!r.ok) expect(r.error).toMatch(/hello/); - }); -}); diff --git a/src/ipc.rs b/src/ipc.rs index ebce82f..dc0789c 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -187,16 +187,12 @@ pub enum Request { Nip46Status, /// Approve/reject a pending NIP-46 request. `always = true` additionally /// records a standing grant so this peer's future requests of the same - /// method run without prompting. `grant_kinds` (with `always`) scopes a - /// `sign_event` grant to the given event kinds as edited in the approval - /// UI; `None` falls back to the kind of the request being approved. + /// method run without prompting. Nip46Approve { id: String, approved: bool, #[serde(default)] always: bool, - #[serde(default)] - grant_kinds: Option>, }, /// ===== LEGACY NIP-46 BUNKER (server mode) ===== /// Start the NIP-46 remote signer for a `nostrconnect://` link (acting as bunker). @@ -218,10 +214,6 @@ pub enum Request { /// grant for this peer + method. #[serde(default)] always: bool, - /// Kind scope for the grant (with `always`), as edited in the - /// approval UI; `None` falls back to the approved request's kind. - #[serde(default)] - grant_kinds: Option>, }, /// List standing "always allow" grants for apps using us as signer. SignerGrantsList, @@ -569,7 +561,6 @@ fn nip46_status_as_bunker_json(status: &crate::signer::types::Nip46Status) -> se "id": p.id, "method": p.method, "summary": p.summary, - "details": p.details, })).collect::>(), }) } @@ -703,13 +694,12 @@ async fn run(app: &Arc>, request: Request) -> Result { let Some(signer) = ensure_nip46_signer(app).await else { return Err(AppError::config("NIP-46 signer not initialized")); }; signer - .respond_to_approval_with_always(&id, approved, always, grant_kinds) + .respond_to_approval_with_always(&id, approved, always) .await?; let status = signer.status().await; Ok(json!(status)) @@ -753,13 +743,12 @@ async fn run(app: &Arc>, request: Request) -> Result { let guard = app.lock().await; if guard.signer_mode == SignerMode::Nip46Client && guard.nip46_signer.is_some() { if let Some(signer) = &guard.nip46_signer { signer - .respond_to_approval_with_always(&id, approved, always, grant_kinds) + .respond_to_approval_with_always(&id, approved, always) .await?; let status = signer.status().await; return Ok(nip46_status_as_bunker_json(&status)); diff --git a/src/signer/nip46_client.rs b/src/signer/nip46_client.rs index eeb88ca..9b220f1 100644 --- a/src/signer/nip46_client.rs +++ b/src/signer/nip46_client.rs @@ -1404,7 +1404,7 @@ impl Nip46ClientSigner { /// Approve or reject a pending request. pub async fn respond_to_approval(&self, id: &str, approved: bool) -> Result<(), AppError> { - self.respond_to_approval_with_always(id, approved, false, None) + self.respond_to_approval_with_always(id, approved, false) .await } @@ -1416,7 +1416,6 @@ impl Nip46ClientSigner { id: &str, approved: bool, always: bool, - grant_kinds: Option>, ) -> Result<(), AppError> { let (entry, peer_hex) = { let mut inner = self.inner.lock().await; @@ -1434,28 +1433,13 @@ impl Nip46ClientSigner { (entry, peer) }; if approved && always && !peer_hex.is_empty() { - // The scope of an always-allow grant: - // - `grant_kinds = Some(list)` — the user edited the scope in the - // approval UI; normalize (sorted + de-duped) and store verbatim. - // An empty Some list broadens to all kinds, the same explicit - // act the grant editor requires, never the result of omission. - // - `grant_kinds = None` — fall back to the request the user - // actually saw: a "sign_event" always-allow covers only that - // event's kind; other gated methods have no kind dimension. - let kinds: Vec = match grant_kinds { - Some(list) => { - let mut normalised = list; - normalised.sort_unstable(); - normalised.dedup(); - normalised - } - None => { - if entry.method == "sign_event" { - entry.details.event_kind.into_iter().collect() - } else { - Vec::new() - } - } + // A "sign_event" always-allow covers only the kinds of the + // request the user actually saw — never other kinds. Other + // gated methods have no kind dimension. + let kinds: Vec = if entry.method == "sign_event" { + entry.details.event_kind.into_iter().collect() + } else { + Vec::new() }; let mut app = self.app.lock().await; app.vault