import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import { latestPath, ROUTES } from "./shared.mjs";
// a bump means no saved page can be trusted
export const RENDERER_FORMAT = 5;
export const tokenOf = (did) => createHash("sha256").update(did).digest("hex").slice(0, 32);
// svfe's url for a repo's owner: the handle, or the did when it has none
export const ownerPrefixOf = (repo) => `/${repo.owner_handle?.toLowerCase() ?? repo.owner_did}`;
export const prefixOf = (repo) => `${ownerPrefixOf(repo)}/${repo.name}`;
// a later page of a list is a route of its own, "<list>?page=N", stored next to page 1 as
// "<list>~page-N.html": no ref or path can hold a "~", so the name is never a real one
export const PAGE_QUERY = /\?page=([1-9][0-9]*)$/;
// where a route's page lives under the repo's directory: a file is its name plus ".html", a
// folder its own directory, so a file page costs no directory. the front page is index.html
export const fileOf = (prefix, route) => {
if (route === prefix) return "index.html";
const paged = PAGE_QUERY.exec(route);
const pathname = paged ? route.slice(0, paged.index) : route;
return pathname.slice(prefix.length + 1) + (paged ? `~page-${paged[1]}` : "") + ".html";
};
// a user's page, and the routes file that tells the SPA to load it as a document
export const ownerFiles = (out, ownerPrefix) => {
const at = path.join(out, "www", ownerPrefix.slice(1));
return [at + ".html", path.join(at, latestPath(ROUTES))];
};
// the stub first, so nginx never serves a stub without its page
export const unpublishOwner = async (out, ownerPrefix) => {
for (const file of ownerFiles(out, ownerPrefix)) {
await rm(file, { force: true });
await rm(file + ".gz", { force: true });
}
};
let written = 0;
// concurrent writers of one file each rename their own tmp, the last rename wins
export const atomicJson = async (file, value) => {
await mkdir(path.dirname(file), { recursive: true });
const tmp = `${file}.${process.pid}.${written++}.tmp`;
await writeFile(tmp, JSON.stringify(value));
await rename(tmp, file);
};
export const readJson = (file) => {
try {
return JSON.parse(readFileSync(file, "utf8"));
} catch {
return null;
}
};
// the knot answers a repo's manifest uri as the repo did
const MANIFEST = /^at:\/\/(did:[^/]+)\/org\.tangled\.repo\.manifest\/self$/;
// one spelling per response: the nsid and its params sorted by name, values decoded, empty
// values left out. the knot's xrpc diff spells its keys the same way
export const keyOf = (nsid, params) => {
const pairs = Object.entries(params)
.filter(([, value]) => value !== "" && value !== undefined && value !== null)
.map(([name, value]) => [name, String(value).replace(MANIFEST, "$1")])
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
return `${nsid}?${pairs.map(([name, value]) => `${name}=${value}`).join("&")}`;
};
export const keyOfUrl = (url) =>
keyOf(url.pathname.slice("/xrpc/".length), Object.fromEntries(url.searchParams));