diff --git a/lib/links/kutt.js b/lib/links/kutt.js new file mode 100644 index 0000000..b079437 --- /dev/null +++ b/lib/links/kutt.js @@ -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(); +} diff --git a/tests/links/kutt.test.js b/tests/links/kutt.test.js new file mode 100644 index 0000000..1d96839 --- /dev/null +++ b/tests/links/kutt.test.js @@ -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'); + }); +});