fix(auth): make IndeeHub sign-out forget app selection
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user