decentralised message store

feat: handshake

FINALLY

serenity e3419096 ceef6ff0

Changed files
+193
src
lib
handlers
routes
handshake
+186
src/lib/handlers/handshake.ts
···
···
+
import { OWNER_DID, SERVICE_DID } from "@/lib/env";
+
import { generateSessionId, generateSessionInfo } from "@/lib/sessions";
+
import { systemsGmstnDevelopmentChannelRecordSchema } from "@/lib/types/gmstn";
+
import { HttpGeneralErrorType } from "@/lib/types/http/errors";
+
import { handshakeDataSchema } from "@/lib/types/http/handlers";
+
import type { RouteHandler } from "@/lib/types/routes";
+
import { stringToAtUri } from "@/lib/utils/atproto";
+
import {
+
getConstellationBacklink,
+
getPdsRecordFromBacklink,
+
} from "@/lib/utils/constellation";
+
import {
+
newErrorResponse,
+
newSuccessResponse,
+
} from "@/lib/utils/http/responses";
+
import { verifyServiceJwt } from "@/lib/utils/verifyJwt";
+
import { z } from "zod";
+
+
export const handshakeHandler: RouteHandler = async (req) => {
+
const {
+
success: handshakeParseSuccess,
+
error: handshakeParseError,
+
data: handshakeData,
+
} = handshakeDataSchema.safeParse(req.body);
+
if (!handshakeParseSuccess) {
+
return newErrorResponse(400, {
+
message: HttpGeneralErrorType.TYPE_ERROR,
+
details: z.treeifyError(handshakeParseError),
+
});
+
}
+
+
const { interServiceJwt } = handshakeData;
+
+
const verifyJwtResult = await verifyServiceJwt(interServiceJwt);
+
if (!verifyJwtResult.ok) {
+
const { error } = verifyJwtResult;
+
return newErrorResponse(
+
401,
+
{
+
message:
+
"JWT authentication failed. Did you submit the right inter-service JWT to the right endpoint with the right signatures?",
+
details: error,
+
},
+
{
+
headers: {
+
"WWW-Authenticate":
+
'Bearer error="invalid_token", error_description="JWT signature verification failed"',
+
},
+
},
+
);
+
}
+
+
// TODO:
+
// if(PRIVATE_SHARD) doAllowCheck()
+
// see the sequence diagram for the proper flow.
+
// not implemented for now because we support public first
+
+
const constellationResponse = await getConstellationBacklink({
+
subject: OWNER_DID,
+
source: {
+
nsid: "systems.gmstn.development.shard",
+
fieldName: "storeAt.uri",
+
},
+
});
+
if (!constellationResponse.ok) {
+
const { error } = constellationResponse;
+
if ("fetchStatus" in error)
+
return newErrorResponse(error.fetchStatus, {
+
message:
+
"Could not fetch backlinks from constellation. Likely something went wrong on our side.",
+
details: error.message,
+
});
+
else
+
return newErrorResponse(400, {
+
message: HttpGeneralErrorType.TYPE_ERROR,
+
details: z.treeifyError(error),
+
});
+
}
+
+
const pdsRecordFetchPromises = constellationResponse.data.records.map(
+
async (backlink) => {
+
const recordResult = await getPdsRecordFromBacklink(backlink);
+
if (!recordResult.ok) {
+
console.error(
+
`something went wrong fetching the record from the given backlink ${JSON.stringify(backlink)}`,
+
);
+
throw new Error(
+
JSON.stringify({ error: recordResult.error, backlink }),
+
);
+
}
+
return recordResult.data;
+
},
+
);
+
+
let pdsChannelRecords;
+
try {
+
pdsChannelRecords = await Promise.all(pdsRecordFetchPromises);
+
} catch (err) {
+
return newErrorResponse(500, {
+
message:
+
"Something went wrong when fetching backlink channel records. Check the Shard logs if possible.",
+
details: err,
+
});
+
}
+
+
const {
+
success: channelRecordsParseSuccess,
+
error: channelRecordsParseError,
+
data: channelRecordsParsed,
+
} = z
+
.array(systemsGmstnDevelopmentChannelRecordSchema)
+
.safeParse(pdsChannelRecords);
+
if (!channelRecordsParseSuccess) {
+
return newErrorResponse(500, {
+
message:
+
"One of the backlinks returned by Constellation did not resolve to a proper lexicon Channel record.",
+
details: z.treeifyError(channelRecordsParseError),
+
});
+
}
+
+
// TODO:
+
// for private shards, ensure that the channels described by constellation backlinks are made
+
// by authorised parties (check owner pds for workspace management permissions)
+
// do another fetch to owner's pds first to grab the records, then cross-reference with the
+
// did of the backlink. if there are any channels described by unauthorised parties, simply drop them.
+
+
let mismatchOrIncorrect = false;
+
const requestingLatticeDid = verifyJwtResult.value.issuer;
+
+
channelRecordsParsed.forEach((channel) => {
+
if (mismatchOrIncorrect) return;
+
+
const { storeAt: storeAtRecord, routeThrough: routeThroughRecord } =
+
channel;
+
// @ts-expect-error atcute's schema shape gives an unknown when it should give string. will probably swap to a different library eventually.
+
const storeAtRecordParseResult = stringToAtUri(storeAtRecord.uri);
+
if (!storeAtRecordParseResult.ok) {
+
mismatchOrIncorrect = true;
+
return;
+
}
+
const storeAtUri = storeAtRecordParseResult.data;
+
+
// FIXME: this assumes that the current shard's SERVICE_DID is a did:web.
+
// we should resolve the full record or add something that can tell us where to find this shard.
+
// likely, we should simply resolve the described shard record, which we can technically do faaaaar earlier on in the request
+
// or even store it in memory upon first boot of a shard.
+
// also incorrectly assumes that the storeAt rkey is a domain when it can in fact be anything.
+
// we should probably just resolve this properly first but for now, i cba.
+
if (storeAtUri.rKey !== SERVICE_DID.slice(8)) {
+
mismatchOrIncorrect = true;
+
return;
+
}
+
+
const routeThroughRecordParseResult = stringToAtUri(
+
// @ts-expect-error atcute's schema shape gives an unknown when it should give string. will probably swap to a different library eventually.
+
routeThroughRecord.uri,
+
);
+
if (!routeThroughRecordParseResult.ok) {
+
mismatchOrIncorrect = true;
+
return;
+
}
+
const routeThroughUri = routeThroughRecordParseResult.data;
+
+
// FIXME: this also assumes that the requesting lattice's DID is a did:web
+
// see above for the rest of the issues.
+
if (routeThroughUri.rKey === requestingLatticeDid.slice(8)) {
+
mismatchOrIncorrect = true;
+
return;
+
}
+
});
+
+
// we are mutating this above @typescript-eslint/no-unnecessary-condition
+
// eslint-disable-next-line
+
if (mismatchOrIncorrect)
+
return newErrorResponse(400, {
+
message:
+
"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.",
+
});
+
+
// yipee, it's a valid request :3
+
+
const sessionId = generateSessionId();
+
const sessionInfo = generateSessionInfo(sessionId);
+
+
return newSuccessResponse({ sessionInfo });
+
};
+7
src/routes/handshake/route.ts
···
···
+
import { handshakeHandler } from "@/lib/handlers/handshake";
+
import type { Route } from "@/lib/types/routes";
+
+
export const handshakeRoute: Route = {
+
method: "POST",
+
handler: handshakeHandler,
+
};