feat(integrations): add test-connection endpoints for Nightscout, Polar, Oura

This commit is contained in:
Marc-André Bombeck committed 2026-06-14 21:11:09 +02:00
1 parent 95739369e1
commit e5fad221a0
3 files changed
+253

No files matched your search

+90
View File
@@ -0,0 +1,90 @@
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { apiSuccess, apiError } from "@/lib/api-response";
import { checkRateLimit } from "@/lib/rate-limit";
import { annotate } from "@/lib/logging/context";
import { getUserNightscoutCredentials } from "@/lib/nightscout/credentials";
import { fetchSgvEntries, NightscoutApiError } from "@/lib/nightscout/client";
import { SafeFetchError } from "@/lib/safe-fetch";
export const dynamic = "force-dynamic";
// Cheap live probe — re-validate the stored URL + token by pulling a single
// SGV entry off the user's instance via the same client the sync uses. No new
// credentials path: it reuses `getUserNightscoutCredentials` + `fetchSgvEntries`.
const TIMEOUT_MS = 5_000;
function categoriseStatus(status: number | null): {
code: string;
message: string;
} {
if (status === 401 || status === 403) {
return {
code: "credentials_rejected",
message: "Nightscout rejected the token",
};
}
if (status === 429) {
return {
code: "rate_limited",
message: "Nightscout rate-limited the request",
};
}
if (status !== null && status >= 500) {
return { code: "upstream_error", message: "Nightscout returned an error" };
}
return { code: "connection_failed", message: "Nightscout connection failed" };
}
export const POST = apiHandler(async () => {
const { user } = await requireAuth();
annotate({ action: { name: "nightscout.test" } });
const rl = await checkRateLimit(`nightscout-test:${user.id}`, 5, 60_000);
if (!rl.allowed) {
return apiError("Too many test requests", 429, {
errorCode: "rate_limited_self",
});
}
const creds = await getUserNightscoutCredentials(user.id);
if (!creds) {
return apiError("Nightscout not connected", 422, {
errorCode: "not_configured",
});
}
const start = performance.now();
try {
await fetchSgvEntries({
baseUrl: creds.baseUrl,
token: creds.token,
count: 1,
allowPrivateHost: creds.allowPrivateHost,
timeoutMs: TIMEOUT_MS,
});
const latencyMs = Math.round(performance.now() - start);
return apiSuccess({ ok: true, latencyMs });
} catch (e) {
if (e instanceof NightscoutApiError) {
const cat = categoriseStatus(e.status);
annotate({
meta: { nightscout_test_status: e.status, nightscout_test_code: cat.code },
});
return apiError(cat.message, 502, { errorCode: cat.code });
}
const isTimeout = e instanceof SafeFetchError && e.kind === "timeout";
const isPrivate = e instanceof SafeFetchError && e.kind === "private_host";
const code = isTimeout
? "timeout"
: isPrivate
? "url_not_public"
: "connection_failed";
annotate({ meta: { nightscout_test_code: code } });
return apiError(
isTimeout ? "Nightscout request timed out" : "Nightscout connection failed",
502,
{ errorCode: code },
);
}
});
+82
View File
@@ -0,0 +1,82 @@
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { apiSuccess, apiError } from "@/lib/api-response";
import { checkRateLimit } from "@/lib/rate-limit";
import { annotate } from "@/lib/logging/context";
import { getOuraConnection } from "@/lib/oura/credentials";
import { safeFetch, SafeFetchError } from "@/lib/safe-fetch";
export const dynamic = "force-dynamic";
// Lightweight token probe — fetch the personal-info record, the cheapest
// authenticated Oura call that confirms the stored grant is still valid.
// Reuses `getOuraConnection`; no new credential path.
// Base from `src/lib/oura/client.ts` (`OURA_API_BASE`).
const OURA_PERSONAL_INFO_URL =
"https://api.ouraring.com/v2/usercollection/personal_info";
const TIMEOUT_MS = 5_000;
function categoriseHttpStatus(status: number): { code: string; message: string } {
if (status === 401 || status === 403) {
return { code: "credentials_rejected", message: "Oura rejected the token" };
}
if (status === 429) {
return { code: "rate_limited", message: "Oura rate-limited the request" };
}
if (status >= 500) {
return { code: "upstream_error", message: "Oura returned a server error" };
}
return { code: "connection_failed", message: "Oura connection failed" };
}
export const POST = apiHandler(async () => {
const { user } = await requireAuth();
annotate({ action: { name: "oura.test" } });
const rl = await checkRateLimit(`oura-test:${user.id}`, 5, 60_000);
if (!rl.allowed) {
return apiError("Too many test requests", 429, {
errorCode: "rate_limited_self",
});
}
const connection = await getOuraConnection(user.id);
if (!connection) {
return apiError("Oura not connected", 422, { errorCode: "not_configured" });
}
const start = performance.now();
try {
const res = await safeFetch(
OURA_PERSONAL_INFO_URL,
{
method: "GET",
headers: {
Authorization: `Bearer ${connection.accessToken}`,
Accept: "application/json",
},
},
{ timeoutMs: TIMEOUT_MS },
);
const latencyMs = Math.round(performance.now() - start);
if (!res.ok) {
const cat = categoriseHttpStatus(res.status);
annotate({
meta: { oura_test_status: res.status, oura_test_code: cat.code },
});
return apiError(cat.message, 502, { errorCode: cat.code });
}
return apiSuccess({ ok: true, latencyMs });
} catch (e) {
const isTimeout = e instanceof SafeFetchError && e.kind === "timeout";
const code = isTimeout ? "timeout" : "connection_failed";
annotate({ meta: { oura_test_code: code } });
return apiError(
isTimeout ? "Oura request timed out" : "Oura connection failed",
502,
{ errorCode: code },
);
}
});
+81
View File
@@ -0,0 +1,81 @@
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { apiSuccess, apiError } from "@/lib/api-response";
import { checkRateLimit } from "@/lib/rate-limit";
import { annotate } from "@/lib/logging/context";
import { getPolarConnection } from "@/lib/polar/credentials";
import { safeFetch, SafeFetchError } from "@/lib/safe-fetch";
export const dynamic = "force-dynamic";
// Lightweight token probe — fetch the registered AccessLink user record, the
// cheapest authenticated Polar call that confirms the stored grant is still
// valid. Reuses `getPolarConnection`; no new credential path.
// Base from `src/lib/polar/client.ts` (`POLAR_API_BASE`).
const POLAR_USERS_BASE = "https://www.polaraccesslink.com/v3/users";
const TIMEOUT_MS = 5_000;
function categoriseHttpStatus(status: number): { code: string; message: string } {
if (status === 401 || status === 403) {
return { code: "credentials_rejected", message: "Polar rejected the token" };
}
if (status === 429) {
return { code: "rate_limited", message: "Polar rate-limited the request" };
}
if (status >= 500) {
return { code: "upstream_error", message: "Polar returned a server error" };
}
return { code: "connection_failed", message: "Polar connection failed" };
}
export const POST = apiHandler(async () => {
const { user } = await requireAuth();
annotate({ action: { name: "polar.test" } });
const rl = await checkRateLimit(`polar-test:${user.id}`, 5, 60_000);
if (!rl.allowed) {
return apiError("Too many test requests", 429, {
errorCode: "rate_limited_self",
});
}
const connection = await getPolarConnection(user.id);
if (!connection) {
return apiError("Polar not connected", 422, { errorCode: "not_configured" });
}
const start = performance.now();
try {
const res = await safeFetch(
`${POLAR_USERS_BASE}/${encodeURIComponent(connection.polarUserId)}`,
{
method: "GET",
headers: {
Authorization: `Bearer ${connection.accessToken}`,
Accept: "application/json",
},
},
{ timeoutMs: TIMEOUT_MS },
);
const latencyMs = Math.round(performance.now() - start);
if (!res.ok) {
const cat = categoriseHttpStatus(res.status);
annotate({
meta: { polar_test_status: res.status, polar_test_code: cat.code },
});
return apiError(cat.message, 502, { errorCode: cat.code });
}
return apiSuccess({ ok: true, latencyMs });
} catch (e) {
const isTimeout = e instanceof SafeFetchError && e.kind === "timeout";
const code = isTimeout ? "timeout" : "connection_failed";
annotate({ meta: { polar_test_code: code } });
return apiError(
isTimeout ? "Polar request timed out" : "Polar connection failed",
502,
{ errorCode: code },
);
}
});