atproto explorer pdsls.dev
atproto tool
at main 11 kB view raw
1import { 2 CompatibleOperationOrTombstone, 3 defs, 4 IndexedEntry, 5 IndexedEntryLog, 6 processIndexedEntryLog, 7} from "@atcute/did-plc"; 8import { createEffect, createResource, createSignal, For, Show } from "solid-js"; 9import { localDateFromTimestamp } from "../utils/date.js"; 10import { createOperationHistory, DiffEntry, groupBy } from "../utils/plc-logs.js"; 11 12type PlcEvent = "handle" | "rotation_key" | "service" | "verification_method"; 13 14export const PlcLogView = (props: { did: string }) => { 15 const [activePlcEvent, setActivePlcEvent] = createSignal<PlcEvent | undefined>(); 16 const [validLog, setValidLog] = createSignal<boolean | undefined>(undefined); 17 const [rawLogs, setRawLogs] = createSignal<IndexedEntryLog | undefined>(undefined); 18 19 const shouldShowDiff = (diff: DiffEntry) => 20 !activePlcEvent() || diff.type.startsWith(activePlcEvent()!); 21 22 const shouldShowEntry = (diffs: DiffEntry[]) => 23 !activePlcEvent() || diffs.some((d) => d.type.startsWith(activePlcEvent()!)); 24 25 const fetchPlcLogs = async () => { 26 const res = await fetch( 27 `${localStorage.plcDirectory ?? "https://plc.directory"}/${props.did}/log/audit`, 28 ); 29 const json = await res.json(); 30 const logs = defs.indexedEntryLog.parse(json); 31 setRawLogs(logs); 32 const opHistory = createOperationHistory(logs).reverse(); 33 return Array.from(groupBy(opHistory, (item) => item.orig)); 34 }; 35 36 const validateLog = async (logs: IndexedEntryLog) => { 37 try { 38 await processIndexedEntryLog(props.did as any, logs); 39 setValidLog(true); 40 } catch (e) { 41 console.error(e); 42 setValidLog(false); 43 } 44 }; 45 46 const [plcOps] = 47 createResource<[IndexedEntry<CompatibleOperationOrTombstone>, DiffEntry[]][]>(fetchPlcLogs); 48 49 createEffect(() => { 50 const logs = rawLogs(); 51 if (logs) { 52 setValidLog(undefined); 53 // Defer validation to next tick to avoid blocking rendering 54 setTimeout(() => validateLog(logs), 0); 55 } 56 }); 57 58 const FilterButton = (props: { icon: string; event: PlcEvent; label: string }) => { 59 const isActive = () => activePlcEvent() === props.event; 60 const toggleFilter = () => setActivePlcEvent(isActive() ? undefined : props.event); 61 62 return ( 63 <button 64 classList={{ 65 "flex items-center gap-1 sm:gap-1.5 rounded-lg px-3 py-2 sm:px-2 sm:py-1.5 text-base sm:text-sm transition-colors": true, 66 "bg-neutral-700 text-white dark:bg-neutral-200 dark:text-neutral-900": isActive(), 67 "bg-neutral-200 text-neutral-700 hover:bg-neutral-300 dark:bg-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-600": 68 !isActive(), 69 }} 70 onclick={toggleFilter} 71 > 72 <span class={props.icon}></span> 73 <span class="hidden font-medium sm:inline">{props.label}</span> 74 </button> 75 ); 76 }; 77 78 const DiffItem = (props: { diff: DiffEntry }) => { 79 const diff = props.diff; 80 81 const getDiffConfig = () => { 82 switch (diff.type) { 83 case "identity_created": 84 return { icon: "lucide--bell", title: "Identity created" }; 85 case "identity_tombstoned": 86 return { icon: "lucide--skull", title: "Identity tombstoned" }; 87 case "handle_added": 88 return { 89 icon: "lucide--at-sign", 90 title: "Alias added", 91 value: diff.handle, 92 isAddition: true, 93 }; 94 case "handle_removed": 95 return { 96 icon: "lucide--at-sign", 97 title: "Alias removed", 98 value: diff.handle, 99 isRemoval: true, 100 }; 101 case "handle_changed": 102 return { 103 icon: "lucide--at-sign", 104 title: "Alias updated", 105 oldValue: diff.prev_handle, 106 newValue: diff.next_handle, 107 }; 108 case "rotation_key_added": 109 return { 110 icon: "lucide--key-round", 111 title: "Rotation key added", 112 value: diff.rotation_key, 113 isAddition: true, 114 }; 115 case "rotation_key_removed": 116 return { 117 icon: "lucide--key-round", 118 title: "Rotation key removed", 119 value: diff.rotation_key, 120 isRemoval: true, 121 }; 122 case "service_added": 123 return { 124 icon: "lucide--hard-drive", 125 title: "Service added", 126 badge: diff.service_id, 127 value: diff.service_endpoint, 128 isAddition: true, 129 }; 130 case "service_removed": 131 return { 132 icon: "lucide--hard-drive", 133 title: "Service removed", 134 badge: diff.service_id, 135 value: diff.service_endpoint, 136 isRemoval: true, 137 }; 138 case "service_changed": 139 return { 140 icon: "lucide--hard-drive", 141 title: "Service updated", 142 badge: diff.service_id, 143 oldValue: diff.prev_service_endpoint, 144 newValue: diff.next_service_endpoint, 145 }; 146 case "verification_method_added": 147 return { 148 icon: "lucide--shield-check", 149 title: "Verification method added", 150 badge: diff.method_id, 151 value: diff.method_key, 152 isAddition: true, 153 }; 154 case "verification_method_removed": 155 return { 156 icon: "lucide--shield-check", 157 title: "Verification method removed", 158 badge: diff.method_id, 159 value: diff.method_key, 160 isRemoval: true, 161 }; 162 case "verification_method_changed": 163 return { 164 icon: "lucide--shield-check", 165 title: "Verification method updated", 166 badge: diff.method_id, 167 oldValue: diff.prev_method_key, 168 newValue: diff.next_method_key, 169 }; 170 default: 171 return { icon: "lucide--circle-help", title: "Unknown log entry" }; 172 } 173 }; 174 175 const config = getDiffConfig(); 176 const { 177 icon, 178 title, 179 value = "", 180 oldValue = "", 181 newValue = "", 182 badge = "", 183 isAddition = false, 184 isRemoval = false, 185 } = config; 186 187 return ( 188 <div 189 classList={{ 190 "grid grid-cols-[auto_1fr] gap-y-0.5 gap-x-2": true, 191 "opacity-70": diff.orig.nullified, 192 }} 193 > 194 <div class={`${icon} iconify shrink-0 self-center`} /> 195 <div class="flex min-w-0 items-center gap-1.5"> 196 <p 197 classList={{ 198 "font-medium text-sm": true, 199 "line-through": diff.orig.nullified, 200 }} 201 > 202 {title} 203 </p> 204 <Show when={badge}> 205 <span class="shrink-0 rounded bg-neutral-200 px-1.5 py-0.5 text-xs font-medium text-neutral-700 dark:bg-neutral-700 dark:text-neutral-300"> 206 #{badge} 207 </span> 208 </Show> 209 <Show when={diff.orig.nullified}> 210 <span class="ml-auto rounded bg-neutral-200 px-2 py-0.5 text-xs font-medium dark:bg-neutral-700"> 211 Nullified 212 </span> 213 </Show> 214 </div> 215 <Show when={value}> 216 <div></div> 217 <div 218 classList={{ 219 "text-sm break-all flex items-start gap-2 min-w-0": true, 220 "text-green-700 dark:text-green-300": isAddition, 221 "text-red-700 dark:text-red-300": isRemoval, 222 "text-neutral-600 dark:text-neutral-400": !isAddition && !isRemoval, 223 }} 224 > 225 <Show when={isAddition}> 226 <span class="shrink-0">+</span> 227 </Show> 228 <Show when={isRemoval}> 229 <span class="shrink-0"></span> 230 </Show> 231 <span class="break-all">{value}</span> 232 </div> 233 </Show> 234 <Show when={oldValue && newValue}> 235 <div></div> 236 <div class="flex min-w-0 flex-col text-sm"> 237 <div class="flex items-start gap-2 text-red-700 dark:text-red-300"> 238 <span class="shrink-0"></span> 239 <span class="break-all">{oldValue}</span> 240 </div> 241 <div class="flex items-start gap-2 text-green-700 dark:text-green-300"> 242 <span class="shrink-0">+</span> 243 <span class="break-all">{newValue}</span> 244 </div> 245 </div> 246 </Show> 247 </div> 248 ); 249 }; 250 251 return ( 252 <div class="flex w-full flex-col gap-3 wrap-anywhere"> 253 <div class="flex flex-col gap-2"> 254 <div class="flex items-center gap-1.5 text-sm"> 255 <div class="iconify lucide--filter" /> 256 <p class="font-medium">Filter by type</p> 257 </div> 258 <div class="flex flex-wrap gap-1 sm:gap-2"> 259 <FilterButton icon="iconify lucide--at-sign" event="handle" label="Alias" /> 260 <FilterButton icon="iconify lucide--hard-drive" event="service" label="Service" /> 261 <FilterButton 262 icon="iconify lucide--shield-check" 263 event="verification_method" 264 label="Verification" 265 /> 266 <FilterButton 267 icon="iconify lucide--key-round" 268 event="rotation_key" 269 label="Rotation Key" 270 /> 271 </div> 272 </div> 273 <div class="flex items-center gap-1.5 text-sm font-medium"> 274 <Show when={validLog() === true}> 275 <span class="iconify lucide--check-circle-2 text-green-500 dark:text-green-400"></span> 276 <span>Valid log</span> 277 </Show> 278 <Show when={validLog() === false}> 279 <span class="iconify lucide--x-circle text-red-500 dark:text-red-400"></span> 280 <span>Log validation failed</span> 281 </Show> 282 <Show when={validLog() === undefined}> 283 <span class="iconify lucide--loader-circle animate-spin"></span> 284 <span>Validating log...</span> 285 </Show> 286 </div> 287 <div class="flex flex-col gap-3"> 288 <For each={plcOps()}> 289 {([entry, diffs]) => ( 290 <Show when={shouldShowEntry(diffs)}> 291 <div class="flex flex-col gap-1"> 292 <span class="text-sm font-semibold text-neutral-700 dark:text-neutral-300"> 293 {localDateFromTimestamp(new Date(entry.createdAt).getTime())} 294 </span> 295 <div class="flex flex-col gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-3 text-sm dark:border-neutral-700 dark:bg-neutral-800"> 296 <For each={diffs.filter(shouldShowDiff)}> 297 {(diff) => <DiffItem diff={diff} />} 298 </For> 299 </div> 300 </div> 301 </Show> 302 )} 303 </For> 304 </div> 305 </div> 306 ); 307};