this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Adds a developer page

orta 1e469b3c f8f0f6c8

+760 -2
+235
apps/keytrace.dev/components/developers/ClaimVerification.vue
··· 1 + <template> 2 + <div class="border border-zinc-800 rounded-lg overflow-hidden"> 3 + <button 4 + class="w-full flex items-center justify-between p-4 bg-kt-surface hover:bg-zinc-800/50 transition-colors text-left" 5 + @click="expanded = !expanded" 6 + > 7 + <div class="flex items-center gap-3"> 8 + <div 9 + class="w-6 h-6 rounded-full flex items-center justify-center" 10 + :class="claim.verified ? 'bg-verified/20' : 'bg-failed/20'" 11 + > 12 + <CheckIcon v-if="claim.verified" class="w-4 h-4 text-verified" /> 13 + <XIcon v-else class="w-4 h-4 text-failed" /> 14 + </div> 15 + <div> 16 + <span class="font-mono text-sm text-zinc-200">{{ claim.type }}:{{ claim.identity.subject }}</span> 17 + </div> 18 + </div> 19 + <ChevronDownIcon 20 + class="w-4 h-4 text-zinc-500 transition-transform" 21 + :class="{ 'rotate-180': expanded }" 22 + /> 23 + </button> 24 + 25 + <div v-if="expanded" class="border-t border-zinc-800 p-4 space-y-4 bg-kt-inset"> 26 + <div 27 + v-for="(step, index) in claim.steps" 28 + :key="step.step" 29 + class="relative" 30 + > 31 + <!-- Step header --> 32 + <div class="flex items-start gap-3"> 33 + <div class="flex flex-col items-center"> 34 + <div 35 + class="w-5 h-5 rounded-full flex items-center justify-center flex-shrink-0" 36 + :class="step.success ? 'bg-verified/20' : 'bg-failed/20'" 37 + > 38 + <CheckIcon v-if="step.success" class="w-3 h-3 text-verified" /> 39 + <XIcon v-else class="w-3 h-3 text-failed" /> 40 + </div> 41 + <div 42 + v-if="index < claim.steps.length - 1" 43 + class="w-px flex-1 bg-zinc-700 mt-1 min-h-[16px]" 44 + /> 45 + </div> 46 + <div class="flex-1 min-w-0 pb-3"> 47 + <div class="font-mono text-xs text-zinc-400 mb-1">{{ formatStepName(step.step) }}</div> 48 + <div class="text-xs text-zinc-500"> 49 + {{ step.detail || step.error }} 50 + </div> 51 + 52 + <!-- Step-specific data display --> 53 + <div v-if="step.success" class="mt-2"> 54 + <!-- validate_claim: Show signature structure --> 55 + <template v-if="step.step === 'validate_claim'"> 56 + <div class="bg-zinc-900 border border-zinc-800 rounded p-2 overflow-x-auto"> 57 + <pre class="text-xs font-mono text-zinc-400"><span class="text-zinc-500">claim.sig = </span>{ 58 + <span class="text-violet-400">src</span>: <span class="text-green-400">"{{ truncate(claim.claim.sig.src, 50) }}"</span>, 59 + <span class="text-violet-400">signedAt</span>: <span class="text-green-400">"{{ claim.claim.sig.signedAt }}"</span>, 60 + <span class="text-violet-400">attestation</span>: <span class="text-green-400">"{{ truncate(claim.claim.sig.attestation, 40) }}..."</span> 61 + }</pre> 62 + </div> 63 + </template> 64 + 65 + <!-- fetch_key: Show the AT URI --> 66 + <template v-else-if="step.step === 'fetch_key'"> 67 + <div class="bg-zinc-900 border border-zinc-800 rounded p-2 overflow-x-auto"> 68 + <pre class="text-xs font-mono"><span class="text-zinc-500">// Fetched from AT URI:</span> 69 + <a :href="`https://pdsls.dev/${claim.claim.sig.src}`" target="_blank" rel="noopener" class="text-violet-400 hover:text-violet-300 underline">{{ claim.claim.sig.src }}</a> 70 + 71 + <span class="text-zinc-500">// Returns KeyRecord with publicJwk</span></pre> 72 + </div> 73 + </template> 74 + 75 + <!-- parse_key: Show key format --> 76 + <template v-else-if="step.step === 'parse_key'"> 77 + <div class="bg-zinc-900 border border-zinc-800 rounded p-2 overflow-x-auto"> 78 + <pre v-if="keyData" class="text-xs font-mono text-zinc-400"><span class="text-zinc-500">// Parsed ES256 public key from KeyRecord.publicJwk</span> 79 + { 80 + <span class="text-violet-400">kty</span>: <span class="text-green-400">"{{ keyData.kty }}"</span>, 81 + <span class="text-violet-400">crv</span>: <span class="text-green-400">"{{ keyData.crv }}"</span>, 82 + <span class="text-violet-400">x</span>: <span class="text-green-400">"{{ keyData.x }}"</span>, 83 + <span class="text-violet-400">y</span>: <span class="text-green-400">"{{ keyData.y }}"</span> 84 + }</pre> 85 + <pre v-else-if="keyLoading" class="text-xs font-mono text-zinc-500">Loading key data...</pre> 86 + <pre v-else class="text-xs font-mono text-zinc-400"><span class="text-zinc-500">// ES256 public key (P-256 curve)</span> 87 + { 88 + <span class="text-violet-400">kty</span>: <span class="text-green-400">"EC"</span>, 89 + <span class="text-violet-400">crv</span>: <span class="text-green-400">"P-256"</span>, 90 + <span class="text-violet-400">x</span>: <span class="text-green-400">"..."</span>, 91 + <span class="text-violet-400">y</span>: <span class="text-green-400">"..."</span> 92 + }</pre> 93 + </div> 94 + </template> 95 + 96 + <!-- reconstruct_data: Show the canonical signed data --> 97 + <template v-else-if="step.step === 'reconstruct_data'"> 98 + <div class="bg-zinc-900 border border-zinc-800 rounded p-2 overflow-x-auto"> 99 + <pre class="text-xs font-mono text-zinc-400"><span class="text-zinc-500">// Canonical data that was signed:</span> 100 + <span class="text-violet-400">SignedClaimData</span> = { 101 + <span class="text-violet-400">did</span>: <span class="text-green-400">"{{ did }}"</span>, 102 + <span class="text-violet-400">subject</span>: <span class="text-green-400">"{{ claim.identity.subject }}"</span>, 103 + <span class="text-violet-400">type</span>: <span class="text-green-400">"{{ claim.type }}"</span>, 104 + <span class="text-violet-400">verifiedAt</span>: <span class="text-green-400">"{{ claim.claim.sig.signedAt }}"</span> 105 + }</pre> 106 + </div> 107 + </template> 108 + 109 + <!-- verify_signature: Show signature verification --> 110 + <template v-else-if="step.step === 'verify_signature'"> 111 + <div class="bg-zinc-900 border border-zinc-800 rounded p-2 overflow-x-auto"> 112 + <pre class="text-xs font-mono text-zinc-400"><span class="text-zinc-500">// JWS Compact Serialization (ES256):</span> 113 + <span class="text-violet-400">header</span>.<span class="text-yellow-400">payload</span>.<span class="text-green-400">signature</span> 114 + 115 + <span class="text-zinc-500">// Attestation:</span> 116 + {{ formatJws(claim.claim.sig.attestation) }} 117 + 118 + <span class="text-zinc-500">// Verification:</span> 119 + <span class="text-verified">crypto.subtle.verify("ES256", publicKey, signature, payload)</span></pre> 120 + </div> 121 + </template> 122 + </div> 123 + 124 + <!-- Error display --> 125 + <div v-else-if="step.error" class="mt-2 bg-failed/10 border border-failed/20 rounded p-2"> 126 + <pre class="text-xs font-mono text-failed">{{ step.error }}</pre> 127 + </div> 128 + </div> 129 + </div> 130 + </div> 131 + </div> 132 + </div> 133 + </template> 134 + 135 + <script setup lang="ts"> 136 + import { Check as CheckIcon, X as XIcon, ChevronDown as ChevronDownIcon } from "lucide-vue-next"; 137 + import type { ClaimVerificationResult } from "@keytrace/verify"; 138 + 139 + interface ES256PublicJwk { 140 + kty: string; 141 + crv: string; 142 + x: string; 143 + y: string; 144 + } 145 + 146 + const props = defineProps<{ 147 + claim: ClaimVerificationResult; 148 + did: string; 149 + defaultExpanded?: boolean; 150 + }>(); 151 + 152 + const expanded = ref(props.defaultExpanded ?? false); 153 + const keyData = ref<ES256PublicJwk | null>(null); 154 + const keyLoading = ref(false); 155 + 156 + // Fetch the key when expanded 157 + watch(expanded, async (isExpanded) => { 158 + if (isExpanded && !keyData.value && !keyLoading.value) { 159 + await fetchKey(); 160 + } 161 + }); 162 + 163 + // Also fetch immediately if defaultExpanded 164 + onMounted(async () => { 165 + if (props.defaultExpanded) { 166 + await fetchKey(); 167 + } 168 + }); 169 + 170 + async function fetchKey() { 171 + if (!props.claim.claim.sig?.src) return; 172 + 173 + keyLoading.value = true; 174 + try { 175 + // Parse AT URI: at://did:plc:xxx/collection/rkey 176 + const atUri = props.claim.claim.sig.src; 177 + const match = atUri.match(/^at:\/\/([^/]+)\/([^/]+)\/([^/]+)$/); 178 + if (!match) return; 179 + 180 + const [, repo, collection, rkey] = match; 181 + 182 + // First resolve the PDS for this DID 183 + let pdsUrl = "https://bsky.social"; 184 + if (repo.startsWith("did:plc:")) { 185 + const plcResponse = await fetch(`https://plc.directory/${repo}`); 186 + if (plcResponse.ok) { 187 + const didDoc = await plcResponse.json(); 188 + const pdsService = didDoc.service?.find( 189 + (s: { id: string; type: string }) => s.id === "#atproto_pds" || s.type === "AtprotoPersonalDataServer" 190 + ); 191 + if (pdsService?.serviceEndpoint) { 192 + pdsUrl = pdsService.serviceEndpoint; 193 + } 194 + } 195 + } 196 + 197 + // Fetch the key record from the correct PDS 198 + const response = await fetch( 199 + `${pdsUrl}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(repo)}&collection=${encodeURIComponent(collection)}&rkey=${encodeURIComponent(rkey)}` 200 + ); 201 + 202 + if (response.ok) { 203 + const data = await response.json(); 204 + if (data.value?.publicJwk) { 205 + keyData.value = JSON.parse(data.value.publicJwk); 206 + } 207 + } 208 + } catch (e) { 209 + console.error("Failed to fetch key:", e); 210 + } finally { 211 + keyLoading.value = false; 212 + } 213 + } 214 + 215 + function formatStepName(step: string): string { 216 + return step.replace(/_/g, " "); 217 + } 218 + 219 + function truncate(str: string, len: number): string { 220 + if (str.length <= len) return str; 221 + return str.slice(0, len) + "..."; 222 + } 223 + 224 + function formatJws(jws: string): string { 225 + const parts = jws.split("."); 226 + if (parts.length !== 3) return jws; 227 + 228 + const [header, payload, sig] = parts; 229 + const truncatedHeader = header.length > 20 ? header.slice(0, 20) + "..." : header; 230 + const truncatedPayload = payload.length > 20 ? payload.slice(0, 20) + "..." : payload; 231 + const truncatedSig = sig.length > 20 ? sig.slice(0, 20) + "..." : sig; 232 + 233 + return `${truncatedHeader}.${truncatedPayload}.${truncatedSig}`; 234 + } 235 + </script>
+158
apps/keytrace.dev/components/developers/VerificationProcess.vue
··· 1 + <template> 2 + <div class="space-y-4"> 3 + <!-- Step 1: Resolve Handle --> 4 + <div class="bg-kt-card border border-zinc-800 rounded-lg p-5"> 5 + <div class="flex items-center gap-2 mb-3"> 6 + <div class="w-6 h-6 rounded-full bg-violet-600/20 flex items-center justify-center"> 7 + <span class="text-xs font-bold text-violet-400">1</span> 8 + </div> 9 + <h3 class="text-sm font-semibold text-zinc-300">Resolve Handle</h3> 10 + </div> 11 + <p class="text-sm text-zinc-400 mb-4"> 12 + The ATProto handle is resolved to a DID using the public API. DIDs are persistent identifiers that remain stable even if the handle changes. 13 + </p> 14 + <div class="bg-zinc-900 border border-zinc-800 rounded p-3"> 15 + <div class="flex items-center gap-2 text-sm"> 16 + <span class="text-zinc-500">{{ result.handle || 'handle' }}</span> 17 + <ArrowRightIcon class="w-4 h-4 text-zinc-600" /> 18 + <span class="font-mono text-violet-400">{{ result.did }}</span> 19 + </div> 20 + </div> 21 + <div class="mt-3 bg-zinc-900/50 border border-zinc-800/50 rounded p-2"> 22 + <code class="text-xs text-zinc-500">result.did <span class="text-zinc-600">//</span> <span class="text-green-400">"{{ result.did }}"</span></code> 23 + </div> 24 + </div> 25 + 26 + <!-- Step 2: Locate PDS --> 27 + <div class="bg-kt-card border border-zinc-800 rounded-lg p-5"> 28 + <div class="flex items-center gap-2 mb-3"> 29 + <div class="w-6 h-6 rounded-full bg-violet-600/20 flex items-center justify-center"> 30 + <span class="text-xs font-bold text-violet-400">2</span> 31 + </div> 32 + <h3 class="text-sm font-semibold text-zinc-300">Locate PDS</h3> 33 + </div> 34 + <p class="text-sm text-zinc-400 mb-4"> 35 + The DID document tells us where the user's data is stored (their Personal Data Server). This is fetched from the PLC directory. 36 + </p> 37 + <div class="bg-zinc-900 border border-zinc-800 rounded p-3"> 38 + <div class="flex items-center gap-2 text-sm"> 39 + <span class="text-zinc-500">PDS:</span> 40 + <span class="font-mono text-violet-400">{{ pdsUrl }}</span> 41 + </div> 42 + </div> 43 + </div> 44 + 45 + <!-- Step 3: Fetch Claims --> 46 + <div class="bg-kt-card border border-zinc-800 rounded-lg p-5"> 47 + <div class="flex items-center gap-2 mb-3"> 48 + <div class="w-6 h-6 rounded-full bg-violet-600/20 flex items-center justify-center"> 49 + <span class="text-xs font-bold text-violet-400">3</span> 50 + </div> 51 + <h3 class="text-sm font-semibold text-zinc-300">Fetch Claims</h3> 52 + </div> 53 + <p class="text-sm text-zinc-400 mb-4"> 54 + We list all <code class="font-mono text-xs bg-zinc-800 px-1 rounded">dev.keytrace.claim</code> records from the user's ATProto repository. Each claim represents a verified external identity. 55 + </p> 56 + <div class="bg-zinc-900 border border-zinc-800 rounded p-3"> 57 + <div class="text-sm"> 58 + <span class="text-zinc-500">Found</span> 59 + <span class="font-mono text-violet-400 mx-1">{{ result.claims.length }}</span> 60 + <span class="text-zinc-500">{{ result.claims.length === 1 ? 'claim' : 'claims' }}:</span> 61 + <span class="font-mono text-zinc-400 ml-2"> 62 + {{ result.claims.map(c => c.type).join(', ') }} 63 + </span> 64 + </div> 65 + </div> 66 + <div class="mt-3 bg-zinc-900/50 border border-zinc-800/50 rounded p-2"> 67 + <code class="text-xs text-zinc-500">result.claims.length <span class="text-zinc-600">//</span> <span class="text-green-400">{{ result.claims.length }}</span></code> 68 + </div> 69 + </div> 70 + 71 + <!-- Step 4: Verify Each Claim --> 72 + <div class="bg-kt-card border border-zinc-800 rounded-lg p-5"> 73 + <div class="flex items-center gap-2 mb-3"> 74 + <div class="w-6 h-6 rounded-full bg-violet-600/20 flex items-center justify-center"> 75 + <span class="text-xs font-bold text-violet-400">4</span> 76 + </div> 77 + <h3 class="text-sm font-semibold text-zinc-300">Verify Each Claim</h3> 78 + </div> 79 + <p class="text-sm text-zinc-400 mb-4"> 80 + Each claim contains a cryptographic signature that proves Keytrace verified the identity. We verify this signature using the Web Crypto API. 81 + </p> 82 + 83 + <!-- Claims list --> 84 + <div v-if="result.claims.length > 0" class="space-y-3 mb-4"> 85 + <ClaimVerification 86 + v-for="(claim, index) in result.claims" 87 + :key="claim.uri" 88 + :claim="claim" 89 + :did="result.did" 90 + :default-expanded="index === 0" 91 + /> 92 + </div> 93 + 94 + <!-- Step explanations --> 95 + <div class="bg-zinc-900/50 border border-zinc-800/50 rounded p-4 text-xs text-zinc-500 space-y-2"> 96 + <div class="font-semibold text-zinc-400 mb-2">Verification steps:</div> 97 + <div><span class="font-mono text-zinc-400">validate_claim</span> - Check signature fields exist (sig.src, sig.attestation, sig.signedAt)</div> 98 + <div><span class="font-mono text-zinc-400">fetch_key</span> - Fetch the ES256 public key from the sig.src AT URI</div> 99 + <div><span class="font-mono text-zinc-400">parse_key</span> - Parse the JWK and verify it's a P-256 ECDSA key</div> 100 + <div><span class="font-mono text-zinc-400">reconstruct_data</span> - Rebuild the canonical data: {'{'}did, subject, type, verifiedAt{'}'}</div> 101 + <div><span class="font-mono text-zinc-400">verify_signature</span> - Verify the ES256 signature using Web Crypto</div> 102 + </div> 103 + </div> 104 + 105 + <!-- Summary --> 106 + <div class="bg-kt-card border border-zinc-800 rounded-lg p-5"> 107 + <div class="flex items-center gap-2 mb-3"> 108 + <div 109 + class="w-6 h-6 rounded-full flex items-center justify-center" 110 + :class="result.summary.failed === 0 ? 'bg-verified/20' : 'bg-failed/20'" 111 + > 112 + <CheckIcon v-if="result.summary.failed === 0" class="w-4 h-4 text-verified" /> 113 + <XIcon v-else class="w-4 h-4 text-failed" /> 114 + </div> 115 + <h3 class="text-sm font-semibold text-zinc-300">Summary</h3> 116 + </div> 117 + <div class="text-lg font-semibold mb-3"> 118 + <span :class="result.summary.failed === 0 ? 'text-verified' : 'text-zinc-200'"> 119 + {{ result.summary.verified }}/{{ result.summary.total }} 120 + </span> 121 + <span class="text-zinc-400 font-normal ml-2">claims verified</span> 122 + </div> 123 + <div class="bg-zinc-900/50 border border-zinc-800/50 rounded p-2"> 124 + <code class="text-xs text-zinc-500">result.summary <span class="text-zinc-600">//</span> <span class="text-green-400">{{ JSON.stringify(result.summary) }}</span></code> 125 + </div> 126 + </div> 127 + </div> 128 + </template> 129 + 130 + <script setup lang="ts"> 131 + import { ArrowRight as ArrowRightIcon, Check as CheckIcon, X as XIcon } from "lucide-vue-next"; 132 + import type { VerificationResult } from "@keytrace/verify"; 133 + 134 + const props = defineProps<{ 135 + result: VerificationResult; 136 + }>(); 137 + 138 + // Extract PDS URL from the first claim's sig.src if available 139 + const pdsUrl = computed(() => { 140 + if (props.result.claims.length > 0 && props.result.claims[0].claim.sig?.src) { 141 + try { 142 + // Parse AT URI: at://did:plc:xxx/collection/rkey 143 + const atUri = props.result.claims[0].claim.sig.src; 144 + const parts = atUri.replace("at://", "").split("/"); 145 + const did = parts[0]; 146 + // We don't have the actual PDS URL in the result, so we show a placeholder 147 + // In reality, the verify package resolves this internally 148 + if (did.startsWith("did:plc:")) { 149 + return "https://bsky.social (or user's PDS)"; 150 + } 151 + return "Resolved from DID document"; 152 + } catch { 153 + return "Resolved from DID document"; 154 + } 155 + } 156 + return "Resolved from DID document"; 157 + }); 158 + </script>
+1 -1
apps/keytrace.dev/components/ui/NavBar.vue
··· 29 29 </div> 30 30 </nav> 31 31 <!-- Alpha warning banner --> 32 - <div class="bg-amber-500/10 border-b border-amber-500/20 px-4 py-2"> 32 + <div class="bg-amber-950 border-b border-amber-500/20 px-4 py-2"> 33 33 <p class="max-w-5xl mx-auto text-xs text-amber-300/90 text-center"> 34 34 <TriangleAlertIcon class="w-3 h-3 inline-block mr-1 -mt-0.5" /> 35 35 Keytrace is in alpha. You likely will need to re-verify your claims when we it goes live.
+49
apps/keytrace.dev/composables/useVerifyDemo.ts
··· 1 + import type { VerificationResult } from "@keytrace/verify"; 2 + 3 + type DemoStatus = "idle" | "verifying" | "complete" | "error"; 4 + 5 + const handle = ref(""); 6 + const status = ref<DemoStatus>("idle"); 7 + const result = ref<VerificationResult | null>(null); 8 + const error = ref<string | null>(null); 9 + 10 + export function useVerifyDemo() { 11 + async function verify(handleInput: string) { 12 + if (!handleInput.trim()) { 13 + error.value = "Please enter a handle or DID"; 14 + status.value = "error"; 15 + return; 16 + } 17 + 18 + handle.value = handleInput.trim(); 19 + status.value = "verifying"; 20 + error.value = null; 21 + result.value = null; 22 + 23 + try { 24 + // Dynamic import for client-side only 25 + const { verifyClaims } = await import("@keytrace/verify"); 26 + result.value = await verifyClaims(handle.value); 27 + status.value = "complete"; 28 + } catch (err) { 29 + error.value = err instanceof Error ? err.message : "Verification failed"; 30 + status.value = "error"; 31 + } 32 + } 33 + 34 + function reset() { 35 + handle.value = ""; 36 + status.value = "idle"; 37 + result.value = null; 38 + error.value = null; 39 + } 40 + 41 + return { 42 + handle: readonly(handle), 43 + status: readonly(status), 44 + result: readonly(result), 45 + error: readonly(error), 46 + verify, 47 + reset, 48 + }; 49 + }
+3
apps/keytrace.dev/layouts/default.vue
··· 55 55 <NuxtLink to="/recipes" class="text-xs text-zinc-600 hover:text-zinc-400 transition-colors"> 56 56 How it works 57 57 </NuxtLink> 58 + <NuxtLink to="/developers" class="text-xs text-zinc-600 hover:text-zinc-400 transition-colors"> 59 + For Developers 60 + </NuxtLink> 58 61 <a href="https://github.com/orta/keytrace" target="_blank" rel="noopener" class="text-zinc-600 hover:text-zinc-400 transition-colors" aria-label="GitHub"> 59 62 <svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor"> 60 63 <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
+4 -1
apps/keytrace.dev/package.json
··· 6 6 "dev": "nuxt dev", 7 7 "build": "nuxt build", 8 8 "preview": "nuxt preview", 9 - "start": "node .output/server/index.mjs" 9 + "start": "node .output/server/index.mjs", 10 + "verify": "tsx scripts/verify-claims.ts" 10 11 }, 11 12 "dependencies": { 12 13 "@atproto/oauth-client-node": "^0.3.0", 13 14 "@aws-sdk/client-s3": "^3.700.0", 15 + "@keytrace/verify": "workspace:*", 14 16 "@nuxtjs/google-fonts": "^3.2.0", 15 17 "@nuxtjs/tailwindcss": "^6.14.0", 16 18 "lucide-vue-next": "^0.563.0", ··· 19 21 "vue": "^3.5.0" 20 22 }, 21 23 "devDependencies": { 24 + "tsx": "^4.19.0", 22 25 "typescript": "^5.7.0" 23 26 } 24 27 }
+196
apps/keytrace.dev/pages/developers.vue
··· 1 + <template> 2 + <div class="max-w-3xl mx-auto px-4 py-12"> 3 + <!-- Header --> 4 + <div class="mb-8"> 5 + <div class="flex items-center gap-3 mb-2"> 6 + <div class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md bg-violet-600/15 border border-violet-500/20"> 7 + <span class="font-mono text-xs font-bold text-violet-400">@keytrace/verify</span> 8 + </div> 9 + <a href="https://www.npmjs.com/package/@keytrace/verify" target="_blank" rel="noopener" class="text-xs text-zinc-500 hover:text-zinc-400"> npm </a> 10 + </div> 11 + <h1 class="text-2xl font-bold text-zinc-100 mb-2">Verify Keytrace Claims</h1> 12 + <p class="text-zinc-400">Verify identity claims in the browser or Node.js. Zero runtime dependencies.</p> 13 + </div> 14 + 15 + <!-- Install command --> 16 + <div class="mb-8 bg-zinc-900 border border-zinc-800 rounded-lg p-4 flex items-center justify-between"> 17 + <code class="font-mono text-sm text-zinc-300">npm install @keytrace/verify</code> 18 + <button class="text-zinc-500 hover:text-zinc-300 transition-colors" @click="copyInstall" :title="copied ? 'Copied!' : 'Copy'"> 19 + <CheckIcon v-if="copied" class="w-4 h-4 text-verified" /> 20 + <CopyIcon v-else class="w-4 h-4" /> 21 + </button> 22 + </div> 23 + 24 + <!-- Handle Input --> 25 + <div class="mb-8"> 26 + <div class="flex gap-2 mb-3"> 27 + <input 28 + v-model="handleInput" 29 + type="text" 30 + placeholder="alice.bsky.social or did:plc:..." 31 + class="flex-1 bg-kt-inset border border-zinc-800 rounded-lg px-4 py-2 text-sm text-zinc-200 placeholder-zinc-600 focus:outline-none focus:border-violet-500/50" 32 + @keydown.enter="runVerify" 33 + :disabled="status === 'verifying'" 34 + /> 35 + <button 36 + class="px-4 py-2 bg-violet-600 hover:bg-violet-500 disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-medium rounded-lg transition-colors" 37 + @click="runVerify" 38 + :disabled="status === 'verifying' || !handleInput.trim()" 39 + > 40 + <LoaderIcon v-if="status === 'verifying'" class="w-4 h-4 animate-spin" /> 41 + <span v-else>Verify</span> 42 + </button> 43 + </div> 44 + 45 + <!-- Recent handles --> 46 + <div v-if="recentHandles.length > 0" class="flex items-center gap-2 flex-wrap"> 47 + <span class="text-xs text-zinc-600">Recent:</span> 48 + <button 49 + v-for="h in recentHandles" 50 + :key="h" 51 + class="px-2 py-1 text-xs font-mono bg-zinc-800 hover:bg-zinc-700 text-zinc-400 hover:text-zinc-200 rounded-full transition-colors" 52 + @click="selectHandle(h)" 53 + :disabled="status === 'verifying'" 54 + > 55 + {{ h }} 56 + </button> 57 + </div> 58 + </div> 59 + 60 + <!-- Error state --> 61 + <div v-if="status === 'error' && error" class="mb-8 bg-failed/10 border border-failed/30 rounded-lg p-4"> 62 + <div class="flex items-center gap-2 text-failed"> 63 + <AlertCircleIcon class="w-4 h-4" /> 64 + <span class="text-sm font-medium">Verification failed</span> 65 + </div> 66 + <p class="text-sm text-zinc-400 mt-1">{{ error }}</p> 67 + </div> 68 + 69 + <!-- Loading state --> 70 + <div v-if="status === 'verifying'" class="mb-8"> 71 + <div class="bg-kt-card border border-zinc-800 rounded-lg p-8 flex flex-col items-center justify-center"> 72 + <LoaderIcon class="w-8 h-8 text-violet-400 animate-spin mb-4" /> 73 + <p class="text-sm text-zinc-400 mb-4"> 74 + Verifying claims for <span class="font-mono text-violet-400">{{ handle }}</span>... 75 + </p> 76 + <p class="text-xs text-zinc-500 mb-2 w-full text-left">Running this code:</p> 77 + <div class="w-full bg-zinc-900 border border-zinc-800 rounded p-3 overflow-x-auto"> 78 + <pre class="text-xs font-mono text-zinc-400"><span class="text-violet-400">import</span> { verifyClaims } <span class="text-violet-400">from</span> <span class="text-green-400">'@keytrace/verify'</span>; 79 + 80 + <span class="text-violet-400">const</span> result = <span class="text-violet-400">await</span> verifyClaims(<span class="text-green-400">'{{ handle }}'</span>);</pre> 81 + </div> 82 + </div> 83 + </div> 84 + 85 + <!-- Verification Process --> 86 + <div v-if="status === 'complete' && result" class="mb-8"> 87 + <div v-if="result.claims.length === 0" class="bg-kt-card border border-zinc-800 rounded-lg p-8 text-center"> 88 + <p class="text-zinc-400">No claims found for this user.</p> 89 + <p class="text-sm text-zinc-500 mt-2">This user hasn't created any Keytrace identity claims yet.</p> 90 + </div> 91 + 92 + <VerificationProcess v-else :result="result" /> 93 + </div> 94 + 95 + <!-- Documentation - always visible --> 96 + <div class="space-y-6"> 97 + <div class="bg-kt-card border border-zinc-800 rounded-lg p-5"> 98 + <h2 class="text-sm font-semibold text-zinc-300 mb-3">Quick Start</h2> 99 + <div class="bg-zinc-900 border border-zinc-800 rounded p-4 overflow-x-auto"> 100 + <pre 101 + class="text-xs text-zinc-300 font-mono" 102 + ><span class="text-violet-400">import</span> { verifyClaims } <span class="text-violet-400">from</span> <span class="text-green-400">'@keytrace/verify'</span>; 103 + 104 + <span class="text-violet-400">const</span> result = <span class="text-violet-400">await</span> verifyClaims(<span class="text-green-400">'alice.bsky.social'</span>); 105 + 106 + console.log(<span class="text-green-400">`${result.summary.verified}/${result.summary.total} claims verified`</span>); 107 + 108 + <span class="text-violet-400">for</span> (<span class="text-violet-400">const</span> claim <span class="text-violet-400">of</span> result.claims) { 109 + <span class="text-violet-400">if</span> (claim.verified) { 110 + console.log(<span class="text-green-400">`</span><span class="text-zinc-500">&#10003;</span> <span class="text-green-400">${claim.type}: ${claim.identity.subject}`</span>); 111 + } 112 + }</pre> 113 + </div> 114 + </div> 115 + 116 + <div class="bg-kt-card border border-zinc-800 rounded-lg p-5"> 117 + <h2 class="text-sm font-semibold text-zinc-300 mb-3">How It Works</h2> 118 + <div class="text-sm text-zinc-400 space-y-3"> 119 + <p>Enter a handle above to see the verification process in action. The library performs these steps:</p> 120 + <ol class="list-decimal list-inside space-y-2 text-zinc-500"> 121 + <li>Resolves the handle to a DID via the public ATProto API</li> 122 + <li>Locates the user's Personal Data Server (PDS) from their DID document</li> 123 + <li>Fetches all <code class="font-mono text-xs bg-zinc-800 px-1 rounded">dev.keytrace.claim</code> records</li> 124 + <li>Verifies each claim's cryptographic signature using Web Crypto</li> 125 + </ol> 126 + </div> 127 + </div> 128 + 129 + <div class="bg-kt-card border border-zinc-800 rounded-lg p-5"> 130 + <h2 class="text-sm font-semibold text-zinc-300 mb-3">Platform Support</h2> 131 + <div class="flex flex-wrap gap-2"> 132 + <span class="px-2 py-1 text-xs bg-zinc-800 text-zinc-400 rounded">Node.js 18+</span> 133 + <span class="px-2 py-1 text-xs bg-zinc-800 text-zinc-400 rounded">Modern Browsers</span> 134 + <span class="px-2 py-1 text-xs bg-zinc-800 text-zinc-400 rounded">Deno</span> 135 + <span class="px-2 py-1 text-xs bg-zinc-800 text-zinc-400 rounded">Cloudflare Workers</span> 136 + </div> 137 + <p class="text-xs text-zinc-500 mt-3">Zero runtime dependencies - uses standard fetch and crypto.subtle APIs.</p> 138 + </div> 139 + </div> 140 + 141 + <!-- Footer link --> 142 + <div class="mt-12 pt-8 border-t border-zinc-800/50"> 143 + <a 144 + href="https://github.com/orta/keytrace/tree/main/packages/verify" 145 + target="_blank" 146 + rel="noopener" 147 + class="inline-flex items-center gap-2 text-sm text-zinc-500 hover:text-zinc-300 transition-colors" 148 + > 149 + <GithubIcon class="w-4 h-4" /> 150 + View on GitHub 151 + </a> 152 + </div> 153 + </div> 154 + </template> 155 + 156 + <script setup lang="ts"> 157 + import { Copy as CopyIcon, Check as CheckIcon, Loader2 as LoaderIcon, AlertCircle as AlertCircleIcon, Github as GithubIcon } from "lucide-vue-next"; 158 + 159 + const { handle, status, result, error, verify, reset } = useVerifyDemo(); 160 + 161 + const handleInput = ref(""); 162 + const copied = ref(false); 163 + 164 + // Fetch recent handles from the API 165 + const { data: recentClaims } = await useFetch("/api/recent-claims"); 166 + 167 + const recentHandles = computed(() => { 168 + if (!recentClaims.value) return []; 169 + const handles = new Set<string>(); 170 + for (const claim of recentClaims.value) { 171 + if (claim.handle && handles.size < 5) { 172 + handles.add(claim.handle); 173 + } 174 + } 175 + return Array.from(handles); 176 + }); 177 + 178 + function selectHandle(h: string) { 179 + handleInput.value = h; 180 + runVerify(); 181 + } 182 + 183 + function runVerify() { 184 + if (handleInput.value.trim()) { 185 + verify(handleInput.value.trim()); 186 + } 187 + } 188 + 189 + async function copyInstall() { 190 + await navigator.clipboard.writeText("npm install @keytrace/verify"); 191 + copied.value = true; 192 + setTimeout(() => { 193 + copied.value = false; 194 + }, 2000); 195 + } 196 + </script>
+114
apps/keytrace.dev/scripts/verify-claims.ts
··· 1 + #!/usr/bin/env npx tsx 2 + /** 3 + * CLI script to verify keytrace claims for a handle. 4 + * 5 + * Usage: npx tsx scripts/verify-claims.ts <handle> 6 + * Example: npx tsx scripts/verify-claims.ts kitten.sh 7 + */ 8 + 9 + import { verifyClaims } from "@keytrace/verify"; 10 + 11 + const COLORS = { 12 + reset: "\x1b[0m", 13 + bold: "\x1b[1m", 14 + dim: "\x1b[2m", 15 + green: "\x1b[32m", 16 + red: "\x1b[31m", 17 + yellow: "\x1b[33m", 18 + blue: "\x1b[34m", 19 + cyan: "\x1b[36m", 20 + magenta: "\x1b[35m", 21 + }; 22 + 23 + function c(color: keyof typeof COLORS, text: string): string { 24 + return `${COLORS[color]}${text}${COLORS.reset}`; 25 + } 26 + 27 + async function main() { 28 + const handle = process.argv[2]; 29 + 30 + if (!handle) { 31 + console.error("Usage: npx tsx scripts/verify-claims.ts <handle>"); 32 + console.error("Example: npx tsx scripts/verify-claims.ts kitten.sh"); 33 + process.exit(1); 34 + } 35 + 36 + console.log(c("bold", `\nVerifying claims for ${c("cyan", handle)}...\n`)); 37 + 38 + try { 39 + const result = await verifyClaims(handle); 40 + 41 + // Header 42 + console.log(c("dim", "─".repeat(60))); 43 + console.log(c("bold", "Profile")); 44 + console.log(c("dim", "─".repeat(60))); 45 + console.log(` DID: ${c("blue", result.did)}`); 46 + if (result.handle) { 47 + console.log(` Handle: ${c("cyan", result.handle)}`); 48 + } 49 + console.log(); 50 + 51 + // Summary 52 + const { total, verified, failed } = result.summary; 53 + console.log(c("bold", "Summary")); 54 + console.log(c("dim", "─".repeat(60))); 55 + console.log(` Total claims: ${total}`); 56 + console.log(` Verified: ${c("green", String(verified))}`); 57 + console.log(` Failed: ${failed > 0 ? c("red", String(failed)) : String(failed)}`); 58 + console.log(); 59 + 60 + if (result.claims.length === 0) { 61 + console.log(c("yellow", "No claims found for this user.")); 62 + return; 63 + } 64 + 65 + // Individual claims 66 + console.log(c("bold", "Claims")); 67 + console.log(c("dim", "─".repeat(60))); 68 + 69 + for (const claim of result.claims) { 70 + const status = claim.verified ? c("green", "VERIFIED") : c("red", "FAILED"); 71 + const typeLabel = c("magenta", claim.type.padEnd(12)); 72 + 73 + console.log(); 74 + console.log(` ${status} ${typeLabel} ${c("cyan", claim.identity.subject)}`); 75 + console.log(` ${c("dim", "Claim URI:")} ${claim.claimUri}`); 76 + 77 + if (claim.identity.displayName) { 78 + console.log(` ${c("dim", "Display:")} ${claim.identity.displayName}`); 79 + } 80 + if (claim.identity.profileUrl) { 81 + console.log(` ${c("dim", "Profile:")} ${claim.identity.profileUrl}`); 82 + } 83 + 84 + // Show verification steps 85 + console.log(` ${c("dim", "Steps:")}`); 86 + for (const step of claim.steps) { 87 + const stepStatus = step.success ? c("green", "OK") : c("red", "FAIL"); 88 + const stepDetail = step.detail || step.error || ""; 89 + console.log(` ${stepStatus.padEnd(14)} ${c("dim", step.step.padEnd(20))} ${stepDetail}`); 90 + } 91 + 92 + if (claim.error) { 93 + console.log(` ${c("red", "Error:")} ${claim.error}`); 94 + } 95 + 96 + // Signature info 97 + console.log(` ${c("dim", "Signed at:")} ${claim.claim.sig.signedAt}`); 98 + console.log(` ${c("dim", "Record:")} at://${result.did}/dev.keytrace.claim/${claim.rkey}`); 99 + } 100 + 101 + console.log(); 102 + console.log(c("dim", "─".repeat(60))); 103 + 104 + // Exit code based on verification status 105 + if (failed > 0) { 106 + process.exit(1); 107 + } 108 + } catch (err) { 109 + console.error(c("red", `Error: ${err instanceof Error ? err.message : String(err)}`)); 110 + process.exit(1); 111 + } 112 + } 113 + 114 + main();