PATCH { status: "on" } no longer works in current UpSnap version.
Switch to GET /api/upsnap/wake/{id} with 10s timeout (WoL packet
is sent immediately, endpoint blocks until device responds).
Co-Authored-By: Claude <noreply@anthropic.com>
123 lines
3.6 KiB
TypeScript
123 lines
3.6 KiB
TypeScript
import type { UpSnapAuthResponse } from '@sleepguard/shared';
|
|
import { config } from '../config.js';
|
|
|
|
let token: string | null = null;
|
|
|
|
async function authenticate(): Promise<string> {
|
|
// 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<string> {
|
|
if (!token) {
|
|
return authenticate();
|
|
}
|
|
return token;
|
|
}
|
|
|
|
async function upSnapFetch(path: string, options: RequestInit = {}): Promise<Response> {
|
|
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<void> {
|
|
// 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<void> {
|
|
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}`);
|
|
}
|
|
}
|