// Client for the belt's HTTP API. export class ApiError extends Error { constructor(status, message) { super(message); this.status = status; } } const KEY = 'wh.key'; export const keyStore = { get() { try { return localStorage.getItem(KEY); } catch { return null; } }, set(k) { try { localStorage.setItem(KEY, k); } catch { /* private mode: session only */ } memoryKey = k; }, clear() { try { localStorage.removeItem(KEY); } catch { /* ignore */ } memoryKey = null; }, }; let memoryKey = null; const currentKey = () => memoryKey ?? keyStore.get(); let unauthorized = () => {}; export function onUnauthorized(cb) { unauthorized = cb; } async function call(method, path, { json, body, type, auth = true, raw = false, key } = {}) { const headers = {}; // An explicit key (used by login) is tried as given and never triggers the // global "session rejected" handler. const k = key ?? (auth ? currentKey() : null); if (k) headers.Authorization = `Bearer ${k}`; let payload = body; if (json !== undefined) { payload = JSON.stringify(json); headers['Content-Type'] = 'application/json'; } else if (type) { headers['Content-Type'] = type; } let res; try { res = await fetch(path, { method, headers, body: payload }); } catch { throw new ApiError(0, 'LINK DOWN: cannot reach the server'); } if (!res.ok) { let msg = `${res.status} ${res.statusText}`; try { msg = (await res.json()).error ?? msg; } catch { /* not JSON */ } if (res.status === 401 && auth && key === undefined) unauthorized(); throw new ApiError(res.status, msg); } if (raw) return res; if ((res.headers.get('Content-Type') ?? '').includes('json')) return res.json(); return null; } export const api = { info: () => call('GET', '/info', { auth: false }), market: () => call('GET', '/market', { auth: false }), register: (name) => call('POST', '/register', { json: { name }, auth: false }), me: (key) => call('GET', '/me', { key }), ships: () => call('GET', '/ships'), assemble: (source) => call('POST', '/assemble', { body: source, type: 'text/plain', auth: false }), setProgram: (id, bytes) => call('PUT', `/ships/${id}/program`, { body: bytes, type: 'application/octet-stream' }), launch: (id) => call('POST', `/ships/${id}/launch`), setUplink: (id, bytes) => call('PUT', `/ships/${id}/uplink`, { body: bytes, type: 'application/octet-stream' }), async downlink(id) { const res = await call('GET', `/ships/${id}/downlink`, { raw: true }); return { bytes: new Uint8Array(await res.arrayBuffer()), day: Number(res.headers.get('X-Downlink-Day')), }; }, async example(name) { const res = await fetch(`/examples/${name}.s`); if (!res.ok) throw new ApiError(res.status, `example ${name} not found`); return res.text(); }, async examples() { const res = await fetch('/examples/index.json'); return res.ok ? res.json() : []; }, }; export function b64ToBytes(b64) { const bin = atob(b64); const out = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); return out; }