Suite of AT Protocol TypeScript libraries built on web standards
21
fork

Configure Feed

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

at main 193 lines 4.7 kB view raw
1import { z } from "zod"; 2 3export const isValidDidDoc = (doc: unknown): doc is DidDocument => { 4 return didDocument.safeParse(doc).success; 5}; 6 7export const getDid = (doc: DidDocument): string => { 8 const id = doc.id; 9 if (typeof id !== "string") { 10 throw new Error("No `id` on document"); 11 } 12 return id; 13}; 14 15export const getHandle = (doc: DidDocument): string | undefined => { 16 const aka = doc.alsoKnownAs; 17 if (aka) { 18 for (let i = 0; i < aka.length; i++) { 19 const alias = aka[i]; 20 if (alias.startsWith("at://")) { 21 return alias.slice(5); 22 } 23 } 24 } 25 return undefined; 26}; 27 28export const getSigningKey = ( 29 doc: DidDocument, 30): { type: string; publicKeyMultibase: string } | undefined => { 31 return getVerificationMaterial(doc, "atproto"); 32}; 33 34export const getVerificationMaterial = ( 35 doc: DidDocument, 36 keyId: string, 37): { type: string; publicKeyMultibase: string } | undefined => { 38 const key = findItemById(doc, "verificationMethod", `#${keyId}`); 39 if (!key) { 40 return undefined; 41 } 42 43 if (!key.publicKeyMultibase) { 44 return undefined; 45 } 46 47 return { 48 type: key.type, 49 publicKeyMultibase: key.publicKeyMultibase, 50 }; 51}; 52 53export const getSigningDidKey = (doc: DidDocument): string | undefined => { 54 const parsed = getSigningKey(doc); 55 if (!parsed) return; 56 return `did:key:${parsed.publicKeyMultibase}`; 57}; 58 59export const getPdsEndpoint = (doc: DidDocument): string | undefined => { 60 return getServiceEndpoint(doc, { 61 id: "#atproto_pds", 62 type: "AtprotoPersonalDataServer", 63 }); 64}; 65 66export const getFeedGenEndpoint = (doc: DidDocument): string | undefined => { 67 return getServiceEndpoint(doc, { 68 id: "#bsky_fg", 69 type: "BskyFeedGenerator", 70 }); 71}; 72 73export const getNotifEndpoint = (doc: DidDocument): string | undefined => { 74 return getServiceEndpoint(doc, { 75 id: "#bsky_notif", 76 type: "BskyNotificationService", 77 }); 78}; 79 80export const getServiceEndpoint = ( 81 doc: DidDocument, 82 opts: { id: string; type?: string }, 83): string | undefined => { 84 const service = findItemById(doc, "service", opts.id); 85 if (!service) { 86 return undefined; 87 } 88 89 if (opts.type && service.type !== opts.type) { 90 return undefined; 91 } 92 93 if (typeof service.serviceEndpoint !== "string") { 94 return undefined; 95 } 96 97 return validateUrl(service.serviceEndpoint); 98}; 99 100function findItemById< 101 D extends DidDocument, 102 T extends "verificationMethod" | "service", 103>(doc: D, type: T, id: string): NonNullable<D[T]>[number] | undefined; 104function findItemById( 105 doc: DidDocument, 106 type: "verificationMethod" | "service", 107 id: string, 108) { 109 const items = doc[type]; 110 if (items) { 111 for (let i = 0; i < items.length; i++) { 112 const item = items[i]; 113 const itemId = item.id; 114 115 if ( 116 itemId[0] === "#" 117 ? itemId === id 118 : itemId.length === doc.id.length + id.length && 119 itemId[doc.id.length] === "#" && 120 itemId.endsWith(id) && 121 itemId.startsWith(doc.id) 122 ) { 123 return item; 124 } 125 } 126 } 127 return undefined; 128} 129 130const validateUrl = (urlStr: string): string | undefined => { 131 if (!urlStr.startsWith("http://") && !urlStr.startsWith("https://")) { 132 return undefined; 133 } 134 135 if (!canParseUrl(urlStr)) { 136 return undefined; 137 } 138 139 return urlStr; 140}; 141 142const canParseUrl = URL.canParse ?? 143 ((urlStr: string): boolean => { 144 try { 145 new URL(urlStr); 146 return true; 147 } catch { 148 return false; 149 } 150 }); 151 152const verificationMethod: VerificationMethod = z.object({ 153 id: z.string(), 154 type: z.string(), 155 controller: z.string(), 156 publicKeyMultibase: z.string().optional(), 157}); 158 159type VerificationMethod = z.ZodObject<{ 160 id: z.ZodString; 161 type: z.ZodString; 162 controller: z.ZodString; 163 publicKeyMultibase: z.ZodOptional<z.ZodString>; 164}, z.core.$strip>; 165 166const service: Service = z.object({ 167 id: z.string(), 168 type: z.string(), 169 serviceEndpoint: z.union([z.string(), z.record(z.string(), z.unknown())]), 170}); 171 172type Service = z.ZodObject<{ 173 id: z.ZodString; 174 type: z.ZodString; 175 serviceEndpoint: z.ZodUnion< 176 readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>] 177 >; 178}, z.core.$strip>; 179 180export const didDocument: DidDocumentType = z.object({ 181 id: z.string(), 182 alsoKnownAs: z.array(z.string()).optional(), 183 verificationMethod: z.array(verificationMethod).optional(), 184 service: z.array(service).optional(), 185}); 186type DidDocumentType = z.ZodObject<{ 187 id: z.ZodString; 188 alsoKnownAs: z.ZodOptional<z.ZodArray<z.ZodString>>; 189 verificationMethod: z.ZodOptional<z.ZodArray<VerificationMethod>>; 190 service: z.ZodOptional<z.ZodArray<Service>>; 191}, z.core.$strip>; 192 193export type DidDocument = z.infer<DidDocumentType>;