feat(marketplace-ui): show whether an app's authorship was actually proven
Demo images / Build & push demo images (push) Successful in 3m27s

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) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-08 06:00:19 -04:00
co-authored by Claude Opus 5
parent f0c289a415
commit cd5d7daeae
4 changed files with 82 additions and 1 deletions
+10
View File
@@ -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
}> {
+5
View File
@@ -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'
@@ -32,7 +32,7 @@
</div>
<!-- Trust badge for Nostr community apps -->
<div v-if="app.trustTier" class="flex items-center gap-2 mb-3">
<div v-if="app.trustTier" class="flex items-center flex-wrap gap-2 mb-3">
<span
class="text-xs px-2 py-0.5 rounded-full font-medium"
:class="{
@@ -42,6 +42,31 @@
'bg-red-400/20 text-red-400': app.trustTier === 'untrusted',
}"
>{{ app.trustTier }}</span>
<!--
Authorship, which is a different question from the trust score: the
score blends relay count, provenance and policy compliance, while this
says only whether the author proved control of the key their DID names.
-->
<span
v-if="app.signature"
class="text-xs px-2 py-0.5 rounded-full font-medium inline-flex items-center gap-1"
:class="{
'bg-green-400/20 text-green-400': app.signature.status === 'valid',
'bg-white/10 text-white/50': app.signature.status === 'missing',
'bg-red-400/20 text-red-400': app.signature.status === 'invalid',
}"
:title="signatureTooltip"
>
<svg v-if="app.signature.status === 'valid'" class="w-3 h-3" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd" />
</svg>
<svg v-else-if="app.signature.status === 'invalid'" class="w-3 h-3" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
{{ signatureLabel }}
</span>
<span class="text-xs text-white/40">Score: {{ app.trustScore }}/100</span>
<span v-if="app.relayCount" class="text-xs text-white/40">&middot; {{ app.relayCount }} relay{{ app.relayCount !== 1 ? 's' : '' }}</span>
</div>
@@ -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'
@@ -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