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 @@
-
@@ -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