feat(signer): grant kind-editing backend (vault + IPC + api plumbing)
Step 4 remainder, first half: Vault::update_signer_grant_kinds replaces a standing grant's kind scope (sorted, deduped; empty = all kinds, a deliberate broadening matching legacy semantics). New signer_grant_update IPC + api/AppProvider/fakeBackend plumbing. The Signer-screen editor UI is the next unit; no UI surface calls the RPC yet.
This commit is contained in:
parent
ec8b515d05
commit
d7cf2a5fda
5 changed files with 120 additions and 0 deletions
|
|
@ -144,6 +144,12 @@ export const api = {
|
|||
app_pubkey: appPubkey,
|
||||
grant_method: grantMethod,
|
||||
}),
|
||||
signerGrantUpdate: (appPubkey: string, grantMethod: string, grantKinds: number[]) =>
|
||||
call<{ updated: boolean }>('signer_grant_update', {
|
||||
app_pubkey: appPubkey,
|
||||
grant_method: grantMethod,
|
||||
grant_kinds: grantKinds,
|
||||
}),
|
||||
|
||||
deleteProfile: (npub: string) => call<AppState>('delete_profile', { npub }),
|
||||
undoDelete: () => call<AppState>('undo_delete'),
|
||||
|
|
|
|||
|
|
@ -94,6 +94,11 @@ interface AppContextValue {
|
|||
signerApprove: (id: string, approved: boolean, always?: boolean) => Promise<SignerStatus>;
|
||||
signerGrantsList: () => Promise<SignerGrant[]>;
|
||||
signerGrantRevoke: (appPubkey: string, grantMethod: string) => Promise<{ removed: boolean }>;
|
||||
signerGrantUpdate: (
|
||||
appPubkey: string,
|
||||
grantMethod: string,
|
||||
grantKinds: number[],
|
||||
) => Promise<{ updated: boolean }>;
|
||||
deleteProfile: (npub: string) => Promise<AppState>;
|
||||
undoDelete: () => Promise<AppState>;
|
||||
clearLastDeleted: () => void;
|
||||
|
|
@ -305,6 +310,11 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
|||
(appPubkey: string, grantMethod: string) => api.signerGrantRevoke(appPubkey, grantMethod),
|
||||
[],
|
||||
);
|
||||
const signerGrantUpdate = useCallback(
|
||||
(appPubkey: string, grantMethod: string, grantKinds: number[]) =>
|
||||
api.signerGrantUpdate(appPubkey, grantMethod, grantKinds),
|
||||
[],
|
||||
);
|
||||
|
||||
const setVaultPassword = useCallback(
|
||||
(currentPassword: string | null, newPassword: string) =>
|
||||
|
|
@ -398,6 +408,7 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
|||
signerApprove,
|
||||
signerGrantsList,
|
||||
signerGrantRevoke,
|
||||
signerGrantUpdate,
|
||||
deleteProfile,
|
||||
undoDelete,
|
||||
publishProfileMetadata,
|
||||
|
|
@ -459,6 +470,7 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
|||
signerApprove,
|
||||
signerGrantsList,
|
||||
signerGrantRevoke,
|
||||
signerGrantUpdate,
|
||||
copyText,
|
||||
],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -434,6 +434,25 @@ export function createFakeBackend(initial?: AppState): FakeBackend {
|
|||
return { removed: backend.signerGrants.length < before };
|
||||
}
|
||||
|
||||
case 'signer_grant_update': {
|
||||
const app = String(params.app_pubkey ?? '');
|
||||
const method = String(params.grant_method ?? '');
|
||||
// Mirrors Vault::update_signer_grant_kinds: normalise (sort + dedupe)
|
||||
// and replace the kinds of every matching grant.
|
||||
const kinds = [...((params.grant_kinds as number[]) ?? [])]
|
||||
.sort((a, b) => a - b)
|
||||
.filter((k, i, arr) => i === 0 || k !== arr[i - 1]);
|
||||
let updated = false;
|
||||
backend.signerGrants = backend.signerGrants.map((g) => {
|
||||
if (g.app_pubkey === app && g.method === method) {
|
||||
updated = true;
|
||||
return { ...g, allowed_kinds: kinds };
|
||||
}
|
||||
return g;
|
||||
});
|
||||
return { updated };
|
||||
}
|
||||
|
||||
case 'relay_add': {
|
||||
const url = String(params.url);
|
||||
const nextSettings: Settings = {
|
||||
|
|
|
|||
25
src/ipc.rs
25
src/ipc.rs
|
|
@ -223,6 +223,16 @@ pub enum Request {
|
|||
app_pubkey: String,
|
||||
grant_method: String,
|
||||
},
|
||||
/// Edit the kind scope of a standing `sign_event` grant (interactive
|
||||
/// grant editing). `grant_kinds` replaces the covered kinds verbatim
|
||||
/// after normalising; an EMPTY list means "all kinds", so broadening is
|
||||
/// a deliberate act, never the result of an omitted field.
|
||||
SignerGrantUpdate {
|
||||
app_pubkey: String,
|
||||
grant_method: String,
|
||||
#[serde(default)]
|
||||
grant_kinds: Vec<u16>,
|
||||
},
|
||||
DeleteProfile {
|
||||
npub: String,
|
||||
},
|
||||
|
|
@ -766,6 +776,21 @@ async fn run(app: &Arc<Mutex<App>>, request: Request) -> Result<serde_json::Valu
|
|||
}
|
||||
Ok(json!({ "removed": removed }))
|
||||
}
|
||||
Request::SignerGrantUpdate {
|
||||
app_pubkey,
|
||||
grant_method,
|
||||
grant_kinds,
|
||||
} => {
|
||||
let mut guard = app.lock().await;
|
||||
let updated =
|
||||
guard
|
||||
.vault
|
||||
.update_signer_grant_kinds(&app_pubkey, &grant_method, &grant_kinds);
|
||||
if updated {
|
||||
guard.save_vault()?;
|
||||
}
|
||||
Ok(json!({ "updated": updated }))
|
||||
}
|
||||
|
||||
// Network-only requests (no shared state lock)
|
||||
Request::RelayTest { url } => {
|
||||
|
|
|
|||
58
src/vault.rs
58
src/vault.rs
|
|
@ -260,6 +260,33 @@ impl Vault {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Replace the kind scope of an existing grant (interactive grant
|
||||
/// editing). Returns whether a matching grant was updated.
|
||||
///
|
||||
/// `allowed_kinds` is normalised (sorted, de-duplicated). An EMPTY list
|
||||
/// means "all kinds" — the same semantics a legacy grant carries — so
|
||||
/// broadening to all kinds is a deliberate editor action, never the
|
||||
/// result of omission. Grants for non-signing methods always keep an
|
||||
/// empty list; editing one is a no-op on the kinds field by construction.
|
||||
pub fn update_signer_grant_kinds(
|
||||
&mut self,
|
||||
app_pubkey: &str,
|
||||
method: &str,
|
||||
allowed_kinds: &[u16],
|
||||
) -> bool {
|
||||
let mut normalised: Vec<u16> = allowed_kinds.to_vec();
|
||||
normalised.sort_unstable();
|
||||
normalised.dedup();
|
||||
let mut found = false;
|
||||
for g in self.signer_grants.iter_mut() {
|
||||
if g.app_pubkey == app_pubkey && g.method == method {
|
||||
g.allowed_kinds = normalised.clone();
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
found
|
||||
}
|
||||
|
||||
/// Drop a standing grant; returns whether one was removed.
|
||||
pub fn revoke_signer_grant(&mut self, app_pubkey: &str, method: &str) -> bool {
|
||||
let before = self.signer_grants.len();
|
||||
|
|
@ -877,6 +904,37 @@ mod tests {
|
|||
assert!(vault.has_signer_grant("aa", "nip44_decrypt", None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signer_grant_kinds_can_be_edited() {
|
||||
let mut vault = Vault::empty();
|
||||
vault.grant_signer_method("aa", "sign_event", &[1]).unwrap();
|
||||
vault
|
||||
.grant_signer_method("aa", "nip44_decrypt", &[])
|
||||
.unwrap();
|
||||
|
||||
// Narrowing: add kind 30023 (normalised: sorted + de-duplicated).
|
||||
assert!(vault.update_signer_grant_kinds("aa", "sign_event", &[30023, 1, 1]));
|
||||
assert!(vault.has_signer_grant("aa", "sign_event", Some(1)));
|
||||
assert!(vault.has_signer_grant("aa", "sign_event", Some(30023)));
|
||||
assert!(!vault.has_signer_grant("aa", "sign_event", Some(6)));
|
||||
let g = vault
|
||||
.signer_grants
|
||||
.iter()
|
||||
.find(|g| g.method == "sign_event")
|
||||
.unwrap();
|
||||
assert_eq!(g.allowed_kinds, vec![1, 30023]);
|
||||
|
||||
// Editing an unknown (app, method) reports false and changes nothing.
|
||||
assert!(!vault.update_signer_grant_kinds("zz", "sign_event", &[1]));
|
||||
assert_eq!(vault.signer_grants.len(), 2);
|
||||
|
||||
// Explicitly broadening to ALL kinds is the empty list — the same
|
||||
// representation legacy grants use.
|
||||
assert!(vault.update_signer_grant_kinds("aa", "sign_event", &[]));
|
||||
assert!(vault.has_signer_grant("aa", "sign_event", Some(6)));
|
||||
assert!(vault.has_signer_grant("aa", "sign_event", None));
|
||||
}
|
||||
|
||||
static COUNTER: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
fn temp_vault_path() -> PathBuf {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue