Merge branch 'v1422/fixes' into release/v1.42.2

This commit is contained in:
Marc-André Bombeck committed 2026-10-11 13:28:29 +02:00
commit 3c4e053dcd
83 files changed
+1808 -258

No files matched your search

+5 -3
View File
@@ -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
+1 -1
View File
@@ -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),
+2 -2
View File
@@ -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,
+1 -1
View File
@@ -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")
+5 -2
View File
@@ -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
+11 -2
View File
@@ -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 },