fix: retain native signer session through repeated public-key lookups
This commit is contained in:
@@ -40,3 +40,25 @@ Review found additional app-side concerns to test: production network failures
|
|||||||
can flip the app into mock mode and fabricate subscribed users, and the header
|
can flip the app into mock mode and fabricate subscribed users, and the header
|
||||||
can discard a valid backend Nostr session when the local signer account has not
|
can discard a valid backend Nostr session when the local signer account has not
|
||||||
been restored. Do not claim these repaired by the broker object-cloning fix.
|
been restored. Do not claim these repaired by the broker object-cloning fix.
|
||||||
|
|
||||||
|
## Live candidate qualification and restored-session prompting
|
||||||
|
|
||||||
|
2026-10-05: actual Yaya NIP-98 session exchange returns201 and authenticated
|
||||||
|
profile returns200 using candidate dashboard and app assets. The overlay hides;
|
||||||
|
a full refresh reuses the session and profile without another auth exchange.
|
||||||
|
Evidence: /tmp/archy-indeehub-full-candidate-2.log. This uses headless Chromium
|
||||||
|
390x844, real cookies/signatures/RPC/API, with only static candidate assets routed
|
||||||
|
locally. Initial asset-routing attempts hit Chromium private-network checks;
|
||||||
|
forwarding real API requests in the fixture resolved that test harness failure.
|
||||||
|
No backend authentication was mocked or bypassed.
|
||||||
|
|
||||||
|
Public-key lookups can inherit transient activation from the identity-picker
|
||||||
|
click/reload. The provider now avoids interpreting a restored-session hint or
|
||||||
|
already selected identity as a new account-switch request. Requests still pass
|
||||||
|
to the authenticated broker; the hint grants no signature permission. Explicit
|
||||||
|
selectIdentity remains available. Twelve provider/tab-signer regressions pass.
|
||||||
|
|
||||||
|
The combined frontend production check found strict TypeScript nullability
|
||||||
|
errors in Fleet test array indexing; the fixture now asserts both cards exist
|
||||||
|
and uses non-null indexing. No production Fleet behavior changed in that repair.
|
||||||
|
Actual deployment, companion lifecycle and the remaining recovery cases stay open.
|
||||||
|
|||||||
@@ -211,7 +211,16 @@
|
|||||||
selectedPublicKeyTimer = null;
|
selectedPublicKeyTimer = null;
|
||||||
return Promise.resolve(publicKey);
|
return Promise.resolve(publicKey);
|
||||||
}
|
}
|
||||||
if (navigator.userActivation && navigator.userActivation.isActive) {
|
// The picker click itself leaves transient user activation active. A second
|
||||||
|
// public-key lookup in the same login must not open another picker.
|
||||||
|
var restoringSession = false;
|
||||||
|
try {
|
||||||
|
var sessionHint = sessionStorage.getItem('nostr_token');
|
||||||
|
restoringSession = !!sessionHint && sessionHint.indexOf('mock-') !== 0;
|
||||||
|
} catch (_) {}
|
||||||
|
// This is only a UI hint: the broker still verifies the node session and
|
||||||
|
// signing permissions. A restored app token never authorizes a signature.
|
||||||
|
if (!restoringSession && !selectedIdentity && navigator.userActivation && navigator.userActivation.isActive) {
|
||||||
return selectIdentity().then(function () {
|
return selectIdentity().then(function () {
|
||||||
return getPublicKey();
|
return getPublicKey();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ describe('nostr-provider identity selection', () => {
|
|||||||
let providerWindow: ProviderWindow
|
let providerWindow: ProviderWindow
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
sessionStorage.clear()
|
||||||
providerWindow = window as ProviderWindow
|
providerWindow = window as ProviderWindow
|
||||||
delete providerWindow.__archipelagoNostr
|
delete providerWindow.__archipelagoNostr
|
||||||
delete providerWindow.nostr
|
delete providerWindow.nostr
|
||||||
@@ -122,6 +123,46 @@ describe('nostr-provider identity selection', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('does not reopen after a selected identity when activation survives account restoration', async () => {
|
||||||
|
const { frame, postMessage, signerOrigin } = loadProvider(true)
|
||||||
|
const selected = new MessageEvent('message', {
|
||||||
|
data: { type: 'archipelago:signer-identity', identity: { nostr_pubkey: 'selected-key' } },
|
||||||
|
origin: signerOrigin,
|
||||||
|
})
|
||||||
|
Object.defineProperty(selected, 'source', { value: frame.contentWindow })
|
||||||
|
window.dispatchEvent(selected)
|
||||||
|
await expect(providerWindow.nostr!.getPublicKey()).resolves.toBe('selected-key')
|
||||||
|
postMessage.mockClear()
|
||||||
|
const next = providerWindow.nostr!.getPublicKey()
|
||||||
|
expect(postMessage).not.toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ type: 'archipelago:signer-select-identity' }), expect.anything(),
|
||||||
|
)
|
||||||
|
const request = postMessage.mock.calls[0]![0] as { id: number }
|
||||||
|
const response = new MessageEvent('message', {
|
||||||
|
data: { type: 'nostr-response', id: request.id, result: 'selected-key' }, origin: signerOrigin,
|
||||||
|
})
|
||||||
|
Object.defineProperty(response, 'source', { value: frame.contentWindow })
|
||||||
|
window.dispatchEvent(response)
|
||||||
|
await expect(next).resolves.toBe('selected-key')
|
||||||
|
// Explicit account switching remains available after a successful login.
|
||||||
|
void providerWindow.archipelagoNostr!.selectIdentity()
|
||||||
|
expect(postMessage).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ type: 'archipelago:signer-select-identity', force: true }), signerOrigin,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not mistake reload activation for a new login while restoring a session', () => {
|
||||||
|
sessionStorage.setItem('nostr_token', 'test-session-hint')
|
||||||
|
const { postMessage, signerOrigin } = loadProvider(true)
|
||||||
|
void providerWindow.nostr!.getPublicKey()
|
||||||
|
expect(postMessage).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ type: 'nostr-request', method: 'getPublicKey' }), signerOrigin,
|
||||||
|
)
|
||||||
|
expect(postMessage).not.toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ type: 'archipelago:signer-select-identity' }), expect.anything(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it('parks the hidden broker off-screen and reuses it for the next request', async () => {
|
it('parks the hidden broker off-screen and reuses it for the next request', async () => {
|
||||||
const surface = {
|
const surface = {
|
||||||
expectPageTransition: vi.fn(),
|
expectPageTransition: vi.fn(),
|
||||||
|
|||||||
@@ -15,12 +15,13 @@ describe('fleet unavailable readings', () => {
|
|||||||
global: {mocks: {$ver: (v: string) => v}},
|
global: {mocks: {$ver: (v: string) => v}},
|
||||||
})
|
})
|
||||||
const cards = wrapper.findAll('.fleet-node-card')
|
const cards = wrapper.findAll('.fleet-node-card')
|
||||||
expect(cards[0].text()).toContain('0%')
|
expect(cards).toHaveLength(2)
|
||||||
expect(cards[0].text()).toContain('—')
|
expect(cards[0]!.text()).toContain('0%')
|
||||||
expect(cards[0].text()).toContain('Offline · last seen 2d ago')
|
expect(cards[0]!.text()).toContain('—')
|
||||||
expect(cards[1].text()).toContain('Status unknown')
|
expect(cards[0]!.text()).toContain('Offline · last seen 2d ago')
|
||||||
expect(cards[1].text()).not.toContain('0%')
|
expect(cards[1]!.text()).toContain('Status unknown')
|
||||||
expect(cards[1].text()).toContain('Uptime unavailable')
|
expect(cards[1]!.text()).not.toContain('0%')
|
||||||
|
expect(cards[1]!.text()).toContain('Uptime unavailable')
|
||||||
expect(wrapper.text()).not.toContain('NaN')
|
expect(wrapper.text()).not.toContain('NaN')
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user