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 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 () => {
|
||||
if (!session.unlocked) {
|
||||
push("/unlock");
|
||||
return;
|
||||
}
|
||||
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 {
|
||||
registryRecord = await lookup(session.unlocked.handle);
|
||||
} catch (e) {
|
||||
@ -109,7 +122,7 @@
|
||||
<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="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>
|
||||
<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"
|
||||
|
||||
@ -140,6 +140,10 @@
|
||||
return;
|
||||
}
|
||||
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
|
||||
// new message lands. The service is already running (it started on
|
||||
// session unlock in store.svelte.ts) regardless of which route the
|
||||
@ -157,41 +161,47 @@
|
||||
conversations = await listConversations();
|
||||
}
|
||||
|
||||
// Verify the peer whenever a conversation is opened. The 24h cache in
|
||||
// verifyPeer makes this a no-op on repeat opens (and after setVerified
|
||||
// refreshes the list, so no loop).
|
||||
// Verify the active peer whenever a conversation is opened (covers
|
||||
// conversations started/arrived this session). The 24h per-peer cache
|
||||
// makes this a no-op on repeat opens, so the post-verify refresh can't loop.
|
||||
$effect(() => {
|
||||
const conv = activeConv;
|
||||
if (conv) void verifyPeer(conv);
|
||||
if (conv) void verifyPeers([conv]);
|
||||
});
|
||||
|
||||
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
|
||||
* subjects from the server, runs the real channel verifiers against
|
||||
* each, and caches whether ≥1 passed → drives the verified badge.
|
||||
* Runs in the background on conversation open; never blocks the UI.
|
||||
* Verify a batch of peers' published proofs (24h cache per peer). For
|
||||
* each peer we fetch their claimed subjects from the server, run the
|
||||
* real channel verifiers, count how many pass, and set the verified
|
||||
* 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) {
|
||||
const fresh =
|
||||
conv.verified_checked_at &&
|
||||
Date.now() - new Date(conv.verified_checked_at).getTime() < VERIFY_CACHE_MS;
|
||||
if (fresh) return;
|
||||
try {
|
||||
const record = await lookupByPrimary(conv.peer_primary);
|
||||
let ok = false;
|
||||
for (const subject of record.proofs ?? []) {
|
||||
if (await verifySubject(subject, conv.peer_primary)) {
|
||||
ok = true;
|
||||
break; // one verified proof is enough for the badge
|
||||
async function verifyPeers(convs: Conversation[]) {
|
||||
let changed = false;
|
||||
for (const conv of convs) {
|
||||
const fresh =
|
||||
conv.verified_checked_at &&
|
||||
Date.now() - new Date(conv.verified_checked_at).getTime() < VERIFY_CACHE_MS;
|
||||
if (fresh) continue;
|
||||
try {
|
||||
const record = await lookupByPrimary(conv.peer_primary);
|
||||
let verifiedCount = 0;
|
||||
for (const subject of record.proofs ?? []) {
|
||||
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. */
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user