diff --git a/neode-ui/src/api/rpc-client.ts b/neode-ui/src/api/rpc-client.ts index 6bb70fff..ad766c4d 100644 --- a/neode-ui/src/api/rpc-client.ts +++ b/neode-ui/src/api/rpc-client.ts @@ -1084,6 +1084,16 @@ class RPCClient { relay_count: number first_seen: string nostr_pubkey: string + /** + * Whether the author proved control of the key their `author.did` names. + * `invalid` manifests are dropped during discovery and should never + * appear here; typed anyway so the UI fails safe rather than falling + * through to "signed" if that ever changes. + */ + signature?: + | { status: 'valid' } + | { status: 'missing' } + | { status: 'invalid'; reason: string } }> relay_count: number }> { diff --git a/neode-ui/src/views/Marketplace.vue b/neode-ui/src/views/Marketplace.vue index b88158af..3235ab1a 100644 --- a/neode-ui/src/views/Marketplace.vue +++ b/neode-ui/src/views/Marketplace.vue @@ -306,6 +306,11 @@ async function loadNostrMarketplace() { trustScore: app.trust_score, trustTier: app.trust_tier, relayCount: app.relay_count, + // Default to `missing` rather than leaving it undefined: a node running + // an older backend returns no field at all, and "we couldn't check" must + // never render as "signed". + signature: app.signature ?? { status: 'missing' as const }, + authorDid: app.manifest.author.did, })) } catch (e) { nostrError.value = e instanceof Error ? e.message : 'Discovery failed' diff --git a/neode-ui/src/views/marketplace/MarketplaceAppCard.vue b/neode-ui/src/views/marketplace/MarketplaceAppCard.vue index ebd06de9..94515a05 100644 --- a/neode-ui/src/views/marketplace/MarketplaceAppCard.vue +++ b/neode-ui/src/views/marketplace/MarketplaceAppCard.vue @@ -32,7 +32,7 @@ -
+
{{ app.trustTier }} + + + + + + {{ signatureLabel }} + + Score: {{ app.trustScore }}/100 · {{ app.relayCount }} relay{{ app.relayCount !== 1 ? 's' : '' }}
@@ -175,6 +200,35 @@ defineEmits<{ launch: [app: MarketplaceApp] }>() +const signatureLabel = computed(() => { + switch (props.app.signature?.status) { + case 'valid': return 'signed' + case 'invalid': return 'bad signature' + default: return 'unsigned' + } +}) + +/** + * The badge is two words; the tooltip carries the meaning. "signed" is easy to + * read as "safe", so say what was actually proven — that the author holds the + * key their DID names — and nothing more. + */ +const signatureTooltip = computed(() => { + const sig = props.app.signature + const did = props.app.authorDid + const shortDid = did && did.length > 24 ? `${did.slice(0, 16)}…${did.slice(-6)}` : did + switch (sig?.status) { + case 'valid': + return `Authorship verified: signed by the key ${shortDid ?? 'in author.did'}. ` + + 'This proves who published it, not that the app is safe.' + case 'invalid': + return `Signature did not verify: ${sig.reason}` + default: + return 'No author signature — the publisher\'s identity is unproven. ' + + 'The app may still be fine; nothing has been demonstrated about who wrote it.' + } +}) + const installProgressMessage = computed(() => { const p = props.installProgress if (!p) return 'Installing' diff --git a/neode-ui/src/views/marketplace/marketplaceData.ts b/neode-ui/src/views/marketplace/marketplaceData.ts index 99e54e96..5cecd729 100644 --- a/neode-ui/src/views/marketplace/marketplaceData.ts +++ b/neode-ui/src/views/marketplace/marketplaceData.ts @@ -29,8 +29,20 @@ export interface MarketplaceApp { trustScore?: number trustTier?: string relayCount?: number + /** + * DID-signature verdict for relay-discovered apps. `undefined` for curated + * and local apps, which don't travel through the marketplace protocol at all. + */ + signature?: AppSignature + /** The `author.did` the signature was checked against, for the tooltip. */ + authorDid?: string } +export type AppSignature = + | { status: 'valid' } + | { status: 'missing' } + | { status: 'invalid'; reason: string } + export type AppScreenshot = string | { src: string alt?: string