mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +00:00
feat(api): publish the v1.43 contract with routes that answer 501 until filled
Every new endpoint of the contract exists, wrapped in apiHandler, with its authentication, module gate and request validation, and answers 501 not_implemented until its behaviour lands. The OpenAPI document carries the new operations and schemas and the additive response fields on the changed routes. The four server feature flags are published as false. contract-stub-inventory.test.ts registers the stubbed routes; each one leaves the register with the change that fills it. The route, sharing, session, security, module-gate and outcome inventories name the new routes deliberately, and the error-code catalogue lists the codes the routes emit today.
This commit is contained in:
1 parent
6e72de1c35
commit
3786deb503
74 files changed
+6614
-282
No files matched your search
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* `POST /api/auth/claim/sso` — start claiming a managed profile through
|
||||
* single sign-on (#959).
|
||||
*
|
||||
* Anonymous, like the password claim beside it. `{ token, username }`: the
|
||||
* link is checked and the username reserved for the round trip, a sealed
|
||||
* `hl_claim` cookie (10 minutes, `httpOnly`, `Secure` through
|
||||
* `shouldEmitSecureCookie()`) carries `{ handoverId, username, exp }`, and the
|
||||
* answer names where to go: `{ redirectTo: "/api/auth/oidc/login?intent=claim" }`.
|
||||
* The OIDC callback binds the identity provider's subject to the profile. The
|
||||
* token never rides a URL. Works on a single-sign-on-only server; 404
|
||||
* `profile_claim.sso_unavailable` when no identity provider is configured.
|
||||
*/
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler } from "@/lib/api-handler";
|
||||
import { returnAllZodIssues, safeJson } from "@/lib/api-response";
|
||||
import { getSession } from "@/lib/auth/session";
|
||||
import { notImplemented } from "@/lib/http/not-implemented";
|
||||
import { claimGate } from "@/lib/managed-profiles/claim-gate";
|
||||
import { claimSsoSchema } from "@/lib/validations/claim-sso";
|
||||
|
||||
export const POST = apiHandler(async (request: NextRequest) => {
|
||||
// Refuses a signed-in caller and rate-limits the address, as the password
|
||||
// claim does.
|
||||
const gate = await claimGate(request, "claim", (await getSession()) !== null);
|
||||
if (gate.refusal) return gate.refusal;
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
maxBytes: 4 * 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = claimSsoSchema.safeParse(body);
|
||||
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
|
||||
|
||||
return notImplemented("profile_claim.sso.start");
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* `PUT /api/auth/me/ai-usage` — the account's own AI usage level (#1217).
|
||||
*
|
||||
* `{ level: "frugal" | "standard" | "full" | null }`; `null` follows the
|
||||
* instance default. A level above the operator's ceiling is refused with 422
|
||||
* `ai.usage.aboveCeiling`, never capped silently. Answers the resolved
|
||||
* `AiUsageState`, the same block `GET /api/auth/me` publishes as `ai.usage`.
|
||||
* Own account only (cookie or a full-scope token), at most 30 writes an hour.
|
||||
*/
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { apiError, returnAllZodIssues, safeJson } from "@/lib/api-response";
|
||||
import { notImplemented } from "@/lib/http/not-implemented";
|
||||
import { checkRateLimit } from "@/lib/rate-limit";
|
||||
import { aiUsagePutSchema } from "@/lib/validations/ai-usage";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
const { user } = await requireAuth();
|
||||
|
||||
const rl = await checkRateLimit(`ai-usage:${user.id}`, 30, 60 * 60 * 1000);
|
||||
if (!rl.allowed) {
|
||||
return apiError("Too many changes, try again later", 429, {
|
||||
errorCode: "ai.usage.rateLimited",
|
||||
});
|
||||
}
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request, {
|
||||
maxBytes: 1024,
|
||||
});
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = aiUsagePutSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return returnAllZodIssues(parsed.error, 422, {
|
||||
errorCode: "ai.usage.invalid",
|
||||
});
|
||||
}
|
||||
|
||||
return notImplemented("auth.me.ai_usage.put");
|
||||
});
|
||||
@@ -83,7 +83,15 @@ export const dynamic = "force-dynamic";
|
||||
* by design: a feature the server supports is supported for every account,
|
||||
* and a per-account answer belongs in `modules` or `ai`, not here.
|
||||
*/
|
||||
const SERVER_FEATURES = { trackIntake: true } as const;
|
||||
const SERVER_FEATURES = {
|
||||
trackIntake: true,
|
||||
// v1.43 — each turns true in the change that makes its route or request
|
||||
// form work; until then a client must not send it (contract §0.6).
|
||||
cumulativeHourBuckets: false,
|
||||
syncTriggerBackfill: false,
|
||||
biomarkerMerge: false,
|
||||
lifeCircumstances: false,
|
||||
} as const;
|
||||
|
||||
export const GET = apiHandler(async () => {
|
||||
const auth = await requireActorAuth();
|
||||
|
||||
Reference in new issue
Block a user