Suite of AT Protocol TypeScript libraries built on web standards
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>;