import type { UpSnapAuthResponse } from '@sleepguard/shared'; import { config } from '../config.js'; let token: string | null = null; async function authenticate(): Promise { // PocketBase v0.23+ moved admins to _superusers collection const collections = ['_superusers', 'users']; for (const collection of collections) { const url = `${config.upsnap.url}/api/collections/${collection}/auth-with-password`; const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ identity: config.upsnap.username, password: config.upsnap.password, }), }); if (res.ok) { const data = (await res.json()) as UpSnapAuthResponse; token = data.token; return token; } const errBody = await res.text().catch(() => ''); console.error(`[UpSnap] Auth attempt ${collection} failed (${res.status}): ${errBody}`); } throw new Error('UpSnap auth failed: could not authenticate as superuser or user'); } async function getToken(): Promise { if (!token) { return authenticate(); } return token; } async function upSnapFetch(path: string, options: RequestInit = {}): Promise { let authToken = await getToken(); const doFetch = (t: string) => fetch(`${config.upsnap.url}${path}`, { ...options, headers: { 'Content-Type': 'application/json', Authorization: t, ...options.headers, }, }); let res = await doFetch(authToken); // Token expired — re-auth once if (res.status === 401 || res.status === 403) { authToken = await authenticate(); res = await doFetch(authToken); } return res; } export async function wakeDevice(): Promise { // Use the dedicated wake endpoint — it sends a WoL packet. // This endpoint may block until the device responds or its internal timeout expires, // so we abort after 10s — the WoL packet is already sent by then. const authToken = await getToken(); try { const res = await fetch( `${config.upsnap.url}/api/upsnap/wake/${config.upsnap.deviceId}`, { method: 'GET', headers: { Authorization: authToken }, signal: AbortSignal.timeout(10_000), }, ); if (res.status === 401 || res.status === 403) { const newToken = await authenticate(); await fetch( `${config.upsnap.url}/api/upsnap/wake/${config.upsnap.deviceId}`, { method: 'GET', headers: { Authorization: newToken }, signal: AbortSignal.timeout(10_000), }, ).catch(() => {}); // WoL packet sent regardless of response return; } if (!res.ok) { const body = await res.text().catch(() => ''); throw new Error(`UpSnap wake failed: ${res.status} — ${body}`); } } catch (err) { // AbortError / TimeoutError is expected — the WoL packet was already sent if (err instanceof DOMException && err.name === 'TimeoutError') { console.log('[UpSnap] Wake request timed out (expected — WoL packet was sent)'); return; } if (err instanceof DOMException && err.name === 'AbortError') { console.log('[UpSnap] Wake request aborted (expected — WoL packet was sent)'); return; } throw err; } } export async function shutdownDevice(): Promise { const res = await upSnapFetch(`/api/upsnap/shutdown/${config.upsnap.deviceId}`, { method: 'GET', signal: AbortSignal.timeout(10_000), }); if (!res.ok) { const body = await res.text().catch(() => ''); throw new Error(`UpSnap shutdown failed: ${res.status} — ${body}`); } }