Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213import { ClientResponseError } from "@atcute/client";import { isDid } from "@atcute/lexicons/syntax";import type { OAuthUserAgent } from "@atcute/oauth-browser-client";import { mintServiceAuth, serviceDidForHost } from "$lib/auth/agent";import { missingPermissions, type Permission } from "$lib/auth/scopes";import { jsonGet, jsonPost, serviceClient, serviceUrlFor, toResponseError} from "$lib/api/_request";import { mainSchema as createTaskSchema } from "$lib/api/lexicons/types/org/tangled/temp/migrator/createTask";import type * as Migrator from "$lib/api/lexicons/types/org/tangled/temp/migrator/defs";import { mainSchema as describeSourceSchema } from "$lib/api/lexicons/types/org/tangled/temp/migrator/describeSource";import type { $output as DescribeSourceOutput } from "$lib/api/lexicons/types/org/tangled/temp/migrator/describeSource";import { mainSchema as getTaskSchema } from "$lib/api/lexicons/types/org/tangled/temp/migrator/getTask";import { mainSchema as listTasksSchema } from "$lib/api/lexicons/types/org/tangled/temp/migrator/listTasks";import type { $output as ListTasksOutput } from "$lib/api/lexicons/types/org/tangled/temp/migrator/listTasks";import { mainSchema as retryJobSchema } from "$lib/api/lexicons/types/org/tangled/temp/migrator/retryJob";
const CREATE_TASK = createTaskSchema.nsid;const GET_TASK = getTaskSchema.nsid;const LIST_TASKS = listTasksSchema.nsid;const RETRY_JOB = retryJobSchema.nsid;const DESCRIBE_SOURCE = describeSourceSchema.nsid;
export const describeSourcePermission = (host: string): Permission | null => { const aud = serviceDidForHost(host); return isDid(aud) ? { resource: "rpc", lxm: DESCRIBE_SOURCE, aud } : null;};
export const missingDescribeSourceScope = ( agent: OAuthUserAgent, host: string): readonly Permission[] => { const wanted = describeSourcePermission(host); return wanted === null ? [] : missingPermissions(agent.session.token.scope, [wanted]);};
export type MigrationTask = Migrator.Task;export type NewMigrationJob = Migrator.NewJob;export type MigrationJobStatus = "authorization_required" | "cloning" | "completed" | "failed" | "importing" | "queued";
export class MigrationGrantRequired extends Error { readonly startUrl: string | null; constructor(startUrl: string | null) { super("The migrator needs your authorization before it can migrate anything."); this.name = "MigrationGrantRequired"; this.startUrl = startUrl; }}
// migrator echoes return_to and appends oauth_error; round trips must not nest themconst transportParams = ["return_to", "oauth_error", "migrator"];
const withQuery = (pathname: string, params: URLSearchParams): string => { const query = params.toString(); return query === "" ? pathname : `${pathname}?${query}`;};
export const returnPathFrom = (location: { pathname: string; search: string }): string => { const params = new URLSearchParams(location.search); for (const key of transportParams) params.delete(key); return withQuery(location.pathname, params);};
export const flowReturnPath = (location: { pathname: string; search: string }): string => { const params = new URLSearchParams(location.search); params.delete("github_error"); return withQuery(location.pathname, params);};
const currentReturnPath = (): string => typeof window === "undefined" ? "" : returnPathFrom({ pathname: window.location.pathname, search: window.location.search });
export const startUrlFrom = ( host: string, did: string | undefined, returnTo = currentReturnPath()): string | null => did === undefined ? null : `${serviceUrlFor(host)}/oauth/start?did=${encodeURIComponent(did)}` + (returnTo === "" ? "" : `&return_to=${encodeURIComponent(returnTo)}`);
const extractGrantError = ( cause: unknown, host: string, did: string | undefined): MigrationGrantRequired | null => cause instanceof ClientResponseError && (cause.status === 428 || cause.error === "GrantRequired") ? new MigrationGrantRequired(startUrlFrom(host, did)) : null;
interface CreateMigrationTaskOptions { signal?: AbortSignal; fetch?: typeof globalThis.fetch;}
export const createMigrationTask = async ( agent: OAuthUserAgent, host: string, requestId: string, jobs: NewMigrationJob[], options?: CreateMigrationTaskOptions | AbortSignal): Promise<MigrationTask> => { if (jobs.length === 0) throw new Error("a migration needs at least one repository"); const signal = options instanceof AbortSignal ? options : options?.signal; const customFetch = (options && !(options instanceof AbortSignal) ? options.fetch : undefined) ?? globalThis.fetch;
const token = await mintServiceAuth(agent, { aud: serviceDidForHost(host), lxm: CREATE_TASK, signal });
const response = await customFetch("/_internal/github/migrator/createTask", { method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json", accept: "application/json" }, body: JSON.stringify({ requestId, jobs }), signal });
if (!response.ok) { const err = await toResponseError(response); throw extractGrantError(err, host, agent.sub) ?? err; }
const text = await response.text(); return (text.length > 0 ? JSON.parse(text) : undefined) as MigrationTask;};
export const getMigrationTask = async ( agent: OAuthUserAgent, host: string, taskId: string, signal?: AbortSignal): Promise<MigrationTask> => { const ctx = serviceClient(agent, host); try { return await jsonGet<MigrationTask>(ctx, GET_TASK, { taskId }, { signal }); } catch (cause) { throw extractGrantError(cause, host, agent.sub) ?? cause; }};
export const listMigrationTasks = async ( agent: OAuthUserAgent, host: string, params?: { limit?: number }, signal?: AbortSignal): Promise<MigrationTask[]> => { const ctx = serviceClient(agent, host); try { const out = await jsonGet<ListTasksOutput>(ctx, LIST_TASKS, params, { signal }); return out.tasks; } catch (cause) { throw extractGrantError(cause, host, agent.sub) ?? cause; }};
export const describeSource = async ( agent: OAuthUserAgent, host: string, sourceUrl: string, signal?: AbortSignal): Promise<DescribeSourceOutput> => { const ctx = serviceClient(agent, host); try { return await jsonGet<DescribeSourceOutput>(ctx, DESCRIBE_SOURCE, { sourceUrl }, { signal }); } catch (cause) { throw extractGrantError(cause, host, agent.sub) ?? cause; }};
export const retryMigrationJob = async ( agent: OAuthUserAgent, host: string, input: { taskId: string; jobId: string; githubToken?: string }, signal?: AbortSignal): Promise<MigrationTask> => { const ctx = serviceClient(agent, host); try { return await jsonPost<MigrationTask>(ctx, RETRY_JOB, input, { signal }); } catch (cause) { throw extractGrantError(cause, host, agent.sub) ?? cause; }};
// mirrors migrator isterminal; unrecognized status keeps poll aliveexport const migrationRunning = (status: string): boolean => !["completed", "failed", "authorization_required"].includes(status);
export const migrationsInFlight = (tasks: MigrationTask[]): boolean => tasks.some((task) => task.jobs.some((job) => migrationRunning(job.status)));
// only newest batch can wait on a grant; older batches are historyexport const migrationsUnfinished = (tasks: MigrationTask[]): boolean => migrationsInFlight(tasks) || (tasks[0]?.jobs.some((job) => job.status === "authorization_required") ?? false);