From 44a3334f994c4589fe55fb0a78f2aa51381ef592 Mon Sep 17 00:00:00 2001 From: archipelago Date: Fri, 9 Oct 2026 05:49:47 -0400 Subject: [PATCH 1/2] docs: clear resolved Framework startup blocker --- AGENTS.md | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d7ce1f8e..6a971618 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,27 +1,3 @@ -# Blocking incident: Framework LND startup and false zero balance - -Before starting or resuming work in this repository, read -`docs/incident-framework-lnd-startup.md` and check its status. - -The user explicitly required this incident to take priority over later work. -While its status is OPEN: - -- Surface this blocker at session start, including when the user asks for unrelated work. -- Prioritize investigation and repair on the actual Framework node. If access is - unavailable, ask for it and continue useful offline investigation; defer unrelated - work unless the user explicitly overrides this priority. -- A manual LND restart, a source patch, passing local tests, or publishing an OTA - does not resolve the incident. Do not mark it fixed until the Framework's startup, - Receive flow, and balance behavior are verified on the node, including a controlled - reboot with access and recovery arrangements in place. -- Preserve wallet identity, wallet/channel databases, credentials, and backups. - Never run wallet wipe/recreation as an automatic investigation or recovery step. -- Record evidence, changes, validation, and remaining work in the incident document. - -This priority comes from the user's explicit instruction on 2026-09-15. It remains -in effect across sessions until the documented acceptance criteria are met or the -user explicitly changes it. - ## Unit tests on a live node Run backend unit tests through `scripts/test-backend-isolated.sh`. Do not run From b537009198c0a8cc8d6c5c23193d95c0b59c05a7 Mon Sep 17 00:00:00 2001 From: archipelago Date: Fri, 9 Oct 2026 05:49:47 -0400 Subject: [PATCH 2/2] fix(auth): make IndeeHub sign-out forget app selection --- neode-ui/public/nostr-provider.js | 9 +++++++++ .../src/components/AppLauncherOverlay.vue | 11 +++++++++++ neode-ui/src/views/AppSession.vue | 1 + neode-ui/src/views/NostrTabSigner.vue | 16 ++++++++++++++++ .../__tests__/NostrTabSigner.test.ts | 19 +++++++++++++++++++ .../__tests__/nostrProviderIdentity.test.ts | 5 +++++ .../__tests__/useAppIdentity.test.ts | 17 +++++++++++++++++ .../src/views/appSession/useAppIdentity.ts | 8 ++++++++ 8 files changed, 86 insertions(+) diff --git a/neode-ui/public/nostr-provider.js b/neode-ui/public/nostr-provider.js index 4e23f894..bc75fad7 100644 --- a/neode-ui/public/nostr-provider.js +++ b/neode-ui/public/nostr-provider.js @@ -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) { diff --git a/neode-ui/src/components/AppLauncherOverlay.vue b/neode-ui/src/components/AppLauncherOverlay.vue index 302e987e..0636c7d7 100644 --- a/neode-ui/src/components/AppLauncherOverlay.vue +++ b/neode-ui/src/components/AppLauncherOverlay.vue @@ -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) diff --git a/neode-ui/src/views/AppSession.vue b/neode-ui/src/views/AppSession.vue index ddd62134..163f1787 100644 --- a/neode-ui/src/views/AppSession.vue +++ b/neode-ui/src/views/AppSession.vue @@ -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) } diff --git a/neode-ui/src/views/NostrTabSigner.vue b/neode-ui/src/views/NostrTabSigner.vue index 2532065f..5d6daa33 100644 --- a/neode-ui/src/views/NostrTabSigner.vue +++ b/neode-ui/src/views/NostrTabSigner.vue @@ -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) { 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 } diff --git a/neode-ui/src/views/appSession/__tests__/NostrTabSigner.test.ts b/neode-ui/src/views/appSession/__tests__/NostrTabSigner.test.ts index 90a4612e..65f9560c 100644 --- a/neode-ui/src/views/appSession/__tests__/NostrTabSigner.test.ts +++ b/neode-ui/src/views/appSession/__tests__/NostrTabSigner.test.ts @@ -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> = [] diff --git a/neode-ui/src/views/appSession/__tests__/nostrProviderIdentity.test.ts b/neode-ui/src/views/appSession/__tests__/nostrProviderIdentity.test.ts index 813ecebf..15655e8a 100644 --- a/neode-ui/src/views/appSession/__tests__/nostrProviderIdentity.test.ts +++ b/neode-ui/src/views/appSession/__tests__/nostrProviderIdentity.test.ts @@ -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) diff --git a/neode-ui/src/views/appSession/__tests__/useAppIdentity.test.ts b/neode-ui/src/views/appSession/__tests__/useAppIdentity.test.ts index 717f31af..5ee03ac2 100644 --- a/neode-ui/src/views/appSession/__tests__/useAppIdentity.test.ts +++ b/neode-ui/src/views/appSession/__tests__/useAppIdentity.test.ts @@ -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) + }) }) diff --git a/neode-ui/src/views/appSession/useAppIdentity.ts b/neode-ui/src/views/appSession/useAppIdentity.ts index a906488a..4c2a50df 100644 --- a/neode-ui/src/views/appSession/useAppIdentity.ts +++ b/neode-ui/src/views/appSession/useAppIdentity.ts @@ -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, } }