stoke/src/api.js

149 lines
3.9 KiB
JavaScript
Raw Normal View History

/**
* Forgejo API client.
*
* Endpoints used:
* GET /api/v1/user -> verify credentials / obtain login name
* POST /api/v1/users/{login}/tokens -> create a personal access token
* DELETE /api/v1/users/{login}/tokens/{id} -> revoke a personal access token
*/
class ForgejoClient {
constructor(baseUrl, token = null) {
this.baseUrl = baseUrl.replace(/\/$/, '');
this.token = token;
}
static fromConfig(config) {
if (!config || !config.url || !config.token) {
throw new Error('Not authenticated. Run: forgejo auth login');
}
return new ForgejoClient(config.url, config.token);
}
withBasicAuth(username, password) {
const clone = new ForgejoClient(this.baseUrl, this.token);
clone.basicAuth = Buffer.from(`${username}:${password}`).toString('base64');
return clone;
}
headers(extra = {}) {
const h = {
Accept: 'application/json',
'Content-Type': 'application/json',
'User-Agent': 'forgejo-cli/1.0.0',
...extra,
};
if (this.token) {
h.Authorization = `token ${this.token}`;
} else if (this.basicAuth) {
h.Authorization = `Basic ${this.basicAuth}`;
}
return h;
}
async request(method, endpoint, body = null) {
const url = `${this.baseUrl}/api/v1${endpoint}`;
const opts = {
method,
headers: this.headers(),
};
if (body !== null) {
opts.body = JSON.stringify(body);
}
let res;
try {
res = await fetch(url, opts);
} catch (err) {
throw new Error(`Network error reaching ${this.baseUrl}: ${err.message}`);
}
const text = await res.text();
let data = null;
if (text) {
try {
data = JSON.parse(text);
} catch {
data = { raw: text };
}
}
if (!res.ok) {
const msg = data?.message || data?.raw || `HTTP ${res.status}`;
const err = new Error(msg);
err.status = res.status;
err.body = data;
throw err;
}
return data;
}
get(endpoint) {
return this.request('GET', endpoint);
}
post(endpoint, body) {
return this.request('POST', endpoint, body);
}
del(endpoint) {
return this.request('DELETE', endpoint);
}
async verifyBasicAuth(username, password) {
const client = this.withBasicAuth(username, password);
return client.get('/user');
}
async createToken(username, password, name, scopes) {
const client = this.withBasicAuth(username, password);
return client.post(`/users/${encodeURIComponent(username)}/tokens`, { name, scopes });
}
async deleteToken(login, id) {
return this.del(`/users/${encodeURIComponent(login)}/tokens/${id}`);
}
async createRepo(payload) {
return this.post('/user/repos', payload);
}
async migrateRepo(payload) {
return this.post('/repos/migrate', payload);
}
async getAll(endpoint, params = {}) {
const pageSize = 50;
const all = [];
for (let page = 1; page <= 1000; page += 1) {
const query = new URLSearchParams({ ...params, limit: String(pageSize), page: String(page) }).toString();
const items = await this.get(`${endpoint}?${query}`);
if (!Array.isArray(items) || items.length === 0) break;
all.push(...items);
if (items.length < pageSize) break;
}
return all;
}
async listRepos(opts = {}) {
return this.getAll('/user/repos', opts);
}
async listIssues(owner, repo, opts = {}) {
const params = {};
if (opts.state) params.state = opts.state;
if (opts.type) params.type = opts.type;
return this.getAll(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues`, params);
}
async listPullRequests(owner, repo, opts = {}) {
return this.getAll(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls`, opts);
}
async listBranches(owner, repo, opts = {}) {
return this.getAll(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/branches`, opts);
}
}
module.exports = { ForgejoClient };