fix(ui): https app launches and the nostr bridge follow the frame's real origin

Three launcher/bridge defects combined to make HTTPS dashboards look
broken while HTTP ones worked:

1. portAuth() looked the launch port up under the name the user clicks
   ('mempool-web', 'lnd', 'bitcoin-knots'…), but the signed catalog
   declares those ports under the manifest id that owns them
   (archy-mempool-web, lnd-ui, bitcoin-ui). The lookup missed,
   portIsGateFronted answered false, and an HTTPS dashboard handed app
   frames http:// URLs — blocked as mixed content: mempool and IndeeHub
   'did not connect', bitcoin knots/core opened http:// in a new tab.
   Resolution now follows launch aliases, then a port-wide catalog scan
   that only answers when every declarer of that port agrees (a port
   any app publishes as plain HTTP is never upgraded to https).

2. The signed-catalog cache was only warmed by the Store/Discover
   views, so a user who went straight to My Apps launched apps with an
   empty cache. Warmed at dashboard mount now — fetchAppCatalog()
   already memoizes with a 1h TTL.

3. The NIP-07 bridge compared event.origin for strict equality with the
   recorded (http) app URL and replied to the recorded URL as the
   postMessage targetOrigin — both break the moment a frame is scheme-
   upgraded (cached HSTS did exactly that): every nostr request was
   silently dropped and replies to the stale origin threw. The bridge
   now matches host+port (scheme deliberately ignored) and always
   replies to event.origin — the frame's real origin.

Unit tests cover alias resolution (incl. bitcoin-knots→8334→https),
the conservative port-scan, and scheme-agnostic sender matching.
This commit is contained in:
archipelago
2026-09-01 10:29:05 -04:00
parent e382e679ae
commit 3347b8b8b9
7 changed files with 191 additions and 27 deletions
@@ -1,11 +1,15 @@
/** Composable for NIP-07 Nostr signing bridge between parent and iframe */
/** Composable for NIP-07 Nostr signing between parent and iframe apps.
*
* Replies always target event.origin — the frame's REAL origin. The app's
* recorded URL can carry a stale scheme (HSTS-upgraded http app on an HTTPS
* dashboard); targeting it makes postMessage throw and the app never sees
* its response. */
import { rpcClient } from '@/api/rpc-client'
import type { SelectedIdentity } from './useAppIdentity'
export function useNostrBridge(
getStoredIdentity: () => SelectedIdentity | null,
getAppUrl: () => string,
) {
async function handleNostrRequest(event: MessageEvent) {
const { id, method, params } = event.data
@@ -43,14 +47,15 @@ export function useNostrBridge(
else if (method === 'nip44.encrypt') { result = (await rpcClient.call<{ ciphertext: string }>({ method: 'identity.nostr-encrypt-nip44', params: { id: identityId || undefined, pubkey: params.pubkey, plaintext: params.plaintext } })).ciphertext }
else if (method === 'nip44.decrypt') { result = (await rpcClient.call<{ plaintext: string }>({ method: 'identity.nostr-decrypt-nip44', params: { id: identityId || undefined, pubkey: params.pubkey, ciphertext: params.ciphertext } })).plaintext }
else { throw new Error(`Unsupported NIP-07 method: ${method}`) }
const url = getAppUrl()
const targetOrigin = url ? new URL(url).origin : '*'
source.postMessage({ type: 'nostr-response', id, result }, targetOrigin)
// Reply to the sender's REAL origin, never to the stored app URL:
// a scheme-upgraded frame (HSTS, or any future upgrade) makes the
// stored http:// URL a stale targetOrigin — postMessage then throws
// and the app never receives its response. nostr sign-in on IndeeHub
// over HTTPS died exactly there (2026-09-01).
source.postMessage({ type: 'nostr-response', id, result }, event.origin || '*')
} catch (err) {
if (import.meta.env.DEV) console.error(`[NIP-07] ${method} FAILED:`, err instanceof Error ? err.message : err)
const url = getAppUrl()
const targetOrigin = url ? new URL(url).origin : '*'
source.postMessage({ type: 'nostr-response', id, error: err instanceof Error ? err.message : 'Unknown error' }, targetOrigin)
source.postMessage({ type: 'nostr-response', id, error: err instanceof Error ? err.message : 'Unknown error' }, event.origin || '*')
}
}