atproto explorer pdsls.dev
atproto tool
at main 11 kB view raw
1// courtesy of the best 🐇 mary 2// https://github.com/mary-ext/boat/blob/trunk/src/views/identity/plc-oplogs.tsx 3import { IndexedEntry, Service } from "@atcute/did-plc"; 4 5export type DiffEntry = 6 | { 7 type: "identity_created"; 8 orig: IndexedEntry; 9 nullified: boolean; 10 at: string; 11 rotationKeys: string[]; 12 verificationMethods: Record<string, string>; 13 alsoKnownAs: string[]; 14 services: Record<string, { type: string; endpoint: string }>; 15 } 16 | { 17 type: "identity_tombstoned"; 18 orig: IndexedEntry; 19 nullified: boolean; 20 at: string; 21 } 22 | { 23 type: "rotation_key_added"; 24 orig: IndexedEntry; 25 nullified: boolean; 26 at: string; 27 rotation_key: string; 28 } 29 | { 30 type: "rotation_key_removed"; 31 orig: IndexedEntry; 32 nullified: boolean; 33 at: string; 34 rotation_key: string; 35 } 36 | { 37 type: "verification_method_added"; 38 orig: IndexedEntry; 39 nullified: boolean; 40 at: string; 41 method_id: string; 42 method_key: string; 43 } 44 | { 45 type: "verification_method_removed"; 46 orig: IndexedEntry; 47 nullified: boolean; 48 at: string; 49 method_id: string; 50 method_key: string; 51 } 52 | { 53 type: "verification_method_changed"; 54 orig: IndexedEntry; 55 nullified: boolean; 56 at: string; 57 method_id: string; 58 prev_method_key: string; 59 next_method_key: string; 60 } 61 | { 62 type: "handle_added"; 63 orig: IndexedEntry; 64 nullified: boolean; 65 at: string; 66 handle: string; 67 } 68 | { 69 type: "handle_removed"; 70 orig: IndexedEntry; 71 nullified: boolean; 72 at: string; 73 handle: string; 74 } 75 | { 76 type: "handle_changed"; 77 orig: IndexedEntry; 78 nullified: boolean; 79 at: string; 80 prev_handle: string; 81 next_handle: string; 82 } 83 | { 84 type: "service_added"; 85 orig: IndexedEntry; 86 nullified: boolean; 87 at: string; 88 service_id: string; 89 service_type: string; 90 service_endpoint: string; 91 } 92 | { 93 type: "service_removed"; 94 orig: IndexedEntry; 95 nullified: boolean; 96 at: string; 97 service_id: string; 98 service_type: string; 99 service_endpoint: string; 100 } 101 | { 102 type: "service_changed"; 103 orig: IndexedEntry; 104 nullified: boolean; 105 at: string; 106 service_id: string; 107 prev_service_type: string; 108 next_service_type: string; 109 prev_service_endpoint: string; 110 next_service_endpoint: string; 111 }; 112 113export const createOperationHistory = (entries: IndexedEntry[]): DiffEntry[] => { 114 const history: DiffEntry[] = []; 115 116 for (let idx = 0, len = entries.length; idx < len; idx++) { 117 const entry = entries[idx]; 118 const op = entry.operation; 119 120 if (op.type === "create") { 121 history.push({ 122 type: "identity_created", 123 orig: entry, 124 nullified: entry.nullified, 125 at: entry.createdAt, 126 rotationKeys: [op.recoveryKey, op.signingKey], 127 verificationMethods: { atproto: op.signingKey }, 128 alsoKnownAs: [`at://${op.handle}`], 129 services: { 130 atproto_pds: { 131 type: "AtprotoPersonalDataServer", 132 endpoint: op.service, 133 }, 134 }, 135 }); 136 } else if (op.type === "plc_operation") { 137 const prevOp = findLastMatching(entries, (entry) => !entry.nullified, idx - 1)?.operation; 138 139 let oldRotationKeys: string[]; 140 let oldVerificationMethods: Record<string, string>; 141 let oldAlsoKnownAs: string[]; 142 let oldServices: Record<string, Service>; 143 144 if (!prevOp) { 145 history.push({ 146 type: "identity_created", 147 orig: entry, 148 nullified: entry.nullified, 149 at: entry.createdAt, 150 rotationKeys: op.rotationKeys, 151 verificationMethods: op.verificationMethods, 152 alsoKnownAs: op.alsoKnownAs, 153 services: op.services, 154 }); 155 156 continue; 157 } else if (prevOp.type === "create") { 158 oldRotationKeys = [prevOp.recoveryKey, prevOp.signingKey]; 159 oldVerificationMethods = { atproto: prevOp.signingKey }; 160 oldAlsoKnownAs = [`at://${prevOp.handle}`]; 161 oldServices = { 162 atproto_pds: { 163 type: "AtprotoPersonalDataServer", 164 endpoint: prevOp.service, 165 }, 166 }; 167 } else if (prevOp.type === "plc_operation") { 168 oldRotationKeys = prevOp.rotationKeys; 169 oldVerificationMethods = prevOp.verificationMethods; 170 oldAlsoKnownAs = prevOp.alsoKnownAs; 171 oldServices = prevOp.services; 172 } else { 173 continue; 174 } 175 176 // Check for rotation key changes 177 { 178 const additions = difference(op.rotationKeys, oldRotationKeys); 179 const removals = difference(oldRotationKeys, op.rotationKeys); 180 181 for (const key of additions) { 182 history.push({ 183 type: "rotation_key_added", 184 orig: entry, 185 nullified: entry.nullified, 186 at: entry.createdAt, 187 rotation_key: key, 188 }); 189 } 190 191 for (const key of removals) { 192 history.push({ 193 type: "rotation_key_removed", 194 orig: entry, 195 nullified: entry.nullified, 196 at: entry.createdAt, 197 rotation_key: key, 198 }); 199 } 200 } 201 202 // Check for verification method changes 203 { 204 for (const id in op.verificationMethods) { 205 if (!(id in oldVerificationMethods)) { 206 history.push({ 207 type: "verification_method_added", 208 orig: entry, 209 nullified: entry.nullified, 210 at: entry.createdAt, 211 method_id: id, 212 method_key: op.verificationMethods[id], 213 }); 214 } else if (op.verificationMethods[id] !== oldVerificationMethods[id]) { 215 history.push({ 216 type: "verification_method_changed", 217 orig: entry, 218 nullified: entry.nullified, 219 at: entry.createdAt, 220 method_id: id, 221 prev_method_key: oldVerificationMethods[id], 222 next_method_key: op.verificationMethods[id], 223 }); 224 } 225 } 226 227 for (const id in oldVerificationMethods) { 228 if (!(id in op.verificationMethods)) { 229 history.push({ 230 type: "verification_method_removed", 231 orig: entry, 232 nullified: entry.nullified, 233 at: entry.createdAt, 234 method_id: id, 235 method_key: oldVerificationMethods[id], 236 }); 237 } 238 } 239 } 240 241 // Check for handle changes 242 if (op.alsoKnownAs.length === 1 && oldAlsoKnownAs.length === 1) { 243 if (op.alsoKnownAs[0] !== oldAlsoKnownAs[0]) { 244 history.push({ 245 type: "handle_changed", 246 orig: entry, 247 nullified: entry.nullified, 248 at: entry.createdAt, 249 prev_handle: oldAlsoKnownAs[0], 250 next_handle: op.alsoKnownAs[0], 251 }); 252 } 253 } else { 254 const additions = difference(op.alsoKnownAs, oldAlsoKnownAs); 255 const removals = difference(oldAlsoKnownAs, op.alsoKnownAs); 256 257 for (const handle of additions) { 258 history.push({ 259 type: "handle_added", 260 orig: entry, 261 nullified: entry.nullified, 262 at: entry.createdAt, 263 handle: handle, 264 }); 265 } 266 267 for (const handle of removals) { 268 history.push({ 269 type: "handle_removed", 270 orig: entry, 271 nullified: entry.nullified, 272 at: entry.createdAt, 273 handle: handle, 274 }); 275 } 276 } 277 278 // Check for service changes 279 { 280 for (const id in op.services) { 281 if (!(id in oldServices)) { 282 history.push({ 283 type: "service_added", 284 orig: entry, 285 nullified: entry.nullified, 286 at: entry.createdAt, 287 service_id: id, 288 service_type: op.services[id].type, 289 service_endpoint: op.services[id].endpoint, 290 }); 291 } else if (!dequal(op.services[id], oldServices[id])) { 292 history.push({ 293 type: "service_changed", 294 orig: entry, 295 nullified: entry.nullified, 296 at: entry.createdAt, 297 service_id: id, 298 prev_service_type: oldServices[id].type, 299 next_service_type: op.services[id].type, 300 prev_service_endpoint: oldServices[id].endpoint, 301 next_service_endpoint: op.services[id].endpoint, 302 }); 303 } 304 } 305 306 for (const id in oldServices) { 307 if (!(id in op.services)) { 308 history.push({ 309 type: "service_removed", 310 orig: entry, 311 nullified: entry.nullified, 312 at: entry.createdAt, 313 service_id: id, 314 service_type: oldServices[id].type, 315 service_endpoint: oldServices[id].endpoint, 316 }); 317 } 318 } 319 } 320 } else if (op.type === "plc_tombstone") { 321 history.push({ 322 type: "identity_tombstoned", 323 orig: entry, 324 nullified: entry.nullified, 325 at: entry.createdAt, 326 }); 327 } 328 } 329 330 return history; 331}; 332 333function findLastMatching<T, S extends T>( 334 arr: T[], 335 predicate: (item: T) => item is S, 336 start?: number, 337): S | undefined; 338function findLastMatching<T>( 339 arr: T[], 340 predicate: (item: T) => boolean, 341 start?: number, 342): T | undefined; 343function findLastMatching<T>( 344 arr: T[], 345 predicate: (item: T) => boolean, 346 start: number = arr.length - 1, 347): T | undefined { 348 for (let i = start, v: any; i >= 0; i--) { 349 if (predicate((v = arr[i]))) { 350 return v; 351 } 352 } 353 354 return undefined; 355} 356 357function difference<T>(a: readonly T[], b: readonly T[]): T[] { 358 const set = new Set(b); 359 return a.filter((value) => !set.has(value)); 360} 361 362const dequal = (a: any, b: any): boolean => { 363 let ctor: any; 364 let len: number; 365 366 if (a === b) { 367 return true; 368 } 369 370 if (a && b && (ctor = a.constructor) === b.constructor) { 371 if (ctor === Array) { 372 if ((len = a.length) === b.length) { 373 while (len--) { 374 if (!dequal(a[len], b[len])) { 375 return false; 376 } 377 } 378 } 379 380 return len === -1; 381 } else if (!ctor || ctor === Object) { 382 len = 0; 383 384 for (ctor in a) { 385 len++; 386 387 if (!(ctor in b) || !dequal(a[ctor], b[ctor])) { 388 return false; 389 } 390 } 391 392 return Object.keys(b).length === len; 393 } 394 } 395 396 return a !== a && b !== b; 397}; 398 399export const groupBy = <K, T>(items: T[], keyFn: (item: T, index: number) => K): Map<K, T[]> => { 400 const map = new Map<K, T[]>(); 401 402 for (let idx = 0, len = items.length; idx < len; idx++) { 403 const val = items[idx]; 404 const key = keyFn(val, idx); 405 406 const list = map.get(key); 407 408 if (list !== undefined) { 409 list.push(val); 410 } else { 411 map.set(key, [val]); 412 } 413 } 414 415 return map; 416};