From cd5d7daeae246c8295753c876dfe1c1a5d47a01f Mon Sep 17 00:00:00 2001 From: archipelago Date: Sat, 8 Aug 2026 06:00:19 -0400 Subject: [PATCH] feat(marketplace-ui): show whether an app's authorship was actually proven MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The backend verifies DID signatures as of f0c289a4, but the card only rendered `trust_tier` / `trust_score`, so the verdict reached the frontend and died there. Adds a badge next to the existing trust pill. Deliberately a *separate* badge rather than folding into the trust tier: the score blends relay count, provenance and policy compliance, while this answers one narrow question — did the author prove control of the key their `author.did` names. Merging them would hide the distinction that the signature layer exists to draw. - `valid` → green "signed" with a lock glyph - `missing` → neutral grey "unsigned" (an unsigned publisher is unproven, not hostile, so it reads as absence rather than alarm) - `invalid` → red "bad signature". Discovery drops these before they reach the cache, so it should be unreachable; rendered anyway so the UI fails visibly rather than silently if that ever changes. Two fail-safe details: - The mapping defaults a missing field to `{status:'missing'}` rather than leaving it undefined. A node on an older backend returns no field at all, and "we couldn't check" must never render as "signed". - The `invalid` arm is typed in the RPC client for the same reason: an unhandled status falls through to "unsigned", not to the green badge. The tooltip carries the meaning the two-word badge can't. "Signed" is easy to misread as "safe", so it says what was actually proven — who published it — and explicitly that this is not a statement about the app being safe. Verified: vue-tsc clean, build green, and the new strings are present in the freshly built Marketplace chunk (the build can silently no-op). Co-Authored-By: Claude Opus 5 (1M context) --- neode-ui/src/api/rpc-client.ts | 10 ++++ neode-ui/src/views/Marketplace.vue | 5 ++ .../views/marketplace/MarketplaceAppCard.vue | 56 ++++++++++++++++++- .../src/views/marketplace/marketplaceData.ts | 12 ++++ 4 files changed, 82 insertions(+), 1 deletion(-) 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