mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
Merge branch 'v1422/fixes' into release/v1.42.2
This commit is contained in:
commit
3c4e053dcd
83 files changed
+1808
-258
No files matched your search
@@ -73,11 +73,13 @@ interface ProviderHealthSummary {
|
||||
const CENTRAL_TYPES = [...OPERATOR_HELD_PROVIDER_TYPES];
|
||||
|
||||
export const GET = apiHandler(async () => {
|
||||
await requireAdmin();
|
||||
const { user } = await requireAdmin();
|
||||
annotate({ action: { name: "admin.provider-health.get" } });
|
||||
|
||||
// The UTC day the ledger buckets by — the same key every reservation writes.
|
||||
const dateKey = buildDateKey();
|
||||
// The ledger buckets each account by its own local day; the operator's
|
||||
// "today" is the day in the operator's own zone, which on a single-zone
|
||||
// instance is everyone's.
|
||||
const dateKey = await buildDateKey(user.id);
|
||||
|
||||
const groups = await prisma.providerHealth.groupBy({
|
||||
// `lastStatus` joins the grouping key rather than an aggregate: the
|
||||
|
||||
@@ -145,7 +145,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
// `admin-key` is the operator's own credential and draws the operator
|
||||
// ceiling; an override names the caller's own key, and every other resolved
|
||||
// type is the user's own egress, so those draw the user-plan ceiling.
|
||||
const dateKey = buildDateKey();
|
||||
const dateKey = await buildDateKey(user.id);
|
||||
const reservation = await reserveBudget(
|
||||
user.id,
|
||||
AI_TEST_MAX_TOKENS,
|
||||
|
||||
@@ -183,7 +183,7 @@ async function reserveFor(
|
||||
providerType: ProviderChainType,
|
||||
tokens: number,
|
||||
): Promise<AiRunBudget | null> {
|
||||
const dateKey = buildDateKey();
|
||||
const dateKey = await buildDateKey(userId);
|
||||
const reservation = await reserveBudget(
|
||||
userId,
|
||||
tokens,
|
||||
|
||||
@@ -200,7 +200,7 @@ async function handleVisionIndex(
|
||||
const rl = await checkDocumentAiRateLimit(userId);
|
||||
if (!rl.allowed) return documentAiRateLimited(rl);
|
||||
|
||||
const dateKey = buildDateKey();
|
||||
const dateKey = await buildDateKey(userId);
|
||||
const reservation = await reserveBudget(
|
||||
userId,
|
||||
DOCUMENT_INDEX_RESERVE_TOKENS,
|
||||
|
||||
@@ -240,6 +240,10 @@ describe("serving-posture header matrix (Class A inline vs Class B attachment)",
|
||||
["image/heic", "photo.heic"],
|
||||
["application/xml", "export.xml"],
|
||||
["application/json", "export.json"],
|
||||
// Upload refuses markup outright; a row that somehow carries one must
|
||||
// still never render in-origin, where its script would run.
|
||||
["image/svg+xml", "drawing.svg"],
|
||||
["text/html", "page.html"],
|
||||
])(
|
||||
"Class B %s → attachment + octet-stream, never inline",
|
||||
async (mime, name) => {
|
||||
|
||||
@@ -122,7 +122,7 @@ async function reserveFor(
|
||||
userId: string,
|
||||
providerType: ProviderChainType,
|
||||
): Promise<AiRunBudget | null> {
|
||||
const dateKey = buildDateKey();
|
||||
const dateKey = await buildDateKey(userId);
|
||||
const reservation = await reserveBudget(
|
||||
userId,
|
||||
AI_BUDGETS.documentAssist.maxTokens,
|
||||
|
||||
@@ -216,7 +216,7 @@ async function reserveFor(
|
||||
ask: SummaryRequest,
|
||||
providerType: ProviderChainType,
|
||||
): Promise<AiRunBudget | null> {
|
||||
const dateKey = buildDateKey();
|
||||
const dateKey = await buildDateKey(ask.userId);
|
||||
const reservation = await reserveBudget(
|
||||
ask.userId,
|
||||
budgetFor(ask.mode),
|
||||
|
||||
@@ -157,7 +157,7 @@ async function handleTextExtract(
|
||||
// v1.21.0 (F1) — the operator-cost cap applies only when the picked provider
|
||||
// egresses on the operator's own key; a BYOK / Codex / local pick runs on the
|
||||
// user's own plan and gets the generous user-plan ceiling.
|
||||
const dateKey = buildDateKey();
|
||||
const dateKey = await buildDateKey(userId);
|
||||
const reservation = await reserveBudget(
|
||||
userId,
|
||||
OCR_RESERVE_TOKENS.text,
|
||||
@@ -218,7 +218,7 @@ async function handleVisionExtract(
|
||||
// v1.21.0 (F1) — the operator-cost cap applies only when the picked vision
|
||||
// provider egresses on the operator's own key; a BYOK / Codex pick runs on
|
||||
// the user's own plan and gets the generous user-plan ceiling.
|
||||
const dateKey = buildDateKey();
|
||||
const dateKey = await buildDateKey(userId);
|
||||
const reservation = await reserveBudget(
|
||||
userId,
|
||||
OCR_RESERVE_TOKENS.vision,
|
||||
|
||||
@@ -378,7 +378,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
AND "type" = 'BLOOD_PRESSURE_SYS'::"measurement_type"
|
||||
AND "deleted_at" IS NULL
|
||||
AND "value" >= $3
|
||||
AND "measured_at" >= date_trunc('day', $2::timestamptz)
|
||||
AND "measured_at" >= date_trunc('day', $2::timestamptz AT TIME ZONE 'UTC')
|
||||
ORDER BY date_trunc('day', "measured_at"), (${rank}), "source"::text
|
||||
) c
|
||||
ON c.d = date_trunc('day', m."measured_at")
|
||||
|
||||
@@ -225,7 +225,10 @@ async function handleExtract(request: NextRequest): Promise<Response> {
|
||||
chain.map((entry) => entry.providerType),
|
||||
);
|
||||
|
||||
const today = todayOverride ?? buildDateKey();
|
||||
// The account's own today: "tomorrow" in a description means the
|
||||
// person's tomorrow, and the same key is the budget day below.
|
||||
const accountToday = await buildDateKey(userId);
|
||||
const today = todayOverride ?? accountToday;
|
||||
const { systemPrompt, userPrompt } = buildMedicationExtractionPrompt({
|
||||
text,
|
||||
today,
|
||||
@@ -239,7 +242,7 @@ async function handleExtract(request: NextRequest): Promise<Response> {
|
||||
// paying for their own key by the operator's ceiling. `resolveDailyCap`
|
||||
// charges the operator ceiling only when the chain's primary is the
|
||||
// operator's own credential; every other chain gets the user-plan ceiling.
|
||||
const dateKey = buildDateKey();
|
||||
const dateKey = accountToday;
|
||||
const estimatedTokens =
|
||||
MAX_OUTPUT_TOKENS +
|
||||
Math.ceil((systemPrompt.length + userPrompt.length) / 4);
|
||||
|
||||
@@ -41,7 +41,12 @@ const {
|
||||
|
||||
vi.mock("@/lib/jobs/boss-instance", () => ({ getGlobalBoss: bossMock }));
|
||||
|
||||
vi.mock("@/lib/oura/client", () => ({ exchangeCode: exchangeMock }));
|
||||
vi.mock("@/lib/oura/client", async (importOriginal) => ({
|
||||
isAcceptedOuraIssuer: (
|
||||
await importOriginal<typeof import("@/lib/oura/client")>()
|
||||
).isAcceptedOuraIssuer,
|
||||
exchangeCode: exchangeMock,
|
||||
}));
|
||||
vi.mock("@/lib/oura/credentials", () => ({
|
||||
getOuraClientCredentials: getCredsMock,
|
||||
}));
|
||||
@@ -65,10 +70,12 @@ function makeReq(opts: {
|
||||
code?: string;
|
||||
state?: string;
|
||||
cookie?: string;
|
||||
iss?: string;
|
||||
}): NextRequest {
|
||||
const params = new URLSearchParams();
|
||||
if (opts.code) params.set("code", opts.code);
|
||||
if (opts.state) params.set("state", opts.state);
|
||||
if (opts.iss) params.set("iss", opts.iss);
|
||||
return {
|
||||
url: `https://app.example/api/oura/callback?${params}`,
|
||||
cookies: {
|
||||
@@ -127,10 +134,11 @@ describe("GET /api/oura/callback", () => {
|
||||
const res = await GET(
|
||||
makeReq({ code: "auth-code", state: "S", cookie: "S" }),
|
||||
);
|
||||
expect(exchangeMock).toHaveBeenCalledWith("auth-code", {
|
||||
clientId: "c",
|
||||
clientSecret: "s",
|
||||
});
|
||||
expect(exchangeMock).toHaveBeenCalledWith(
|
||||
"auth-code",
|
||||
{ clientId: "c", clientSecret: "s" },
|
||||
null,
|
||||
);
|
||||
const data = userUpdate.mock.calls[0]![0].data;
|
||||
expect(data.ouraAccessTokenEncrypted).toBe("enc:at");
|
||||
expect(data.ouraRefreshTokenEncrypted).toBe("enc:rt");
|
||||
@@ -139,6 +147,34 @@ describe("GET /api/oura/callback", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Oura's current authorization server names itself on the callback
|
||||
// (RFC 9207); the issuer travels on to pick the token endpoint.
|
||||
describe("GET /api/oura/callback — issuer", () => {
|
||||
it("passes Oura's issuer on to the code exchange", async () => {
|
||||
const iss = "https://moi.ouraring.com/oauth/v2/ext/oauth-anonymous";
|
||||
const res = await GET(makeReq({ code: "c", state: "S", cookie: "S", iss }));
|
||||
expect(exchangeMock).toHaveBeenCalledWith(
|
||||
"c",
|
||||
{ clientId: "c", clientSecret: "s" },
|
||||
iss,
|
||||
);
|
||||
expect(res.headers.get("location")).toContain("oura=connected");
|
||||
});
|
||||
|
||||
it("refuses a callback that names a foreign authorization server", async () => {
|
||||
const res = await GET(
|
||||
makeReq({
|
||||
code: "c",
|
||||
state: "S",
|
||||
cookie: "S",
|
||||
iss: "https://evil.example/oauth",
|
||||
}),
|
||||
);
|
||||
expect(res.headers.get("location")).toContain("reason=issuer");
|
||||
expect(exchangeMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* v1.32.28 — connecting starts a sync straight away. The poll handler has
|
||||
* accepted a single-user payload since it was written; this callback is the
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getSession } from "@/lib/auth/session";
|
||||
import { annotate, getEvent } from "@/lib/logging/context";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { encrypt } from "@/lib/crypto";
|
||||
import { exchangeCode } from "@/lib/oura/client";
|
||||
import { exchangeCode, isAcceptedOuraIssuer } from "@/lib/oura/client";
|
||||
import { getOuraClientCredentials } from "@/lib/oura/credentials";
|
||||
import {
|
||||
oauthStateCookieName,
|
||||
@@ -67,11 +67,20 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
if (!code) return ERR("nocode");
|
||||
|
||||
// RFC 9207: an authorization server that names itself must be Oura's. The
|
||||
// issuer also tells which token endpoint the code belongs to. Older apps
|
||||
// send none, which is fine.
|
||||
const iss = searchParams.get("iss");
|
||||
if (!isAcceptedOuraIssuer(iss)) {
|
||||
annotate({ meta: { reason: "issuer" } });
|
||||
return ERR("issuer");
|
||||
}
|
||||
|
||||
try {
|
||||
const creds = await getOuraClientCredentials(userId);
|
||||
if (!creds) return ERR("nocreds");
|
||||
|
||||
const tokens = await exchangeCode(code, creds);
|
||||
const tokens = await exchangeCode(code, creds, iss);
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
|
||||
Reference in new issue
Block a user