83 lines
4.0 KiB
JavaScript
83 lines
4.0 KiB
JavaScript
import { execFileSync } from 'node:child_process';
|
|
|
|
const origin = new URL(process.env.GITEA_SERVER_URL || 'https://git.render.ink');
|
|
if (origin.protocol !== 'https:' || origin.username || origin.password) throw new Error('Gitea requires an HTTPS origin without credentials');
|
|
export const server = origin.origin;
|
|
export const catalogRepository = process.env.CATALOG_REPOSITORY || 'trove/curios';
|
|
|
|
export function repositoryPath(repository) {
|
|
if (!/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(repository)) throw new Error('Invalid repository name');
|
|
return repository.split('/').map(encodeURIComponent).join('/');
|
|
}
|
|
|
|
export async function api(route, { token = process.env.GITEA_TOKEN, method = 'GET', body, missing = false } = {}) {
|
|
const auth = authorization(token);
|
|
const response = await fetch(`${server}/api/v1${route}`, {
|
|
method, redirect: 'error', signal: AbortSignal.timeout(60_000),
|
|
headers: { Authorization: auth, ...(body && !(body instanceof FormData) ? { 'Content-Type': 'application/json' } : {}) },
|
|
body: body instanceof FormData ? body : body === undefined ? undefined : JSON.stringify(body),
|
|
});
|
|
if (missing && response.status === 404) return null;
|
|
if (!response.ok) {
|
|
const error = new Error(`Gitea ${method} ${route.split('?')[0]} returned HTTP ${response.status}`);
|
|
error.status = response.status;
|
|
throw error;
|
|
}
|
|
if (response.status === 204) return null;
|
|
return response.json();
|
|
}
|
|
|
|
// Keep credentials on this Gitea origin; release storage may redirect to a CDN.
|
|
export async function download(url, token = process.env.GITEA_TOKEN, limit = 512 * 1024 * 1024) {
|
|
let current = new URL(url);
|
|
for (let redirects = 0; redirects <= 5; redirects++) {
|
|
if (current.protocol !== 'https:' || current.username || current.password) throw new Error('Invalid release download URL');
|
|
const response = await fetch(current, {
|
|
redirect: 'manual', signal: AbortSignal.timeout(120_000),
|
|
headers: current.origin === server ? { Authorization: authorization(token) } : {},
|
|
});
|
|
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
const location = response.headers.get('location');
|
|
await response.body?.cancel();
|
|
if (!location) throw new Error('Release redirect has no location');
|
|
current = new URL(location, current);
|
|
continue;
|
|
}
|
|
if (!response.ok) throw new Error(`Release download returned HTTP ${response.status}`);
|
|
const chunks = []; let size = 0;
|
|
for await (const chunk of response.body) {
|
|
size += chunk.length;
|
|
if (size > limit) throw new Error('Release download exceeds declared size limit');
|
|
chunks.push(chunk);
|
|
}
|
|
return Buffer.concat(chunks);
|
|
}
|
|
throw new Error('Too many release download redirects');
|
|
}
|
|
|
|
let localAuthorization;
|
|
function authorization(token) {
|
|
if (token) return `token ${token}`;
|
|
if (process.env.GITEA_ACTIONS === 'true') throw new Error('Workflow token is missing');
|
|
if (!localAuthorization) {
|
|
const output = execFileSync('git', ['-c', 'credential.interactive=false', 'credential', 'fill'], {
|
|
input: `url=${server}\n\n`, encoding: 'utf8', timeout: 10_000,
|
|
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, stdio: ['pipe', 'pipe', 'ignore'],
|
|
});
|
|
const fields = Object.fromEntries(output.trim().split('\n').map(line => {
|
|
const separator = line.indexOf('='); return [line.slice(0, separator), line.slice(separator + 1)];
|
|
}));
|
|
if (!fields.username || !fields.password) throw new Error('Set GITEA_TOKEN or configure Git HTTPS credentials');
|
|
localAuthorization = `Basic ${Buffer.from(`${fields.username}:${fields.password}`).toString('base64')}`;
|
|
}
|
|
return localAuthorization;
|
|
}
|
|
|
|
export async function notifyCatalog(repository, tag) {
|
|
await api(`/repos/${repositoryPath(catalogRepository)}/actions/workflows/sync-release.yml/dispatches`, {
|
|
token: process.env.CATALOG_TOKEN,
|
|
method: 'POST', body: { ref: 'main', inputs: { repository, tag } },
|
|
});
|
|
console.log(`Requested catalog registration: ${repository}@${tag}`);
|
|
}
|