import { readdirSync, readFileSync } from "node:fs";
import path from "node:path";
import type { Nsid } from "@atcute/lexicons/syntax";
import { describe, expect, it } from "vitest";
import { missingPermissions, type Permission } from "#lib/auth/scopes.ts";
import metadata, { loopbackClientId, loopbackScope } from "#lib/oauth-client-metadata.ts";
const KNOT_WRITE_LXMS = [
"com.atproto.repo.createRecord",
"com.atproto.repo.putRecord",
"com.atproto.repo.deleteRecord",
"com.atproto.repo.uploadBlob",
"org.tangled.repo.deleteRepo",
"org.tangled.review.submitPatch"
] as const;
describe("oauth client scope", () => {
it("grants any-audience RPC scopes for every lxm used in knot writes", () => {
const granted = new Set(metadata.scope.split(" "));
for (const lxm of KNOT_WRITE_LXMS) {
expect(
granted.has(`rpc:${lxm}?aud=*`),
`The grant must include rpc:${lxm}?aud=* or the PDS rejects tokens minted for it.`
).toBe(true);
}
});
it("grants repo scope over org.tangled.graph.attestation", () => {
expect(metadata.scope.split(" ")).toContain("repo:org.tangled.graph.attestation");
});
it("requests no sh.tangled scopes", () => {
const legacy = metadata.scope.split(" ").filter((scope) => scope.includes("sh.tangled."));
expect(legacy).toEqual([]);
});
});
const lexicons = path.resolve(import.meta.dirname, "../../../lexicons/org/tangled");
const recordCollections = (dir: string): string[] =>
readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const file = path.join(dir, entry.name);
if (entry.isDirectory()) return recordCollections(file);
const lexicon = JSON.parse(readFileSync(file, "utf8")) as {
id: string;
defs: { main?: { type?: string } };
};
return lexicon.defs.main?.type === "record" ? [lexicon.id] : [];
});
describe("liar migration scope", () => {
it("grants repo scope over every org.tangled record, since the import writes whatever the repo holds", () => {
const wanted = recordCollections(lexicons).map((collection): Permission => ({
resource: "repo",
collection: collection as Nsid,
actions: ["create"]
}));
expect(wanted.length).toBeGreaterThan(20);
expect(missingPermissions(metadata.scope, wanted)).toEqual([]);
});
});
describe("loopbackClientId", () => {
it("keeps authorize URLs within chromium's referrer length limit", () => {
const url = new URL("https://tngl.sh/oauth/authorize");
url.searchParams.set(
"client_id",
loopbackClientId("http://127.0.0.1:5173/oauth/callback", loopbackScope)
);
url.searchParams.set(
"request_uri",
`urn:ietf:params:oauth:request_uri:req-${"0".repeat(32)}`
);
expect(url.href.length).toBeLessThanOrEqual(4096);
});
});