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:
Marc-André Bombeck committed 2026-10-15 21:49:02 +02:00
1 parent 6e72de1c35
commit 3786deb503
74 files changed
+6614 -282

No files matched your search

+37
View File
@@ -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");
});
+42
View File
@@ -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");
});
+9 -1
View File
@@ -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();