feat(kez-chat/web): show verified badge in chat; require 2+ proofs
The verified checkmark only appeared on the profile, never in chat, even for clearly-verified peers. Three gaps fixed: - Chat verified only the open conversation, so list items never showed a badge. Verify all conversations on load (24h per-peer cache). - A peer's proofs were only published to the server on a manual "reverify all", so verified users were invisible to peers. Auto-publish verified subjects when the Identity page loads. - Unify the threshold: a badge now requires >=2 independently-verified proofs, in both chat (VERIFY_MIN_PROOFS) and the profile (isVerified), so "verified" means the same thing everywhere. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
41f9442650
commit
7bbf8baf86
@ -21,12 +21,25 @@
|
|||||||
const failed = $derived(claims.filter((c) => c.last_verify?.status === "fail"));
|
const failed = $derived(claims.filter((c) => c.last_verify?.status === "fail"));
|
||||||
const pending = $derived(claims.filter((c) => !c.last_verify));
|
const pending = $derived(claims.filter((c) => !c.last_verify));
|
||||||
|
|
||||||
|
// Verified badge requires at least this many independently-verified
|
||||||
|
// proofs. Kept in sync with VERIFY_MIN_PROOFS in Messages.svelte so the
|
||||||
|
// badge means the same thing on the profile and in chat.
|
||||||
|
const VERIFY_MIN_PROOFS = 2;
|
||||||
|
const isVerified = $derived(verified.length >= VERIFY_MIN_PROOFS);
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (!session.unlocked) {
|
if (!session.unlocked) {
|
||||||
push("/unlock");
|
push("/unlock");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
claims = await listClaims();
|
claims = await listClaims();
|
||||||
|
// Publish our verified proof subjects to the server profile so peers
|
||||||
|
// can discover + independently verify them (drives our badge in their
|
||||||
|
// chat). Previously this only happened on a manual "reverify all", so
|
||||||
|
// verified users were invisible to peers until they clicked it.
|
||||||
|
if (claims.some((c) => c.last_verify?.status === "ok")) {
|
||||||
|
void publishVerifiedSubjects();
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
registryRecord = await lookup(session.unlocked.handle);
|
registryRecord = await lookup(session.unlocked.handle);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -109,7 +122,7 @@
|
|||||||
<div class="flex items-center gap-2 flex-wrap">
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
<span class="font-mono text-lg font-semibold text-text truncate inline-flex items-center gap-1">
|
<span class="font-mono text-lg font-semibold text-text truncate inline-flex items-center gap-1">
|
||||||
<span class="truncate">{session.unlocked.handle}<span class="text-text-muted">@{session.unlocked.server}</span></span>
|
<span class="truncate">{session.unlocked.handle}<span class="text-text-muted">@{session.unlocked.server}</span></span>
|
||||||
{#if verified.length > 0}<VerifiedBadge size={16} />{/if}
|
{#if isVerified}<VerifiedBadge size={16} />{/if}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
class="text-xs px-2 py-0.5 rounded-sm border border-border text-text-secondary hover:bg-elevated hover:text-text shrink-0"
|
class="text-xs px-2 py-0.5 rounded-sm border border-border text-text-secondary hover:bg-elevated hover:text-text shrink-0"
|
||||||
|
|||||||
@ -140,6 +140,10 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await refresh();
|
await refresh();
|
||||||
|
// Kick off verification for every existing conversation (24h cache per
|
||||||
|
// peer), so the verified badge shows in the list without opening each
|
||||||
|
// thread one by one.
|
||||||
|
void verifyPeers(conversations);
|
||||||
// Subscribe to the always-on inbox service — re-render whenever a
|
// Subscribe to the always-on inbox service — re-render whenever a
|
||||||
// new message lands. The service is already running (it started on
|
// new message lands. The service is already running (it started on
|
||||||
// session unlock in store.svelte.ts) regardless of which route the
|
// session unlock in store.svelte.ts) regardless of which route the
|
||||||
@ -157,41 +161,47 @@
|
|||||||
conversations = await listConversations();
|
conversations = await listConversations();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the peer whenever a conversation is opened. The 24h cache in
|
// Verify the active peer whenever a conversation is opened (covers
|
||||||
// verifyPeer makes this a no-op on repeat opens (and after setVerified
|
// conversations started/arrived this session). The 24h per-peer cache
|
||||||
// refreshes the list, so no loop).
|
// makes this a no-op on repeat opens, so the post-verify refresh can't loop.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const conv = activeConv;
|
const conv = activeConv;
|
||||||
if (conv) void verifyPeer(conv);
|
if (conv) void verifyPeers([conv]);
|
||||||
});
|
});
|
||||||
|
|
||||||
const VERIFY_CACHE_MS = 24 * 60 * 60 * 1000;
|
const VERIFY_CACHE_MS = 24 * 60 * 60 * 1000;
|
||||||
|
/** A peer earns the verified badge once at least this many of their
|
||||||
|
* published proofs independently check out. Kept in sync with the
|
||||||
|
* profile rule in Identity.svelte. */
|
||||||
|
const VERIFY_MIN_PROOFS = 2;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify a peer's published proofs (24h cache). Fetches their claimed
|
* Verify a batch of peers' published proofs (24h cache per peer). For
|
||||||
* subjects from the server, runs the real channel verifiers against
|
* each peer we fetch their claimed subjects from the server, run the
|
||||||
* each, and caches whether ≥1 passed → drives the verified badge.
|
* real channel verifiers, count how many pass, and set the verified
|
||||||
* Runs in the background on conversation open; never blocks the UI.
|
* badge when ≥ VERIFY_MIN_PROOFS. Refreshes the list once at the end if
|
||||||
|
* anything changed. Runs in the background; never blocks the UI.
|
||||||
*/
|
*/
|
||||||
async function verifyPeer(conv: Conversation) {
|
async function verifyPeers(convs: Conversation[]) {
|
||||||
const fresh =
|
let changed = false;
|
||||||
conv.verified_checked_at &&
|
for (const conv of convs) {
|
||||||
Date.now() - new Date(conv.verified_checked_at).getTime() < VERIFY_CACHE_MS;
|
const fresh =
|
||||||
if (fresh) return;
|
conv.verified_checked_at &&
|
||||||
try {
|
Date.now() - new Date(conv.verified_checked_at).getTime() < VERIFY_CACHE_MS;
|
||||||
const record = await lookupByPrimary(conv.peer_primary);
|
if (fresh) continue;
|
||||||
let ok = false;
|
try {
|
||||||
for (const subject of record.proofs ?? []) {
|
const record = await lookupByPrimary(conv.peer_primary);
|
||||||
if (await verifySubject(subject, conv.peer_primary)) {
|
let verifiedCount = 0;
|
||||||
ok = true;
|
for (const subject of record.proofs ?? []) {
|
||||||
break; // one verified proof is enough for the badge
|
if (await verifySubject(subject, conv.peer_primary)) verifiedCount++;
|
||||||
}
|
}
|
||||||
|
await setVerified(conv.peer_primary, verifiedCount >= VERIFY_MIN_PROOFS);
|
||||||
|
changed = true;
|
||||||
|
} catch {
|
||||||
|
// Peer not resolvable / offline channels — leave badge as-is.
|
||||||
}
|
}
|
||||||
await setVerified(conv.peer_primary, ok);
|
|
||||||
await refresh();
|
|
||||||
} catch {
|
|
||||||
// Peer not resolvable / offline channels — leave badge as-is.
|
|
||||||
}
|
}
|
||||||
|
if (changed) await refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** "Start chat with handle" — resolve, ensure conversation, open it. */
|
/** "Start chat with handle" — resolve, ensure conversation, open it. */
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user