[READ ONLY MIRROR] Spark Social AppView Server
github.com/sprksocial/server
atproto
deno
hono
lexicon
1import { DidDocument, getDid, getHandle, IdResolver } from "@atp/identity";
2import { Code, DataPlaneError } from "../util.ts";
3
4// Helper function to format DID document result
5function getResultFromDoc(doc: DidDocument) {
6 const keys: Record<string, { Type: string; PublicKeyMultibase: string }> = {};
7 doc.verificationMethod?.forEach((method) => {
8 const id = method.id.split("#").at(1);
9 if (!id) return;
10 keys[id] = {
11 Type: method.type,
12 PublicKeyMultibase: method.publicKeyMultibase || "",
13 };
14 });
15
16 const services: Record<string, { Type: string; URL: string }> = {};
17 doc.service?.forEach((service) => {
18 const id = service.id.split("#").at(1);
19 if (!id) return;
20 if (typeof service.serviceEndpoint !== "string") return;
21 services[id] = {
22 Type: service.type,
23 URL: service.serviceEndpoint,
24 };
25 });
26
27 return {
28 did: getDid(doc),
29 handle: getHandle(doc),
30 keys: JSON.stringify(keys),
31 services: JSON.stringify(services),
32 updated: new Date().toISOString(),
33 };
34}
35
36export class Identity {
37 private idResolver?: IdResolver;
38
39 constructor(idResolver?: IdResolver) {
40 this.idResolver = idResolver;
41 }
42
43 async getByDid(did: string) {
44 if (!this.idResolver) {
45 throw new DataPlaneError(Code.InternalError);
46 }
47
48 const doc = await this.idResolver.did.resolve(did);
49 if (!doc) {
50 throw new DataPlaneError(Code.NotFound);
51 }
52
53 const result = getResultFromDoc(doc);
54 return result;
55 }
56
57 async getByHandle(handle: string) {
58 if (!this.idResolver) {
59 throw new DataPlaneError(Code.InternalError);
60 }
61
62 const did = await this.idResolver.handle.resolve(handle);
63 if (!did) {
64 throw new DataPlaneError(Code.NotFound);
65 }
66
67 const doc = await this.idResolver.did.resolve(did);
68 if (!doc || did !== getDid(doc)) {
69 throw new DataPlaneError(Code.NotFound);
70 }
71
72 const result = getResultFromDoc(doc);
73 return result;
74 }
75
76 async resolve(identifier: string, type?: "did" | "handle") {
77 if (!this.idResolver) {
78 throw new DataPlaneError(Code.InternalError);
79 }
80
81 let doc: DidDocument | null = null;
82 let resolvedDid: string | null = null;
83
84 // Auto-detect type if not specified
85 const identifierType = type ||
86 (identifier.startsWith("did:") ? "did" : "handle");
87
88 if (identifierType === "did") {
89 doc = await this.idResolver.did.resolve(identifier);
90 resolvedDid = identifier;
91 } else {
92 resolvedDid = await this.idResolver.handle.resolve(identifier) || null;
93 if (resolvedDid) {
94 doc = await this.idResolver.did.resolve(resolvedDid);
95 }
96 }
97
98 if (!doc || (resolvedDid && resolvedDid !== getDid(doc))) {
99 throw new DataPlaneError(Code.NotFound);
100 }
101
102 const result = getResultFromDoc(doc);
103 return {
104 ...result,
105 resolvedFrom: {
106 identifier,
107 type: identifierType,
108 },
109 };
110 }
111
112 async resolveBatch(
113 identifiers: Array<{ value: string; type?: "did" | "handle" }>,
114 ) {
115 if (!this.idResolver) {
116 throw new DataPlaneError(Code.InternalError);
117 }
118
119 const results = await Promise.allSettled(
120 identifiers.map(async ({ value, type }) => {
121 let doc: DidDocument | null = null;
122 let resolvedDid: string | undefined;
123
124 const identifierType = type ||
125 (value.startsWith("did:") ? "did" : "handle");
126 if (identifierType === "did") {
127 doc = await this.idResolver!.did.resolve(value);
128 resolvedDid = value;
129 } else {
130 resolvedDid = await this.idResolver!.handle.resolve(value);
131 if (!resolvedDid) throw new DataPlaneError(Code.NotFound);
132 doc = await this.idResolver!.did.resolve(resolvedDid);
133 }
134
135 if (!doc || (resolvedDid && resolvedDid !== getDid(doc))) {
136 return {
137 identifier: value,
138 type: identifierType,
139 error: "Identity not found",
140 };
141 }
142 return {
143 identifier: value,
144 type: identifierType,
145 ...getResultFromDoc(doc),
146 };
147 }),
148 );
149
150 const resolved = results.map((result, index) => ({
151 index,
152 success: result.status === "fulfilled",
153 data: result.status === "fulfilled" ? result.value : null,
154 error: result.status === "rejected" ? result.reason?.message : null,
155 }));
156
157 return {
158 results: resolved,
159 summary: {
160 total: identifiers.length,
161 successful: resolved.filter((r) => r.success).length,
162 failed: resolved.filter((r) => !r.success).length,
163 },
164 };
165 }
166}