fix: clone public identity fields before cross-frame signer handoff

This commit is contained in:
archipelago
2026-10-05 21:11:03 -04:00
parent 041f1fa2d3
commit 6f098cd9c2
4 changed files with 108 additions and 3 deletions
+13 -1
View File
@@ -74,7 +74,19 @@ function hideSigner(delay = 0) {
}
function sendIdentity(identity: SelectedIdentity) {
parentPost({ type: 'archipelago:signer-identity', identity })
// The picker emits a Vue reactive object. Browser postMessage cannot clone
// that proxy: sending it closes the picker, then throws before the broker
// can hide, leaving a blank full-screen surface. Send public scalar fields
// explicitly, also keeping any unrelated identity metadata out of the app.
const publicIdentity: SelectedIdentity = {
id: identity.id,
name: identity.name,
did: identity.did,
pubkey: identity.pubkey,
nostr_pubkey: identity.nostr_pubkey,
nostr_npub: identity.nostr_npub,
}
parentPost({ type: 'archipelago:signer-identity', identity: publicIdentity })
}
const bridge = useNostrBridge(getStoredIdentity, {
@@ -1,5 +1,7 @@
import { shallowMount } from '@vue/test-utils'
import { flushPromises, shallowMount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { reactive } from 'vue'
import NostrIdentityPicker from '@/components/NostrIdentityPicker.vue'
import NostrTabSigner from '@/views/NostrTabSigner.vue'
describe('NostrTabSigner visibility', () => {
@@ -8,7 +10,7 @@ describe('NostrTabSigner visibility', () => {
window.history.replaceState({}, '', '/nostr-signer')
})
afterEach(() => vi.restoreAllMocks())
afterEach(() => { vi.restoreAllMocks(); vi.useRealTimers() })
function parentMessage(data: Record<string, unknown>) {
const event = new MessageEvent('message', { data, origin: window.location.origin })
@@ -46,4 +48,33 @@ describe('NostrTabSigner visibility', () => {
expect(document.documentElement.classList.contains('nostr-signer-route')).toBe(false)
expect(document.body.classList.contains('nostr-signer-route')).toBe(false)
})
it('hands off a reactive picker identity as cloneable public fields and releases the overlay', async () => {
vi.useFakeTimers()
const sent: Array<Record<string, unknown>> = []
// Real cross-frame postMessage performs structured cloning. A normal spy
// would miss the browser-only DataCloneError that leaves a grey overlay.
vi.spyOn(window.parent, 'postMessage').mockImplementation((message) => {
sent.push(structuredClone(message))
})
const wrapper = shallowMount(NostrTabSigner)
parentMessage({type: 'archipelago:signer-init', appId: 'indeedhub', appName: 'IndeeHub'})
const selected = reactive({
id: 'identity-a', name: 'Alice', did: 'did:key:test', pubkey: 'public-node-key',
nostr_pubkey: 'a'.repeat(64), nostr_npub: 'npub-test', internal_metadata: 'must-stay-local',
})
expect(() => structuredClone(selected)).toThrow()
wrapper.findComponent(NostrIdentityPicker).vm.$emit('select', selected)
await flushPromises()
const identityMessage = sent.find(message => message.type === 'archipelago:signer-identity')
expect(identityMessage?.identity).toEqual({
id: 'identity-a', name: 'Alice', did: 'did:key:test', pubkey: 'public-node-key',
nostr_pubkey: 'a'.repeat(64), nostr_npub: 'npub-test',
})
await vi.advanceTimersByTimeAsync(400)
expect(sent).toContainEqual({type: 'archipelago:signer-hide'})
expect(wrapper.findComponent(NostrIdentityPicker).props('show')).toBe(false)
wrapper.unmount()
})
})