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:
Avi 2026-09-30 15:03:05 -05:00
commit d7cf2a5fda
5 changed files with 120 additions and 0 deletions

View file

@ -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'),

View file

@ -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,
],
);

View file

@ -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 = {