···11-import { OWNER_DID, SERVICE_DID } from "@/lib/env";
22-import { issueNewLatticeToken } from "@/lib/sessions";
33-import { HttpGeneralErrorType } from "@/lib/types/http/errors";
44-import { handshakeDataSchema } from "@/lib/types/http/handlers";
55-import { systemsGmstnDevelopmentChannelRecordSchema } from "@/lib/types/lexicon/systems.gmstn.development.channel";
66-import type { RouteHandler } from "@/lib/types/routes";
77-import { stringToAtUri } from "@/lib/utils/atproto";
88-import {
99- getConstellationBacklink,
1010- getPdsRecordFromBacklink,
1111-} from "@/lib/utils/constellation";
1212-import {
1313- newErrorResponse,
1414- newSuccessResponse,
1515-} from "@/lib/utils/http/responses";
1616-import { verifyServiceJwt } from "@/lib/utils/verifyJwt";
1717-import { z } from "zod";
1818-1919-export const handshakeHandler: RouteHandler = async (req) => {
2020- const {
2121- success: handshakeParseSuccess,
2222- error: handshakeParseError,
2323- data: handshakeData,
2424- } = handshakeDataSchema.safeParse(req.body);
2525- if (!handshakeParseSuccess) {
2626- return newErrorResponse(400, {
2727- message: HttpGeneralErrorType.TYPE_ERROR,
2828- details: z.treeifyError(handshakeParseError),
2929- });
3030- }
3131-3232- const { interServiceJwt, channelAtUris: channelAtUriStrings } =
3333- handshakeData;
3434- const allowedChannels = channelAtUriStrings.map((channel) => {
3535- const res = stringToAtUri(channel);
3636- if (!res.ok) return;
3737- return res.data;
3838- });
3939-4040- const verifyJwtResult = await verifyServiceJwt(interServiceJwt);
4141- if (!verifyJwtResult.ok) {
4242- const { error } = verifyJwtResult;
4343- return newErrorResponse(
4444- 401,
4545- {
4646- message:
4747- "JWT authentication failed. Did you submit the right inter-service JWT to the right endpoint with the right signatures?",
4848- details: error,
4949- },
5050- {
5151- headers: {
5252- "WWW-Authenticate":
5353- 'Bearer error="invalid_token", error_description="JWT signature verification failed"',
5454- },
5555- },
5656- );
5757- }
5858-5959- // TODO:
6060- // if(PRIVATE_SHARD) doAllowCheck()
6161- // see the sequence diagram for the proper flow.
6262- // not implemented for now because we support public first
6363-6464- const constellationResponse = await getConstellationBacklink({
6565- subject: `at://${OWNER_DID}/systems.gmstn.development.shard/${SERVICE_DID.slice(8)}`,
6666- source: {
6767- nsid: "systems.gmstn.development.channel",
6868- fieldName: "storeAt.uri",
6969- },
7070- });
7171- if (!constellationResponse.ok) {
7272- const { error } = constellationResponse;
7373- if ("fetchStatus" in error)
7474- return newErrorResponse(error.fetchStatus, {
7575- message:
7676- "Could not fetch backlinks from constellation. Likely something went wrong on our side.",
7777- details: error.message,
7878- });
7979- else
8080- return newErrorResponse(400, {
8181- message: HttpGeneralErrorType.TYPE_ERROR,
8282- details: z.treeifyError(error),
8383- });
8484- }
8585-8686- const pdsRecordFetchPromises = constellationResponse.data.records.map(
8787- async (backlink) => {
8888- const recordResult = await getPdsRecordFromBacklink(backlink);
8989- if (!recordResult.ok) {
9090- console.error(
9191- `something went wrong fetching the record from the given backlink ${JSON.stringify(backlink)}`,
9292- );
9393- throw new Error(
9494- JSON.stringify({ error: recordResult.error, backlink }),
9595- );
9696- }
9797- return recordResult.data;
9898- },
9999- );
100100-101101- let pdsChannelRecords;
102102- try {
103103- pdsChannelRecords = await Promise.all(pdsRecordFetchPromises);
104104- } catch (err) {
105105- return newErrorResponse(500, {
106106- message:
107107- "Something went wrong when fetching backlink channel records. Check the Shard logs if possible.",
108108- details: err,
109109- });
110110- }
111111-112112- const {
113113- success: channelRecordsParseSuccess,
114114- error: channelRecordsParseError,
115115- data: channelRecordsParsed,
116116- } = z
117117- .array(systemsGmstnDevelopmentChannelRecordSchema)
118118- .safeParse(pdsChannelRecords);
119119- if (!channelRecordsParseSuccess) {
120120- return newErrorResponse(500, {
121121- message:
122122- "One of the backlinks returned by Constellation did not resolve to a proper lexicon Channel record.",
123123- details: z.treeifyError(channelRecordsParseError),
124124- });
125125- }
126126-127127- // TODO:
128128- // for private shards, ensure that the channels described by constellation backlinks are made
129129- // by authorised parties (check owner pds for workspace management permissions)
130130- // do another fetch to owner's pds first to grab the records, then cross-reference with the
131131- // did of the backlink. if there are any channels described by unauthorised parties, simply drop them.
132132-133133- let mismatchOrIncorrect = false;
134134- const requestingLatticeDid = verifyJwtResult.value.issuer;
135135-136136- channelRecordsParsed.forEach((channel) => {
137137- if (mismatchOrIncorrect) return;
138138-139139- const { storeAt: storeAtRecord, routeThrough: routeThroughRecord } =
140140- channel;
141141- const storeAtRecordParseResult = stringToAtUri(storeAtRecord.uri);
142142- if (!storeAtRecordParseResult.ok) {
143143- mismatchOrIncorrect = true;
144144- return;
145145- }
146146- const storeAtUri = storeAtRecordParseResult.data;
147147-148148- // FIXME: this assumes that the current shard's SERVICE_DID is a did:web.
149149- // we should resolve the full record or add something that can tell us where to find this shard.
150150- // likely, we should simply resolve the described shard record, which we can technically do faaaaar earlier on in the request
151151- // or even store it in memory upon first boot of a shard.
152152- // also incorrectly assumes that the storeAt rkey is a domain when it can in fact be anything.
153153- // we should probably just resolve this properly first but for now, i cba.
154154- if (storeAtUri.rKey !== SERVICE_DID.slice(8)) {
155155- mismatchOrIncorrect = true;
156156- return;
157157- }
158158-159159- const routeThroughRecordParseResult = stringToAtUri(
160160- routeThroughRecord.uri,
161161- );
162162- if (!routeThroughRecordParseResult.ok) {
163163- mismatchOrIncorrect = true;
164164- return;
165165- }
166166- const routeThroughUri = routeThroughRecordParseResult.data;
167167-168168- // FIXME: this also assumes that the requesting lattice's DID is a did:web
169169- // see above for the rest of the issues.
170170- if (routeThroughUri.rKey === requestingLatticeDid.slice(8)) {
171171- mismatchOrIncorrect = true;
172172- return;
173173- }
174174- });
175175-176176- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
177177- if (mismatchOrIncorrect)
178178- return newErrorResponse(400, {
179179- message:
180180- "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.",
181181- });
182182-183183- // yipee, it's a valid request :3
184184-185185- const sessionInfo = issueNewLatticeToken({
186186- allowedChannels,
187187- clientDid: verifyJwtResult.value.issuer,
188188- });
189189-190190- return newSuccessResponse({ sessionInfo });
191191-};
+232
src/lib/handlers/latticeHandshake.ts
···11+import { SERVICE_DID } from "@/lib/env";
22+import { issueNewLatticeToken } from "@/lib/sessions";
33+import { shardSessions } from "@/lib/state";
44+import { HttpGeneralErrorType } from "@/lib/types/http/errors";
55+import { latticeHandshakeDataSchema } from "@/lib/types/http/handlers";
66+import { systemsGmstnDevelopmentChannelRecordSchema } from "@/lib/types/lexicon/systems.gmstn.development.channel";
77+import { systemsGmstnDevelopmentChannelInviteRecordSchema } from "@/lib/types/lexicon/systems.gmstn.development.channel.invite";
88+import type { RouteHandler } from "@/lib/types/routes";
99+import { getRecordFromFullAtUri, stringToAtUri } from "@/lib/utils/atproto";
1010+import {
1111+ newErrorResponse,
1212+ newSuccessResponse,
1313+} from "@/lib/utils/http/responses";
1414+import { verifyServiceJwt } from "@/lib/utils/verifyJwt";
1515+import { z } from "zod";
1616+1717+export const latticeHandshakeHandler: RouteHandler = async (req) => {
1818+ const {
1919+ success: handshakeParseSuccess,
2020+ error: handshakeParseError,
2121+ data: handshakeData,
2222+ } = latticeHandshakeDataSchema.safeParse(req.body);
2323+ if (!handshakeParseSuccess) {
2424+ return newErrorResponse(400, {
2525+ message: HttpGeneralErrorType.TYPE_ERROR,
2626+ details: z.treeifyError(handshakeParseError),
2727+ });
2828+ }
2929+3030+ const { interServiceJwt, memberships } = handshakeData;
3131+3232+ const verifyJwtResult = await verifyServiceJwt(interServiceJwt);
3333+ if (!verifyJwtResult.ok) {
3434+ const { error } = verifyJwtResult;
3535+ return newErrorResponse(
3636+ 401,
3737+ {
3838+ message:
3939+ "JWT authentication failed. Did you submit the right inter-service JWT to the right endpoint with the right signatures?",
4040+ details: error,
4141+ },
4242+ {
4343+ headers: {
4444+ "WWW-Authenticate":
4545+ 'Bearer error="invalid_token", error_description="JWT signature verification failed"',
4646+ },
4747+ },
4848+ );
4949+ }
5050+5151+ const { value: verifiedJwt } = verifyJwtResult;
5252+5353+ // TODO:
5454+ // if(PRIVATE_LATTICE) doAllowCheck()
5555+ // see the sequence diagram for the proper flow.
5656+ // not implemented for now because we support public first
5757+5858+ const pdsInviteRecordFetchPromises = memberships.map(async (membership) => {
5959+ const inviteAtUriResult = stringToAtUri(membership.invite.uri);
6060+ if (!inviteAtUriResult.ok) return;
6161+ const { data: inviteAtUri } = inviteAtUriResult;
6262+ if (!inviteAtUri.collection || !inviteAtUri.rKey) return;
6363+ const recordResult = await getRecordFromFullAtUri(inviteAtUri);
6464+ if (!recordResult.ok) {
6565+ console.error(
6666+ `something went wrong fetching the record from the given membership ${JSON.stringify(membership)}`,
6767+ );
6868+ throw new Error(
6969+ JSON.stringify({ error: recordResult.error, membership }),
7070+ );
7171+ }
7272+ return recordResult.data;
7373+ });
7474+7575+ let pdsInviteRecords;
7676+ try {
7777+ pdsInviteRecords = await Promise.all(pdsInviteRecordFetchPromises);
7878+ } catch (err) {
7979+ return newErrorResponse(500, {
8080+ message:
8181+ "Something went wrong when fetching membership channel records. Check the Shard logs if possible.",
8282+ details: err,
8383+ });
8484+ }
8585+8686+ const {
8787+ success: inviteRecordsParseSuccess,
8888+ error: inviteRecordsParseError,
8989+ data: inviteRecordsParsed,
9090+ } = z
9191+ .array(systemsGmstnDevelopmentChannelInviteRecordSchema)
9292+ .safeParse(pdsInviteRecords);
9393+ if (!inviteRecordsParseSuccess) {
9494+ return newErrorResponse(500, {
9595+ message:
9696+ "One of the membership records provided did not resolve to a proper lexicon Invite record.",
9797+ details: z.treeifyError(inviteRecordsParseError),
9898+ });
9999+ }
100100+101101+ for (const invite of inviteRecordsParsed) {
102102+ if (invite.recipient !== verifiedJwt.issuer)
103103+ return newErrorResponse(403, {
104104+ message:
105105+ "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.",
106106+ });
107107+ }
108108+109109+ const pdsChannelRecordFetchPromises = inviteRecordsParsed.map(
110110+ async (invite) => {
111111+ const channelAtUriResult = stringToAtUri(invite.channel.uri);
112112+ if (!channelAtUriResult.ok) return;
113113+ const { data: channelAtUri } = channelAtUriResult;
114114+ if (!channelAtUri.collection || !channelAtUri.rKey) return;
115115+ const recordResult = await getRecordFromFullAtUri(channelAtUri);
116116+ if (!recordResult.ok) {
117117+ console.error(
118118+ `something went wrong fetching the record from the given membership ${JSON.stringify(invite)}`,
119119+ );
120120+ throw new Error(
121121+ JSON.stringify({ error: recordResult.error, invite }),
122122+ );
123123+ }
124124+ return recordResult.data;
125125+ },
126126+ );
127127+128128+ let pdsChannelRecords;
129129+ try {
130130+ pdsChannelRecords = await Promise.all(pdsChannelRecordFetchPromises);
131131+ } catch (err) {
132132+ return newErrorResponse(500, {
133133+ message:
134134+ "Something went wrong when fetching membership channel records. Check the Shard logs if possible.",
135135+ details: err,
136136+ });
137137+ }
138138+139139+ const {
140140+ success: channelRecordsParseSuccess,
141141+ error: channelRecordsParseError,
142142+ data: channelRecordsParsed,
143143+ } = z
144144+ .array(systemsGmstnDevelopmentChannelRecordSchema)
145145+ .safeParse(pdsChannelRecords);
146146+ if (!channelRecordsParseSuccess) {
147147+ return newErrorResponse(500, {
148148+ message:
149149+ "One of the membership records provided did not resolve to a proper lexicon Channel record.",
150150+ details: z.treeifyError(channelRecordsParseError),
151151+ });
152152+ }
153153+154154+ // TODO:
155155+ // for private shards, ensure that the channels described by constellation backlinks are made
156156+ // by authorised parties (check owner pds for workspace management permissions)
157157+ // do another fetch to owner's pds first to grab the records, then cross-reference with the
158158+ // did of the backlink. if there are any channels described by unauthorised parties, simply drop them.
159159+160160+ let mismatchOrIncorrect = false;
161161+ const existingShardConnectionShardDids = shardSessions
162162+ .keys()
163163+ .toArray()
164164+ .map((shardConnections) => {
165165+ return shardConnections.shardDid.slice(8);
166166+ });
167167+168168+ channelRecordsParsed.forEach((channel) => {
169169+ if (mismatchOrIncorrect) return;
170170+171171+ const { storeAt: storeAtRecord, routeThrough: routeThroughRecord } =
172172+ channel;
173173+174174+ const routeThroughRecordParseResult = stringToAtUri(
175175+ routeThroughRecord.uri,
176176+ );
177177+ if (!routeThroughRecordParseResult.ok) {
178178+ mismatchOrIncorrect = true;
179179+ return;
180180+ }
181181+ const routeThroughUri = routeThroughRecordParseResult.data;
182182+183183+ // FIXME: this also assumes that the requesting lattice's DID is a did:web
184184+ // see below for the rest of the issues.
185185+ if (routeThroughUri.rKey === SERVICE_DID.slice(8)) {
186186+ mismatchOrIncorrect = true;
187187+ return;
188188+ }
189189+ const storeAtRecordParseResult = stringToAtUri(storeAtRecord.uri);
190190+ if (!storeAtRecordParseResult.ok) {
191191+ mismatchOrIncorrect = true;
192192+ return;
193193+ }
194194+ const storeAtUri = storeAtRecordParseResult.data;
195195+196196+ // FIXME: this assumes that the current shard's SERVICE_DID is a did:web.
197197+ // we should resolve the full record or add something that can tell us where to find this shard.
198198+ // likely, we should simply resolve the described shard record, which we can technically do faaaaar earlier on in the request
199199+ // or even store it in memory upon first boot of a shard.
200200+ // also incorrectly assumes that the storeAt rkey is a domain when it can in fact be anything.
201201+ // we should probably just resolve this properly first but for now, i cba.
202202+203203+ if (!storeAtUri.rKey) return;
204204+205205+ if (!existingShardConnectionShardDids.includes(storeAtUri.rKey)) {
206206+ mismatchOrIncorrect = true;
207207+ return;
208208+ }
209209+ });
210210+211211+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
212212+ if (mismatchOrIncorrect)
213213+ return newErrorResponse(400, {
214214+ message:
215215+ "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.",
216216+ });
217217+218218+ // yipee, it's a valid request :3
219219+220220+ const allowedChannels = inviteRecordsParsed.map((invite) => {
221221+ const res = stringToAtUri(invite.channel.uri);
222222+ if (!res.ok) return;
223223+ return res.data;
224224+ });
225225+226226+ const sessionInfo = issueNewLatticeToken({
227227+ allowedChannels,
228228+ clientDid: verifyJwtResult.value.issuer,
229229+ });
230230+231231+ return newSuccessResponse({ sessionInfo });
232232+};