feat: add persisted owner guards and events

This commit is contained in:
2026-08-22 16:55:00 +02:00
parent 1dbf5788f9
commit 69fbdb68ee
2 changed files with 37 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
// Mutators must not let a still-running old tab overwrite namespaces after a
// successor has installed its saved-key owner marker. Ownerless data may only
// move through explicit migration APIs after their own proof checks succeed.
import {savedKeyOwnerId} from '../keys'
export class WalletOwnerMismatchError extends Error {
override readonly name = 'WalletOwnerMismatchError'
constructor() {
super('The active wallet owner no longer matches the saved wallet.')
}
}
export const savedKeyOwnerAllows = (ownerId: string): boolean => {
return savedKeyOwnerId() === ownerId
}
export const assertSavedKeyOwner = (ownerId: string): void => {
if (!savedKeyOwnerAllows(ownerId)) throw new WalletOwnerMismatchError()
}
@@ -0,0 +1,17 @@
// Storage events only wake an owning store to reread its saved-key marker.
// Their payload may be stale when writes arrive faster than delivery.
export const LINKING_KEY_STORAGE_KEY = 'sattle_linking_key'
// Each subscription binds its own storage listener and removes exactly it on
// unsubscribe: no shared reference counting, so an abandoned subscriber can
// never keep another subscriber's window listener (or runtime) alive.
export const onSavedKeyStorageChange = (listener: () => void): (() => void) => {
const target = typeof window === 'undefined' ? null : window
const onStorage = (event: StorageEvent): void => {
if (event.key !== LINKING_KEY_STORAGE_KEY && event.key !== null) return
listener()
}
target?.addEventListener('storage', onStorage)
return () => target?.removeEventListener('storage', onStorage)
}