diff --git a/shared/src/wallet/core/internal-wallet.ts b/shared/src/wallet/core/internal-wallet.ts index 851a797..6cbb61f 100644 --- a/shared/src/wallet/core/internal-wallet.ts +++ b/shared/src/wallet/core/internal-wallet.ts @@ -12,6 +12,7 @@ import { type Fr } from "@aztec/aztec.js/fields"; import type { AccountType } from "../database/wallet-db"; import { WalletInteraction, + WalletUpdateEvent, type WalletInteractionType, } from "../types/wallet-interaction"; @@ -356,6 +357,7 @@ export class InternalWallet extends BaseNativeWallet { requestedCapabilities?: GrantedCapability[], ): Promise { await this.db.storeCapabilityGrants(appId, granted, requestedCapabilities); + this.emitCapabilityChange(); } async revokeCapability( @@ -363,6 +365,7 @@ export class InternalWallet extends BaseNativeWallet { capability: GrantedCapability, ): Promise { await this.db.revokeCapability(appId, capability); + this.emitCapabilityChange(); } async updateAccountAuthorization( @@ -370,6 +373,7 @@ export class InternalWallet extends BaseNativeWallet { accounts: Aliased[], ): Promise { await this.db.updateAccountAuthorization(appId, accounts); + this.emitCapabilityChange(); } async updateAddressBookAuthorization( @@ -377,13 +381,32 @@ export class InternalWallet extends BaseNativeWallet { contacts: Aliased[], ): Promise { await this.db.updateAddressBookAuthorization(appId, contacts); + this.emitCapabilityChange(); } async revokeAuthorization(key: string): Promise { await this.db.revokeAuthorization(key); + this.emitCapabilityChange(); } async revokeAppAuthorizations(appId: string): Promise { await this.db.revokeAppAuthorizations(appId); + this.emitCapabilityChange(); + } + + /** + * Emit a wallet-update event so cookie sync picks up capability changes + * made through the Apps tab UI (storeCapabilityGrants, revoke, etc.). + */ + private emitCapabilityChange(): void { + const interaction = WalletInteraction.from({ + type: "capabilityChange" as any, + status: "SUCCESS", + complete: true, + title: "Capability change", + }); + this.interactionManager.dispatchEvent( + new WalletUpdateEvent(interaction), + ); } } diff --git a/shared/src/wallet/database/wallet-db.ts b/shared/src/wallet/database/wallet-db.ts index 711592c..5dfec8c 100644 --- a/shared/src/wallet/database/wallet-db.ts +++ b/shared/src/wallet/database/wallet-db.ts @@ -1160,33 +1160,46 @@ export class WalletDB { /** * Import authorization entries from a portable structure into the DB. - * Additive: merges with existing entries (new entries overwrite conflicting keys). - * Used to bootstrap capability grants from cookies. + * Full overwrite: replaces local entries with cookie data for each app present in the import. + * Keys present in the cookie but missing locally are created. + * Keys present locally but missing in the cookie are deleted (the cookie is authoritative). + * Used to bootstrap capability grants from cookies at PXE init. */ async importAllAuthorizations( apps: Array<{ appId: string; entries: Record }>, ): Promise { - let imported = 0; + let updated = 0; for (const { appId, entries } of apps) { + const prefix = `${appId}:`; + + // Collect existing keys for this app so we can delete stale ones + const existingKeys = new Set(); + for await (const key of this.authorizations.keysAsync({ start: prefix, end: `${prefix}\uffff` })) { + existingKeys.add(key); + } + + // Write all entries from the cookie for (const [storageKey, value] of Object.entries(entries)) { const fullKey = `${appId}:${storageKey}`; - // Only import keys that don't already exist locally. - // This prevents stale cookie data from overwriting local revocations. - const existing = await this.authorizations.getAsync(fullKey); - if (!existing) { - await this.authorizations.set( - fullKey, - Buffer.from(jsonStringify(value)), - ); - imported++; - } + await this.authorizations.set( + fullKey, + Buffer.from(jsonStringify(value)), + ); + existingKeys.delete(fullKey); // Mark as seen + updated++; + } + + // Delete local keys that are no longer in the cookie (revoked on the other side) + for (const staleKey of existingKeys) { + await this.authorizations.delete(staleKey); + updated++; } } this.logger.info( - `Imported ${imported} new authorization entries for ${apps.length} app(s)`, + `Imported authorization data for ${apps.length} app(s) (${updated} entries processed)`, ); - return imported; + return updated; } } diff --git a/web/src/wallet/wallet-service.ts b/web/src/wallet/wallet-service.ts index 6353dd6..611cb10 100644 --- a/web/src/wallet/wallet-service.ts +++ b/web/src/wallet/wallet-service.ts @@ -382,6 +382,9 @@ async function bootstrapContactsFromCookie( // transaction fully commits (same issue as account bootstrap). await new Promise(resolve => setTimeout(resolve, 0)); await db.storeSender(address, contact.alias); + // Yield again before registerSender — it internally calls getAccounts() + // which iterates PXE's keystore, colliding with the storeSender tx above. + await new Promise(resolve => setTimeout(resolve, 0)); await pxe.registerSender(address); imported++; }