decentralised message store

feat: resolve at uri record helper

serenity 03bdc8b1 95d994a8

Changed files
+147 -23
src
+9 -3
src/lib/types/atproto.ts
···
id: z.string(),
type: z.string(),
controller: z.string(),
-
publicKeyMultibase: z.string(),
+
publicKeyMultibase: z.optional(z.string()),
});
export type VerificationMethod = z.infer<typeof verificationMethodSchema>;
···
z.array(
z.object({
id: z.string(),
-
type: z.string(),
-
serviceEndpoint: z.string(),
+
type: z.union([z.string(), z.array(z.string())]),
+
serviceEndpoint: z.union([
+
z.string(),
+
z.record(z.string(), z.string()),
+
z.array(
+
z.union([z.string(), z.record(z.string(), z.string())]),
+
),
+
]),
}),
),
),
+8 -7
src/lib/types/constellation.ts
···
import { z } from "zod";
+
export const constellationBacklinkSchema = z.object({
+
did: z.string(),
+
collection: z.string(),
+
rkey: z.string(),
+
});
+
export type ConstellationBacklink = z.infer<typeof constellationBacklinkSchema>;
+
export const constellationBacklinkResponseSchema = z.object({
total: z.number(),
-
records: z.array(
-
z.object({
-
did: z.string(),
-
collection: z.string(),
-
rkey: z.string(),
-
}),
-
),
+
records: z.array(constellationBacklinkSchema),
cursor: z.optional(z.string()),
});
export type ConstellationBacklinkResponse = z.infer<
+14
src/lib/types/lexicon/com.atproto.repo.getRecord.ts
···
+
import { z } from "zod";
+
+
export const comAtprotoRepoGetRecordResponseSchema = z.object({
+
uri: z.string(),
+
cid: z.optional(z.string()),
+
value: z
+
.object({
+
$type: z.string(),
+
})
+
.catchall(z.unknown()),
+
});
+
export type ComAtprotoRepoGetRecordResponse = z.infer<
+
typeof comAtprotoRepoGetRecordResponseSchema
+
>;
+115
src/lib/utils/atproto.ts
···
+
import {
+
atprotoHandleSchema,
+
type AtprotoHandle,
+
type AtUri,
+
type Did,
+
type DidDocument,
+
} from "@/lib/types/atproto";
+
import { comAtprotoRepoGetRecordResponseSchema } from "@/lib/types/lexicon/com.atproto.repo.getRecord";
+
import type { Result } from "@/lib/utils/result";
+
import type { DidDocumentResolver } from "@atcute/identity-resolver";
+
import {
+
CompositeDidDocumentResolver,
+
CompositeHandleResolver,
+
DohJsonHandleResolver,
+
PlcDidDocumentResolver,
+
WebDidDocumentResolver,
+
WellKnownHandleResolver,
+
} from "@atcute/identity-resolver";
+
+
export const resolveRecordFromAtUri = async ({
+
authority,
+
collection,
+
rKey,
+
}: Required<AtUri>): Promise<Result<unknown, unknown>> => {
+
const didDocResult = await resolveDidDoc(authority);
+
if (!didDocResult.ok) return { ok: false, error: didDocResult.error };
+
+
const { service: services } = didDocResult.data;
+
if (!services)
+
return {
+
ok: false,
+
error: { message: "Resolved DID document has no service field." },
+
};
+
+
const pdsService = services.find(
+
(service) =>
+
service.id === "#atproto_pds" &&
+
service.type === "AtprotoPersonalDataServer",
+
);
+
if (!pdsService)
+
return {
+
ok: false,
+
error: {
+
message:
+
"Resolved DID document has no PDS service listed in the document.",
+
},
+
};
+
+
const pdsEndpointRecord = pdsService.serviceEndpoint;
+
let pdsEndpointUrl;
+
try {
+
// @ts-expect-error yes, we are coercing something that is explicitly not a string into a string, but in this case we want to be specific. only serviceEndpoints with valid atproto pds URLs should be allowed.
+
pdsEndpointUrl = new URL(pdsEndpointRecord);
+
} catch (err) {
+
return { ok: false, error: err };
+
}
+
+
const req = new Request(
+
`${pdsEndpointUrl}/xrpc/com.atproto.repo.getRecord?did=${didDocResult.data.id}&collection=${collection}&rkey=${rKey}`,
+
);
+
const res = await fetch(req);
+
const data: unknown = await res.json();
+
+
const {
+
success: responseParseSuccess,
+
error: responseParseError,
+
data: record,
+
} = comAtprotoRepoGetRecordResponseSchema.safeParse(data);
+
if (!responseParseSuccess) {
+
return { ok: false, error: responseParseError };
+
}
+
+
return { ok: true, data: record.value };
+
};
+
+
export const didDocResolver: DidDocumentResolver =
+
new CompositeDidDocumentResolver({
+
methods: {
+
plc: new PlcDidDocumentResolver(),
+
web: new WebDidDocumentResolver(),
+
},
+
});
+
+
export const handleResolver = new CompositeHandleResolver({
+
strategy: "dns-first",
+
methods: {
+
dns: new DohJsonHandleResolver({
+
dohUrl: "https://mozilla.cloudflare-dns.com/dns-query",
+
}),
+
http: new WellKnownHandleResolver(),
+
},
+
});
+
+
export const resolveDidDoc = async (
+
authority: Did | AtprotoHandle,
+
): Promise<Result<DidDocument, unknown>> => {
+
const { data: handle } = atprotoHandleSchema.safeParse(authority);
+
let did: Did;
+
if (handle) {
+
try {
+
did = await handleResolver.resolve(handle);
+
} catch (err) {
+
return { ok: false, error: err };
+
}
+
} else {
+
// @ts-expect-error if handle is undefined, then we know that authority must be a valid did:web or did:plc
+
did = authority;
+
}
+
try {
+
const doc: DidDocument = await didDocResolver.resolve(did);
+
return { ok: true, data: doc };
+
} catch (err) {
+
return { ok: false, error: err };
+
}
+
};
+1 -13
src/lib/utils/verifyJwt.ts
···
import { SERVICE_DID } from "@/lib/env";
-
import {
-
CompositeDidDocumentResolver,
-
PlcDidDocumentResolver,
-
WebDidDocumentResolver,
-
type DidDocumentResolver,
-
} from "@atcute/identity-resolver";
+
import { didDocResolver } from "@/lib/utils/atproto";
import { ServiceJwtVerifier } from "@atcute/xrpc-server/auth";
-
-
const didDocResolver: DidDocumentResolver = new CompositeDidDocumentResolver({
-
methods: {
-
plc: new PlcDidDocumentResolver(),
-
web: new WebDidDocumentResolver(),
-
},
-
});
export const verifyServiceJwt = async (jwt: string) => {
const verifier = new ServiceJwtVerifier({