feat(links): Kutt API client + release version-compare

This commit is contained in:
root
2026-06-08 23:27:21 +10:00
parent 8f7331129f
commit c8b9dddd61
2 changed files with 54 additions and 0 deletions

31
lib/links/kutt.js Normal file
View File

@@ -0,0 +1,31 @@
// Thin client for stock Kutt's REST API + release-version compare. fetch injected
// for tests; defaults to global fetch (Node 22). No Kutt source coupling.
const norm = v => String(v || '').replace(/^v/, '');
export function compareVersions(running, latest) {
return { running, latest, updateAvailable: norm(running) !== '' && norm(latest) !== '' && norm(running) !== norm(latest) };
}
export async function fetchLatestKuttRelease({ fetch = globalThis.fetch } = {}) {
const res = await fetch('https://api.github.com/repos/thedevs-network/kutt/releases/latest',
{ headers: { 'Accept': 'application/vnd.github+json', 'User-Agent': 'void' } });
if (!res.ok) throw new Error(`github ${res.status}`);
const j = await res.json();
return { latest: j.tag_name, url: j.html_url };
}
export async function createLink(body, { base, key, fetch = globalThis.fetch }) {
const res = await fetch(`${base}/api/v2/links`, {
method: 'POST',
headers: { 'X-API-KEY': key, 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!res.ok) throw new Error(`kutt ${res.status}`);
return res.json();
}
export async function recentLinks({ base, key, fetch = globalThis.fetch, limit = 5 }) {
const res = await fetch(`${base}/api/v2/links?limit=${limit}`, { headers: { 'X-API-KEY': key } });
if (!res.ok) throw new Error(`kutt ${res.status}`);
return res.json();
}

23
tests/links/kutt.test.js Normal file
View File

@@ -0,0 +1,23 @@
import { describe, it, expect } from 'vitest';
import { compareVersions, fetchLatestKuttRelease, createLink } from '../../lib/links/kutt.js';
describe('kutt helpers', () => {
it('compareVersions flags an available update (tolerates v-prefix)', () => {
expect(compareVersions('v3.2.5', 'v3.2.6')).toEqual({ running: 'v3.2.5', latest: 'v3.2.6', updateAvailable: true });
expect(compareVersions('3.2.6', 'v3.2.6')).toMatchObject({ updateAvailable: false });
});
it('fetchLatestKuttRelease returns tag + url from the GitHub API (injected fetch)', async () => {
const fakeFetch = async () => ({ ok: true, json: async () => ({ tag_name: 'v3.2.6', html_url: 'https://x/releases/v3.2.6' }) });
expect(await fetchLatestKuttRelease({ fetch: fakeFetch })).toEqual({ latest: 'v3.2.6', url: 'https://x/releases/v3.2.6' });
});
it('createLink POSTs to the Kutt API with the key and returns the short link', async () => {
let seen;
const fakeFetch = async (url, opts) => { seen = { url, opts }; return { ok: true, json: async () => ({ link: 'https://link.hynesy.com/abc', address: 'abc' }) }; };
const r = await createLink({ target: 'https://example.com' }, { base: 'http://10.0.0.1:3000', key: 'K', fetch: fakeFetch });
expect(seen.url).toBe('http://10.0.0.1:3000/api/v2/links');
expect(seen.opts.headers['X-API-KEY']).toBe('K');
expect(r.link).toBe('https://link.hynesy.com/abc');
});
});