decentralised sync engine
0
fork

Configure Feed

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

feat: client lattice handshake

serenity fe7ed217 2b18a611

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