Merge branch 'pr/fix/indeehub-signout-uat-20261009(8648b736)'
Demo images / Build & push demo images (push) Failing after 38s

This commit is contained in:
archipelago
2026-10-09 05:52:06 -04:00
9 changed files with 86 additions and 24 deletions
-24
View File
@@ -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
+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,
}
}