import { isDid } from "@atcute/lexicons/syntax";
import { error } from "@sveltejs/kit";
import { serviceUrlFor, tryHostForKnot } from "#lib/api/_request.ts";
import { createBobbinClient } from "#lib/api/client.ts";
import { httpStatusFor, toHttpError } from "#lib/api/load.ts";
import { getRepoByRepoDid, type RepoView } from "#lib/api/records.ts";
import { didFromUri } from "#lib/api/uri.ts";
import { getConfig } from "#lib/server/config.ts";
import { capture } from "#lib/server/posthog.ts";
import { resolveRepoViewFromParams } from "#lib/server/repo.ts";
import type { Fetch } from "#lib/fetch.ts";
interface GitEvent<Params> {
params: Params;
url: URL;
fetch: Fetch;
platform?: App.Platform;
}
// git follows this once on info/refs and sends the rest of the clone or push
// straight to the knot
const knotRedirect = (view: RepoView, repoDid: string, service: string, url: URL) => {
const host = tryHostForKnot(view.knot);
if (!host) error(404, "This repository doesn't have a knot yet");
return new Response(null, {
status: 307,
headers: { location: `${serviceUrlFor(host)}/${repoDid}/${service}${url.search}` }
});
};
// the knot serves the clone itself, so its ref advertisement here is the one place
// we see it. fetches and pulls ask for the same thing, as they did on the appview
const trackClone = (event: GitEvent<unknown>, view: RepoView, repoDid: string, service: string) => {
if (service !== "info/refs" || event.url.searchParams.get("service") !== "git-upload-pack")
return;
const declaration = view.value.declaration;
const sent = capture(getConfig(event.platform), "clone", declaration?.owner ?? repoDid, {
repo: declaration?.name,
repo_at: view.uri
});
// on workers the event has to outlive the redirect it rides on
event.platform?.ctx?.waitUntil(sent);
};
export const redirectToKnot =
(service: string) =>
async (event: GitEvent<{ handle: string; repo: string }>): Promise<Response> => {
const { view } = await resolveRepoViewFromParams({
params: { handle: event.params.handle, repo: event.params.repo.replace(/\.git$/, "") },
fetch: event.fetch
});
const repoDid = didFromUri(view.uri);
const response = knotRedirect(view, repoDid, service, event.url);
trackClone(event, view, repoDid, service);
return response;
};
export const redirectRepoDidToKnot =
(service: string) =>
async (event: GitEvent<{ handle: string }>): Promise<Response> => {
const repoDid = event.params.handle;
if (!isDid(repoDid)) error(404, "Not found");
const ctx = createBobbinClient({ serviceUrl: getConfig().bobbinUrl, fetch: event.fetch });
const view = await getRepoByRepoDid(ctx, repoDid).catch((cause) =>
httpStatusFor(cause) === 404 ? null : toHttpError(cause, "Could not load repository")
);
if (!view) error(404, `${repoDid} does not exist`);
const response = knotRedirect(view, repoDid, service, event.url);
trackClone(event, view, repoDid, service);
return response;
};