decentralised sync engine
1import { SERVER_PORT, SERVICE_DID } from "@/lib/env";
2import { issueNewLatticeToken } from "@/lib/sessions";
3import { shardSessions } from "@/lib/state";
4import { HttpGeneralErrorType } from "@/lib/types/http/errors";
5import { latticeHandshakeDataSchema } from "@/lib/types/http/handlers";
6import { systemsGmstnDevelopmentChannelRecordSchema } from "@/lib/types/lexicon/systems.gmstn.development.channel";
7import { systemsGmstnDevelopmentChannelInviteRecordSchema } from "@/lib/types/lexicon/systems.gmstn.development.channel.invite";
8import type { RouteHandler } from "@/lib/types/routes";
9import { getRecordFromFullAtUri, stringToAtUri } from "@/lib/utils/atproto";
10import {
11 newErrorResponse,
12 newSuccessResponse,
13} from "@/lib/utils/http/responses";
14import { verifyServiceJwt } from "@/lib/utils/verifyJwt";
15import { z } from "zod";
16
17export const latticeHandshakeHandler: RouteHandler = async (req) => {
18 const {
19 success: handshakeParseSuccess,
20 error: handshakeParseError,
21 data: handshakeData,
22 } = latticeHandshakeDataSchema.safeParse(req.body);
23 if (!handshakeParseSuccess) {
24 return newErrorResponse(400, {
25 message: HttpGeneralErrorType.TYPE_ERROR,
26 details: z.treeifyError(handshakeParseError),
27 });
28 }
29
30 const { interServiceJwt, memberships } = handshakeData;
31
32 const verifyJwtResult = await verifyServiceJwt(interServiceJwt);
33 if (!verifyJwtResult.ok) {
34 const { error } = verifyJwtResult;
35 return newErrorResponse(
36 401,
37 {
38 message:
39 "JWT authentication failed. Did you submit the right inter-service JWT to the right endpoint with the right signatures?",
40 details: error,
41 },
42 {
43 headers: {
44 "WWW-Authenticate":
45 'Bearer error="invalid_token", error_description="JWT signature verification failed"',
46 },
47 },
48 );
49 }
50
51 const { value: verifiedJwt } = verifyJwtResult;
52
53 // TODO:
54 // if(PRIVATE_LATTICE) doAllowCheck()
55 // see the sequence diagram for the proper flow.
56 // not implemented for now because we support public first
57
58 const pdsInviteRecordFetchPromises = memberships.map(async (membership) => {
59 const inviteAtUriResult = stringToAtUri(membership.invite.uri);
60 if (!inviteAtUriResult.ok) return;
61 const { data: inviteAtUri } = inviteAtUriResult;
62 if (!inviteAtUri.collection || !inviteAtUri.rKey) return;
63 const recordResult = await getRecordFromFullAtUri(inviteAtUri);
64 if (!recordResult.ok) {
65 console.error(
66 `something went wrong fetching the invite record from the given membership ${JSON.stringify(membership)}`,
67 );
68 return;
69 }
70 return recordResult.data;
71 });
72
73 let pdsInviteRecords;
74 try {
75 pdsInviteRecords = (
76 await Promise.all(pdsInviteRecordFetchPromises)
77 ).filter((val) => val !== undefined);
78 } catch (err) {
79 return newErrorResponse(500, {
80 message:
81 "Something went wrong when fetching membership invite records. Check the Shard logs if possible.",
82 details: err,
83 });
84 }
85
86 const {
87 success: inviteRecordsParseSuccess,
88 error: inviteRecordsParseError,
89 data: inviteRecordsParsed,
90 } = z
91 .array(systemsGmstnDevelopmentChannelInviteRecordSchema)
92 .safeParse(pdsInviteRecords);
93 if (!inviteRecordsParseSuccess) {
94 return newErrorResponse(500, {
95 message:
96 "One of the membership records provided did not resolve to a proper lexicon Invite record.",
97 details: z.treeifyError(inviteRecordsParseError),
98 });
99 }
100
101 for (const invite of inviteRecordsParsed) {
102 if (invite.recipient !== verifiedJwt.issuer)
103 return newErrorResponse(403, {
104 message:
105 "Memberships resolved to invites, but the provided JWT's issuer does not match with the recipient DIDs of the invites. Please check the provided membership records.",
106 });
107 }
108
109 const pdsChannelRecordFetchPromises = inviteRecordsParsed.map(
110 async (invite) => {
111 const channelAtUriResult = stringToAtUri(invite.channel.uri);
112 if (!channelAtUriResult.ok) return;
113 const { data: channelAtUri } = channelAtUriResult;
114 if (!channelAtUri.collection || !channelAtUri.rKey) return;
115 const recordResult = await getRecordFromFullAtUri(channelAtUri);
116 if (!recordResult.ok) {
117 console.error(
118 `something went wrong fetching the channel record from the given membership ${JSON.stringify(invite)}`,
119 );
120 throw new Error(
121 JSON.stringify({ error: recordResult.error, invite }),
122 );
123 }
124 return recordResult.data;
125 },
126 );
127
128 let pdsChannelRecords;
129 try {
130 pdsChannelRecords = await Promise.all(pdsChannelRecordFetchPromises);
131 } catch (err) {
132 return newErrorResponse(500, {
133 message:
134 "Something went wrong when fetching membership channel records. Check the Shard logs if possible.",
135 details: err,
136 });
137 }
138
139 const {
140 success: channelRecordsParseSuccess,
141 error: channelRecordsParseError,
142 data: channelRecordsParsed,
143 } = z
144 .array(systemsGmstnDevelopmentChannelRecordSchema)
145 .safeParse(pdsChannelRecords);
146 if (!channelRecordsParseSuccess) {
147 return newErrorResponse(500, {
148 message:
149 "One of the membership records provided did not resolve to a proper lexicon Channel record.",
150 details: z.treeifyError(channelRecordsParseError),
151 });
152 }
153
154 // TODO:
155 // for private shards, ensure that the channels described by constellation backlinks are made
156 // by authorised parties (check owner pds for workspace management permissions)
157 // do another fetch to owner's pds first to grab the records, then cross-reference with the
158 // did of the backlink. if there are any channels described by unauthorised parties, simply drop them.
159
160 let mismatchOrIncorrect = false;
161 const errors: Array<unknown> = [];
162 const existingShardConnectionShardDids = shardSessions
163 .keys()
164 .toArray()
165 .map((shardConnections) => {
166 return shardConnections.shardDid.slice(8);
167 });
168
169 channelRecordsParsed.forEach((channel) => {
170 if (mismatchOrIncorrect) return;
171
172 const { storeAt: storeAtRecord, routeThrough: routeThroughRecord } =
173 channel;
174
175 const routeThroughRecordParseResult = stringToAtUri(
176 routeThroughRecord.uri,
177 );
178 if (!routeThroughRecordParseResult.ok) {
179 errors.push(routeThroughRecordParseResult.error);
180 mismatchOrIncorrect = true;
181 return;
182 }
183 const routeThroughUri = routeThroughRecordParseResult.data;
184
185 // FIXME: this also assumes that the requesting lattice's DID is a did:web
186 // see below for the rest of the issues.
187 let thisLatticeDomain = SERVICE_DID.slice(8);
188 if (thisLatticeDomain === "localhost")
189 thisLatticeDomain = `localhost:${SERVER_PORT.toString()}`;
190 if (routeThroughUri.rKey !== thisLatticeDomain) {
191 errors.push(
192 "Mismatch between claimant lattice and channel routeThrough. Request wants to validate for",
193 routeThroughUri.rKey,
194 ", but this lattice is",
195 SERVICE_DID.slice(8),
196 );
197 mismatchOrIncorrect = true;
198 return;
199 }
200 const storeAtRecordParseResult = stringToAtUri(storeAtRecord.uri);
201 if (!storeAtRecordParseResult.ok) {
202 errors.push(storeAtRecordParseResult.error);
203 mismatchOrIncorrect = true;
204 return;
205 }
206 const storeAtUri = storeAtRecordParseResult.data;
207
208 // FIXME: this assumes that the current shard's SERVICE_DID is a did:web.
209 // we should resolve the full record or add something that can tell us where to find this shard.
210 // likely, we should simply resolve the described shard record, which we can technically do faaaaar earlier on in the request
211 // or even store it in memory upon first boot of a shard.
212 // also incorrectly assumes that the storeAt rkey is a domain when it can in fact be anything.
213 // we should probably just resolve this properly first but for now, i cba.
214
215 if (!storeAtUri.rKey) return;
216
217 if (
218 !existingShardConnectionShardDids.includes(
219 encodeURIComponent(storeAtUri.rKey),
220 )
221 ) {
222 errors.push(
223 "Mismatch between claimant shard and channel storeAt. Request wants to validate for",
224 storeAtUri.rKey,
225 ", but this lattice is only allowed to talk to",
226 existingShardConnectionShardDids,
227 );
228 mismatchOrIncorrect = true;
229 return;
230 }
231 });
232
233 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
234 if (mismatchOrIncorrect)
235 return newErrorResponse(400, {
236 message:
237 "Channels provided during the handshake had a mismatch between the channel values. Ensure that you are only submitting exactly the channels you have access to.",
238 details: errors,
239 });
240
241 // yipee, it's a valid request :3
242
243 const allowedChannels = inviteRecordsParsed.map((invite) => {
244 const res = stringToAtUri(invite.channel.uri);
245 if (!res.ok) return;
246 return res.data;
247 });
248
249 const sessionInfo = issueNewLatticeToken({
250 allowedChannels,
251 clientDid: verifyJwtResult.value.issuer,
252 });
253
254 return newSuccessResponse({ sessionInfo });
255};