Something went wrong. Try again.
Monorepo for Tangled forked from tangled.org/core
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492import type { Did, ResourceUri } from "@atcute/lexicons/syntax";
import { config, handleOf, seeded, type Account } from "./config";import type { Comment, Declaration, LabelDefinition, Profile, Ticket } from "./lexicons";import { get, ok, partsOf, recordUrl, request, retry429, selfUri, stamp, xrpc, type Written} from "./net";
export interface RepoOptions { spindle?: string; forkOf?: string; importFrom?: string;}
const sessions = new Map<string, Promise<string>>();
export const sessionOf = (who: Account) => { if (!sessions.has(who.did)) { const login = request(`${config.pdsUrl}/xrpc/com.atproto.server.createSession`, { method: "POST", json: { identifier: who.handle, password: config.password } }).then(({ status, body }) => { if (status !== 200) throw new Error( `pds refused ${who.name}'s login: ${status} ${JSON.stringify(body)}` ); return body.accessJwt as string; }); // otherwise a refused login stays cached and every call after it fails login.catch(() => sessions.delete(who.did)); sessions.set(who.did, login); } return sessions.get(who.did)!;};
// 401 on login means the account is gone, the other texts are not-found answersconst GONE = /not ?found|does not exist|no .* to erase|pds refused .*login: 401/i;export const alreadyGone = (cause: unknown) => GONE.test(String(cause));
export const getOk = async (url: string) => ok(url, await get(url));
export const tokenFor = async (who: Account, aud: string, lxm: string): Promise<string> => { const auth = await request( xrpc(config.pdsUrl, "com.atproto.server.getServiceAuth", { aud, lxm }), { token: await sessionOf(who) } ); return ok("pds getServiceAuth", auth).token;};
// calls an xrpc on a service using service authexport const serviceDo = async ( who: Account, base: string, aud: string, nsid: string, { json, params }: { json?: object; params?: Record<string, string> } = {}) => request(xrpc(base, nsid, params), { method: json === undefined ? "GET" : "POST", token: await tokenFor(who, aud, nsid), json });
export const spindleGet = (who: Account, nsid: string, params: Record<string, string>) => serviceDo(who, config.spindleUrl, config.spindleDid, nsid, { params });export const spindlePost = (who: Account, nsid: string, json: object) => serviceDo(who, config.spindleUrl, config.spindleDid, nsid, { json });
const QUOTA = "org.tangled.temp.spindle.quota";const spindleAdmin = seeded.alice;export interface QuotaUsage { scope: "user" | "repo"; did: string; resource: string; used: number;}export const quotas = { set: async (did: string, resource: string, limit: number) => ok( `spindle ${QUOTA}.set`, await spindlePost(spindleAdmin, `${QUOTA}.set`, { did, resource, limit }) ), unset: async (did: string, resource: string) => ok( `spindle ${QUOTA}.unset`, await spindlePost(spindleAdmin, `${QUOTA}.unset`, { did, resource }) ), usage: async (did: string) => (ok(`spindle ${QUOTA}.usage`, await spindleGet(spindleAdmin, `${QUOTA}.usage`, { did })) .usages as QuotaUsage[]) ?? []};
// the knot takes a service token once, so every retry mints a new oneconst serviceReply = (who: Account, aud: string, nsid: string, input: object) => retry429(() => serviceDo(who, config.knotUrl, aud, nsid, { json: input }));
export const asKnot = (who: Account, nsid: string, input: object) => serviceReply(who, config.knotDid, nsid, input);
const serviceCall = async (who: Account, aud: string, nsid: string, input: object) => ok(`knot ${nsid}`, await serviceReply(who, aud, nsid, input));
const knotCall = (who: Account, repo: string, nsid: string, input: object) => serviceCall(who, repo, nsid, { repo, ...input });
// unlike knotCall this doesn't throw on a refusal, the caller checks the statusexport const knotReply = (who: Account, repo: string, nsid: string, input: object) => serviceReply(who, repo, nsid, { repo, ...input });
const IMPORT_STATUS = "org.tangled.repo.getImportStatus";// the token is addressed to the repo, like web's, not to the knotexport const importStatus = async (repo: string, who?: Account) => request( xrpc(config.knotUrl, IMPORT_STATUS, { repo }), who ? { token: await tokenFor(who, repo, IMPORT_STATUS) } : {} );
// knotmirror forwards the token to the knot's receive-pack, so it's minted for thatexport const mirrorMerge = async ( who: Account, repo: string, branch: string, target: string, source: string) => request(xrpc(config.bobbinUrl, "sh.tangled.git.mergeCommit"), { method: "POST", token: await tokenFor(who, config.knotDid, "org.tangled.git.receivePack"), json: { target: { $type: "sh.tangled.git.mergeCommit#target", repo, branch, commit: target }, source: { $type: "sh.tangled.git.mergeCommit#source", repo, commit: source }, style: "rebase" } });
export type Policy = "anyone" | "collaborators" | "members";const MANIFEST = "org.tangled.repo.manifest";// the policy has no endpoint of its own because it lives on the manifestexport const setPolicy = async (who: Account, repo: string, policy: Policy) => { const manifest = await knotRecord(selfUri(repo, MANIFEST)); if (!manifest) throw new Error(`the knot has no manifest for ${repo}`); await knotCall(who, repo, "com.atproto.repo.putRecord", { collection: MANIFEST, rkey: "self", record: { ...manifest.value, contributionPolicy: policy }, swapRecord: manifest.cid });};
export const pdsCall = async (who: Account, nsid: string, input: object) => ok( `pds ${nsid}`, await retry429(async () => request(`${config.pdsUrl}/xrpc/${nsid}`, { method: "POST", token: await sessionOf(who), json: { repo: who.did, ...input } }) ) );
const admin = async (nsid: string, json: object) => { if (!config.pdsAdminPassword) throw new Error("set E2E_PDS_ADMIN_PASSWORD, localinfra/pds.env has none to read"); return ok( `pds ${nsid}`, await request(`${config.pdsUrl}/xrpc/${nsid}`, { method: "POST", basic: `admin:${config.pdsAdminPassword}`, json }) );};
export const preauthorize = (did: string, requestUri: string) => request(`${config.pdsUrl}/_preauthorize`, { method: "POST", basic: `admin:${config.pdsAdminPassword}`, json: { did, requestUri } });
const slug = (text: string) => text.toLowerCase().replace(/[^a-z0-9]+/g, "-");
// no other test shares an account's stars, tickets or rate limits// membership is a record in the knot's own repo, keyed by the member's didconst KNOT_MEMBER = "org.tangled.knot.member";
export const accounts = { create: async (label: string): Promise<Account> => { const name = `${slug(label)}-${stamp()}`; const handle = handleOf(name); const { code } = await admin("com.atproto.server.createInviteCode", { useCount: 1 }); const made = ok( "pds createAccount", await request(`${config.pdsUrl}/xrpc/com.atproto.server.createAccount`, { method: "POST", json: { email: `${name}@pds.tngl.boltless.dev`, handle, password: config.password, inviteCode: code } }) ); const account = { name, handle, did: made.did as string }; // hydrant only indexes an account once it writes a record in a signal collection try { await pdsCall(account, "com.atproto.repo.putRecord", { collection: "org.tangled.actor.profile", rkey: "self", record: { $type: "org.tangled.actor.profile" } satisfies Profile }); } catch (cause) { // the caller never gets the account back, so nothing else would clean it up await admin("com.atproto.admin.deleteAccount", { did: account.did }).catch(() => {}); throw cause; } return account; }, // the knot is closed to anyone it hasn't been told about makeMember: (who: Account) => serviceCall(seeded.alice, config.knotDid, "com.atproto.repo.createRecord", { repo: config.knotDid, collection: KNOT_MEMBER, rkey: who.did, record: { $type: KNOT_MEMBER, createdAt: new Date().toISOString() } }), removeMember: (who: Account) => serviceCall(seeded.alice, config.knotDid, "com.atproto.repo.deleteRecord", { repo: config.knotDid, collection: KNOT_MEMBER, rkey: who.did }), remove: async (who: Account) => { await admin("com.atproto.admin.deleteAccount", { did: who.did }); sessions.delete(who.did); }};
// the knot answers ok for a repo it doesn't know, so a half-made one is safe to deleteconst deleteKnotRepo = (who: Account, repo: { did: string }) => serviceCall(who, config.knotDid, "org.tangled.repo.deleteRepo", { repo: repo.did });
// the knot refuses to delete a repo while its declaration is still on the owner's pds, so that goes firstconst removeRepo = async (who: Account, repo: { did: string; name: string }) => { await pdsCall(who, "com.atproto.repo.deleteRecord", { collection: "org.tangled.repo.declaration", rkey: repo.name }); await deleteKnotRepo(who, repo);};
export const repos = { create: async (who: Account, repoName: string, options: RepoOptions = {}) => { const createdAt = new Date().toISOString(); const made = await serviceCall(who, config.knotDid, "org.tangled.repo.createRepo", { name: repoName, createdAt, ...(options.spindle ? { spindle: options.spindle } : {}), ...(options.forkOf ? { source: { $type: "org.tangled.repo.defs#sourceTangled", did: options.forkOf } } : {}), ...(options.importFrom ? { source: { $type: "org.tangled.repo.defs#sourceExternal", uri: options.importFrom } } : {}) }); const declaration = await pdsCall(who, "com.atproto.repo.createRecord", { collection: "org.tangled.repo.declaration", rkey: repoName, record: { $type: "org.tangled.repo.declaration", repo: made.did, createdAt } satisfies Declaration }).catch(async (cause) => { // the caller never gets this repo, so nothing else would remove the knot's half await deleteKnotRepo(who, { did: made.did }).catch(() => {}); throw cause; }); return { did: made.did as Did, name: repoName, declaration: { uri: declaration.uri as string, cid: declaration.cid as string } }; }, remove: removeRepo};
const TICKET = "org.tangled.track.ticket";
// the sites worker keeps claims and site configs itself, behind service authconst sitesDo = async ( who: Account, nsid: string, input: { json?: object; params?: Record<string, string> }) => ok(`sites ${nsid}`, await serviceDo(who, config.sitesUrl, config.sitesDid, nsid, input));
export const sites = { claim: (who: Account, domain: string) => sitesDo(who, "org.tangled.temp.site.claimDomain", { json: { domain } }), config: async (who: Account, repoDid: string) => (await sitesDo(who, "org.tangled.temp.repo.getSiteConfig", { params: { repoDid } })).config, history: async (who: Account, repoDid: string) => (await sitesDo(who, "org.tangled.temp.repo.getDeployHistory", { params: { repoDid } })) .deploys as { status: string; error: string }[], // returns the reply instead of throwing on a refusal saveReply: (who: Account, input: object) => serviceDo(who, config.sitesUrl, config.sitesDid, "org.tangled.temp.repo.updateSiteConfig", { json: input })};
export const decodeRefs = (value: { refs: { $bytes: string } }) => Buffer.from(value.refs.$bytes, "base64") .toString() .split("\n") .filter(Boolean) .map((line) => line.split(" ") as [sha: string, ref: string]);
// a missing record or repo is a 404 or a RecordNotFound/RepoNotFound bodyexport const knotRecord = async (uri: string) => { const { status, body } = await get(recordUrl(config.knotUrl, uri)); if (status === 404 || ["RecordNotFound", "RepoNotFound"].includes(body?.error)) return undefined; return ok("knot getRecord", { status, body });};
export const knotRecords = async (repoDid: string, collection: string) => ( await getOk( xrpc(config.knotUrl, "com.atproto.repo.listRecords", { repo: repoDid, collection, limit: "100" }) ) ).records as { uri: string; cid: string; value: any }[];
export const refsOf = async ( uri: string): Promise<{ cid: string; named: ReturnType<typeof decodeRefs> } | undefined> => { const record = await knotRecord(uri); return record && { cid: record.cid, named: decodeRefs(record.value) };};
export const knotRecordFor = async (uri: string, doing: string) => { const found = await knotRecord(uri); if (!found) throw new Error(`the knot has no ${uri} to ${doing}`); return found;};
// the knot only takes an edit that names the version it was read atexport const editCob = async ( who: Account, uri: string, doing: string, change: (stored: Record<string, any>) => Record<string, unknown>) => { const { repo, collection, rkey } = partsOf(uri); const current = await knotRecordFor(uri, doing); // the knot sets x-tngl-editor itself, so it can't be sent back const { "x-tngl-editor": _editor, ...stored } = current.value; return knotReply(who, repo, "com.atproto.repo.putRecord", { collection, rkey, swapRecord: current.cid, record: change(stored) });};
export const ticketRecord = (title: string, more: Partial<Ticket> = {}) => ({ $type: TICKET, title: { $type: "org.tangled.markup.markdown#inline", text: title }, state: "open", createdAt: new Date().toISOString(), ...more }) satisfies Ticket;
export const tickets = { open: async (who: Account, repo: string, title: string): Promise<Written> => { const made = await knotCall(who, repo, "com.atproto.repo.createRecord", { collection: TICKET, record: ticketRecord(title) }); return { uri: made.uri, cid: made.cid }; }, setState: async (who: Account, ticket: Written, state: "open" | "closed"): Promise<Written> => { const made = ok( "knot com.atproto.repo.putRecord", await editCob(who, ticket.uri, "change", (stored) => ({ ...stored, state })) ); return { uri: made.uri, cid: made.cid }; }};
export const pulls = { open: async ( who: Account, repo: Did, title: string, targetBranch: string, range: { base: string; head: string }, sourceBranch: string ): Promise<{ ticket: Written; patch: Written }> => { const ticket = await knotCall(who, repo, "com.atproto.repo.createRecord", { collection: TICKET, record: ticketRecord(title, { targetBranch }) }); // the knot only takes patches through submitPatch, not createRecord const patch = await knotCall(who, repo, "org.tangled.review.submitPatch", { patch: { ticket: partsOf(ticket.uri).rkey, base: { oid: range.base }, head: { oid: range.head }, sourceRepo: repo, // web resubmits from the source branch, a patch without one has no resubmit button sourceBranch } }); return { ticket: { uri: ticket.uri, cid: ticket.cid }, patch: { uri: patch.uri, cid: patch.cid } }; }};
const COMMENT = "org.tangled.feed.comment";
// the subject's cid must be its current version or the knot refuses itexport const comments = { add: (who: Account, repo: string, subject: Written, text: string) => knotReply(who, repo, "com.atproto.repo.createRecord", { collection: COMMENT, record: { $type: COMMENT, subject: { $type: "com.atproto.repo.strongRef", uri: subject.uri as ResourceUri, cid: subject.cid }, body: { $type: "org.tangled.markup.markdown", text }, createdAt: new Date().toISOString() } satisfies Comment })};
const LABEL = "org.tangled.label.definition";
// the knot only takes a client rkey on label definitions, and this one is the nameexport const labels = { define: (who: Account, repo: string, name: string) => knotReply(who, repo, "com.atproto.repo.createRecord", { collection: LABEL, rkey: name, record: { $type: LABEL, name, color: "#a855f7", createdAt: new Date().toISOString() } satisfies LabelDefinition })};
export const erase = async (who: Account, record: Pick<Written, "uri">) => { const { repo, collection, rkey } = partsOf(record.uri); if (repo === who.did) { await pdsCall(who, "com.atproto.repo.deleteRecord", { collection, rkey }); return; } const current = await knotRecordFor(record.uri, "erase"); await knotCall(who, repo, "com.atproto.repo.deleteRecord", { collection, rkey, swapRecord: current.cid });};