init
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user