fix(auth): make IndeeHub sign-out forget app selection

This commit is contained in:
archipelago
2026-10-09 05:49:47 -04:00
parent 44a3334f99
commit b537009198
8 changed files with 86 additions and 0 deletions
+9
View File
@@ -194,6 +194,15 @@
try {
['nostr_token', 'nostr_pubkey', 'refresh_token'].forEach(function (key) { sessionStorage.removeItem(key); });
} catch (_) {}
// Signing out is also an explicit request to stop the dashboard from
// restoring this app's remembered identity on its next load. The broker
// forgets only the app-to-identity selection; node identities and keys are
// preserved and remain available for a later explicit sign-in.
postToSigner({
type: embedded
? 'archipelago:identity:clear'
: 'archipelago:signer-clear-session',
});
}
function scheduleIdentityAuth(pubkey) {
@@ -312,6 +312,13 @@ function storeIdentity(appUrl: string, identity: SelectedIdentity) {
} catch { /* ignore */ }
}
function clearStoredIdentity(appUrl: string) {
try {
const key = IDENTITY_STORAGE_KEY + appUrl.replace(/[^a-z0-9]/gi, '_')
localStorage.removeItem(key)
} catch { /* ignore */ }
}
/** Handle identity selection from the picker */
function onIdentitySelected(identity: SelectedIdentity) {
showIdentityPicker.value = false
@@ -522,6 +529,10 @@ function onMessage(e: MessageEvent) {
if (e.data?.force === true) showIdentityPicker.value = true
else sendIdentityIfSupported()
}
if (e.data?.type === 'archipelago:identity:clear' && store.isOpen && e.source === iframeRef.value?.contentWindow) {
if (store.url) clearStoredIdentity(store.url)
showIdentityPicker.value = false
}
// Wallet connect — app requests a payment
if (e.data?.type === 'archipelago:payment-request' && store.isOpen) {
handlePaymentRequest(e)
+1
View File
@@ -569,6 +569,7 @@ function onMessage(e: MessageEvent) {
void registrationBridge.handle(e); void rentalBridge.handle(e)
if (e.data?.type === 'nostr-request') nostrBridge.handleNostrRequest(e)
if (e.data?.type === 'archipelago:identity:request') identity.handleIdentityRequest(e.data?.force === true)
if (e.data?.type === 'archipelago:identity:clear') identity.clearRememberedIdentity()
if (e.data?.type === 'archipelago:media:playing') screensaverStore.suppress(screensaverReason.value)
if (e.data?.type === 'archipelago:media:idle') screensaverStore.resume(screensaverReason.value)
}
+16
View File
@@ -57,6 +57,11 @@ function storeIdentity(identity: SelectedIdentity) {
try { localStorage.setItem(`archipelago_app_identity_${appId.value}`, JSON.stringify(identity)) } catch {}
}
function clearStoredIdentity() {
if (!appId.value) return
try { localStorage.removeItem(`archipelago_app_identity_${appId.value}`) } catch {}
}
function parentPost(message: Record<string, unknown>) {
window.parent.postMessage(message, appOrigin.value || '*')
}
@@ -204,6 +209,17 @@ function onMessage(event: MessageEvent) {
return
}
if (data.type === 'archipelago:signer-clear-session' && appId.value && event.origin === appOrigin.value) {
clearStoredIdentity()
queuedRequests.splice(0)
showIdentityPicker.value = false
bridge.cancelPending()
registrationBridge.cancel()
rentalBridge.cancel()
hideSigner()
return
}
if (data.type === 'archipelago-rental-request' && appId.value && event.origin === appOrigin.value) {
void rentalBridge.handle(event); return
}
@@ -66,6 +66,25 @@ describe('NostrTabSigner visibility', () => {
expect(document.body.classList.contains('nostr-signer-route')).toBe(false)
})
it('forgets the remembered app selection when the app signs out', async () => {
localStorage.setItem('archipelago_app_identity_indeedhub', JSON.stringify({
id: 'identity-a', name: 'Alice', nostr_pubkey: 'a'.repeat(64),
}))
localStorage.setItem('unrelated-node-identity', 'preserved')
const wrapper = shallowMount(NostrTabSigner)
try {
parentMessage({ type: 'archipelago:signer-init', appId: 'indeedhub', appName: 'IndeeHub' })
parentMessage({ type: 'archipelago:signer-clear-session' })
await flushPromises()
expect(localStorage.getItem('archipelago_app_identity_indeedhub')).toBeNull()
expect(localStorage.getItem('unrelated-node-identity')).toBe('preserved')
expect(wrapper.findComponent(NostrIdentityPicker).props('show')).toBe(false)
} finally {
wrapper.unmount()
}
})
it('hands off a reactive picker identity as cloneable public fields and releases the overlay', async () => {
vi.useFakeTimers()
const sent: Array<Record<string, unknown>> = []
@@ -482,10 +482,15 @@ describe('nostr-provider identity selection', () => {
vi.stubGlobal('fetch', fetchMock)
const { frame, signerOrigin, postMessage } = loadProvider(false)
choose(frame, signerOrigin, key)
postMessage.mockClear()
providerWindow.archipelagoNostr!.clearSession()
await vi.advanceTimersByTimeAsync(1600)
expect(fetchMock).not.toHaveBeenCalled()
expect(providerWindow.archipelagoNostr!.getSelectedIdentity()).toBeNull()
expect(postMessage).toHaveBeenCalledWith(
{ type: 'archipelago:signer-clear-session' },
signerOrigin,
)
const selection = providerWindow.archipelagoNostr!.selectIdentity()
expect(postMessage).toHaveBeenCalledWith(expect.objectContaining({ type: 'archipelago:signer-select-identity', force: true }), signerOrigin)
choose(frame, signerOrigin, key)
@@ -49,4 +49,21 @@ describe('useAppIdentity explicit identity selection', () => {
expect(showPicker.value).toBe(false)
expect(postMessage).toHaveBeenCalledWith({ type: 'archipelago:identity-cancelled' }, '*')
})
it('forgets automatic app selection without deleting the node identity', () => {
localStorage.setItem('archipelago_app_identity_archipelago-source', JSON.stringify(alice))
localStorage.setItem('unrelated-node-identity', 'preserved')
const showPicker = ref(true)
const identity = useAppIdentity(
ref('archipelago-source'),
ref(null),
showPicker,
)
identity.clearRememberedIdentity()
expect(identity.getStoredIdentity()).toBeNull()
expect(localStorage.getItem('unrelated-node-identity')).toBe('preserved')
expect(showPicker.value).toBe(false)
})
})
@@ -35,6 +35,13 @@ export function useAppIdentity(
try { localStorage.setItem(IDENTITY_KEY + appId.value, JSON.stringify(identity)) } catch {}
}
/** Forget only this app's automatic identity choice. Saved node identities
* and their keys remain intact for a later explicit sign-in. */
function clearRememberedIdentity() {
try { localStorage.removeItem(IDENTITY_KEY + appId.value) } catch {}
showIdentityPicker.value = false
}
async function sendIdentity(identity: SelectedIdentity) {
try {
const challenge = `archipelago-identity:${Date.now()}`
@@ -86,6 +93,7 @@ export function useAppIdentity(
onIdentitySelected,
onIframeLoadIdentity,
handleIdentityRequest,
clearRememberedIdentity,
cancelIdentitySelection,
}
}