Files
tyx_AI_xhs/apps/api/src/app.ts
T

1030 lines
36 KiB
TypeScript

import { randomBytes, randomUUID } from "node:crypto";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import {
AccountDeletionCompleteRequestSchema,
AccountDeletionResponseSchema,
AccountDeletionSendResponseSchema,
AccountProfileUpdateRequestSchema,
AccountProfileUpdateResponseSchema,
AccountSettingsResponseSchema,
AdminAuthenticatedUserSchema,
AdminLoginCompleteRequestSchema,
AdminLoginCompleteResponseSchema,
AdminLoginSendRequestSchema,
AdminSessionResponseSchema,
BootstrapResponseSchema,
CorrelationIdSchema,
AuthenticatedUserSchema,
CreditSummarySchema,
CsrfHeadersSchema,
ErrorDetailsSchema,
ErrorEnvelopeSchema,
GenerationErrorCategorySchema,
LoginCompleteRequestSchema,
LoginCompleteResponseSchema,
LoginSendRequestSchema,
LogoutHeadersSchema,
LogoutResponseSchema,
ModelConfigSseEventSchema,
ModelRuntimeSseEventSchema,
RegistrationCompleteHeadersSchema,
RegistrationCompleteRequestSchema,
RegistrationCompleteResponseSchema,
RegistrationSendRequestSchema,
RegistrationSendResponseSchema,
SseEventSchema,
StableEngineeringErrorCodeSchema,
StateSseEventSchema,
Type,
UserSessionResponseSchema,
createErrorEnvelope,
isCorrelationId,
type BootstrapResponse,
type AdminLoginCompleteRequest,
type AdminLoginSendRequest,
type AccountDeletionCompleteRequest,
type AccountProfileUpdateRequest,
type LoginCompleteRequest,
type LoginSendRequest,
type RegistrationCompleteRequest,
type RegistrationSendRequest,
} from "@dada/shared-contracts";
import swagger from "@fastify/swagger";
import Fastify, { type FastifyReply } from "fastify";
import {
BrowserSupportRequestSchema,
BrowserSupportSuccessSchema,
BrowserUnsupportedReasonSchema,
browserSupportCookieMaxAgeSeconds,
browserSupportCookieName,
checkBrowserSupport,
createBrowserSupportCookie,
supportedBrowserSummary,
verifyBrowserSupportCookie,
type BrowserSupportRelease,
type BrowserUnsupportedReason,
} from "./browser-support.js";
import { EventHub } from "./event-hub.js";
import type { PublicAssetResolver } from "./local-data-root.js";
import { isAllowedNetworkRequest, type NetworkBoundaryOptions } from "./network-boundary.js";
import {
RegistrationError,
registrationFieldError,
} from "./registration-errors.js";
import type { RegistrationService } from "./registration.js";
const defaultBootstrap: BootstrapResponse = {
app_version: "0.0.0",
dependencies: [],
model_summary: {
config_set_version: null,
configured_default_model_id: null,
recommended_model_id: null,
runtime_availability_version: null,
},
public_features: [],
};
export interface CreateAppOptions {
bootstrap?: () => BootstrapResponse | Promise<BootstrapResponse>;
browserGate?: boolean;
browserSupportRelease?: BrowserSupportRelease;
browserSupportSecret?: Buffer;
eventHub?: EventHub;
networkBoundary?: NetworkBoundaryOptions;
publicAssets?: PublicAssetResolver;
registration?: RegistrationService;
}
const supportGateDirectory = resolve(process.env.DADA_SUPPORT_GATE_ROOT ?? "apps/web/support-gate");
const supportGateHtml = readFileSync(resolve(supportGateDirectory, "index.html"), "utf8");
const supportGateCss = readFileSync(resolve(supportGateDirectory, "support-gate.css"), "utf8");
const supportGateJavaScript = readFileSync(resolve(supportGateDirectory, "support-gate.js"), "utf8");
const clientHints = "Sec-CH-UA, Sec-CH-UA-Full-Version-List, Sec-CH-UA-Platform";
const contentSecurityPolicy = [
"default-src 'self'",
"script-src 'self'",
"style-src 'self'",
"img-src 'self' blob:",
"font-src 'self' blob:",
"connect-src 'self'",
"object-src 'none'",
"base-uri 'none'",
"frame-ancestors 'none'",
].join("; ");
const authFlowCookieName = "dada_auth_flow";
const userSessionCookieName = "dada_session";
const adminAuthFlowCookieName = "dada_admin_auth_flow";
const adminSessionCookieName = "dada_admin_session";
function requestCorrelationId(headers: Record<string, string | string[] | undefined>) {
const header = headers["x-correlation-id"];
const candidate = Array.isArray(header) ? header[0] : header;
return isCorrelationId(candidate) ? candidate : randomUUID();
}
function headerValue(value: string | string[] | undefined) {
return Array.isArray(value) ? value[0] : value;
}
function cookieValue(cookieHeader: string | undefined, name: string) {
if (!cookieHeader) return undefined;
for (const item of cookieHeader.split(";")) {
const separator = item.indexOf("=");
if (separator < 0) continue;
if (item.slice(0, separator).trim() === name) return item.slice(separator + 1).trim();
}
return undefined;
}
function registrationFailure(reply: FastifyReply, correlationId: string, error: unknown) {
if (error instanceof RegistrationError) {
return reply.code(error.httpStatus).send(
createErrorEnvelope({
code: error.code,
correlationId,
details: { field_errors: [registrationFieldError(error.reason)] },
}),
);
}
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId }));
}
function registrationValidationFailure(reply: FastifyReply, correlationId: string) {
return reply.code(400).send(
createErrorEnvelope({
code: "REGISTRATION_REQUEST_INVALID",
correlationId,
details: { field_errors: [{ field: "request", message_key: "auth.registration.request_invalid" }] },
}),
);
}
function isSupportGateRequest(method: string, path: string) {
if (method === "POST" && path === "/api/v1/support/check") return true;
if (method !== "GET" && method !== "HEAD") return false;
return (
path === "/" ||
path === "/app" ||
path.startsWith("/app/") ||
path === "/admin" ||
path.startsWith("/admin/") ||
path === "/support-gate.css" ||
path === "/support-gate.js" ||
path === "/RELEASE.json" ||
path === "/healthz"
);
}
function sendBrowserUnsupported(
reply: FastifyReply,
correlationId: string,
reason: BrowserUnsupportedReason,
release: BrowserSupportRelease | undefined,
) {
return reply.code(426).send(
createErrorEnvelope({
code: "BROWSER_UNSUPPORTED",
correlationId,
details: {
reason,
supported_browsers: supportedBrowserSummary(release),
},
}),
);
}
export async function createApp(options: CreateAppOptions = {}) {
const eventHub = options.eventHub ?? new EventHub();
const bootstrap = options.bootstrap ?? (() => defaultBootstrap);
const browserGate = options.browserGate ?? true;
const browserSupportSecret = options.browserSupportSecret ?? randomBytes(32);
const browserSupportRelease = options.browserSupportRelease;
const app = Fastify({
genReqId: (request) => requestCorrelationId(request.headers),
logger: false,
});
await app.register(swagger, {
openapi: {
info: {
title: "Dada P0-A",
version: "0.0.0",
},
openapi: "3.1.0",
},
refResolver: {
buildLocalReference: (schema, _baseUri, _fragment, index) =>
typeof schema.$id === "string" ? schema.$id : `schema-${index}`,
},
});
for (const schema of [
CorrelationIdSchema,
GenerationErrorCategorySchema,
StableEngineeringErrorCodeSchema,
ErrorDetailsSchema,
ErrorEnvelopeSchema,
AuthenticatedUserSchema,
AdminAuthenticatedUserSchema,
AdminLoginSendRequestSchema,
AdminLoginCompleteRequestSchema,
AdminLoginCompleteResponseSchema,
AdminSessionResponseSchema,
CreditSummarySchema,
CsrfHeadersSchema,
AccountSettingsResponseSchema,
AccountProfileUpdateRequestSchema,
AccountProfileUpdateResponseSchema,
AccountDeletionSendResponseSchema,
AccountDeletionCompleteRequestSchema,
AccountDeletionResponseSchema,
RegistrationSendRequestSchema,
RegistrationSendResponseSchema,
RegistrationCompleteRequestSchema,
RegistrationCompleteHeadersSchema,
RegistrationCompleteResponseSchema,
LoginSendRequestSchema,
LoginCompleteRequestSchema,
LoginCompleteResponseSchema,
LogoutHeadersSchema,
LogoutResponseSchema,
UserSessionResponseSchema,
BrowserUnsupportedReasonSchema,
BrowserSupportRequestSchema,
BrowserSupportSuccessSchema,
BootstrapResponseSchema,
StateSseEventSchema,
ModelConfigSseEventSchema,
ModelRuntimeSseEventSchema,
SseEventSchema,
]) {
app.addSchema(schema);
}
app.addHook("onRequest", async (request, reply) => {
reply.header("X-Correlation-Id", request.id);
reply.header("Accept-CH", clientHints);
reply.header("Cache-Control", "no-store");
reply.header("Content-Security-Policy", contentSecurityPolicy);
reply.header("X-Content-Type-Options", "nosniff");
const host = headerValue(request.headers.host);
const origin = headerValue(request.headers.origin);
if (!isAllowedNetworkRequest({ host, method: request.method, origin }, options.networkBoundary)) {
return sendBrowserUnsupported(reply, request.id, "identity_unavailable", browserSupportRelease);
}
const path = request.url.split("?", 1)[0] ?? "/";
if (!browserGate || isSupportGateRequest(request.method, path)) return;
const verified = verifyBrowserSupportCookie({
cookieHeader: headerValue(request.headers.cookie),
release: browserSupportRelease,
secChUa: headerValue(request.headers["sec-ch-ua"]),
secret: browserSupportSecret,
});
if (!verified.supported) {
return sendBrowserUnsupported(reply, request.id, verified.reason, browserSupportRelease);
}
});
for (const route of ["/", "/app", "/app/*", "/admin", "/admin/*"]) {
app.get(route, { schema: { hide: true } }, async (_request, reply) => {
reply.type("text/html; charset=utf-8");
return supportGateHtml;
});
}
app.get("/support-gate.css", { schema: { hide: true } }, async (_request, reply) => {
reply.type("text/css; charset=utf-8");
return supportGateCss;
});
app.get("/support-gate.js", { schema: { hide: true } }, async (_request, reply) => {
reply.type("text/javascript; charset=utf-8");
return supportGateJavaScript;
});
app.get("/RELEASE.json", { schema: { hide: true } }, async () => ({
app_version: browserSupportRelease?.appVersion ?? null,
browsers: supportedBrowserSummary(browserSupportRelease),
}));
app.get("/healthz", { schema: { hide: true } }, async () => ({
bind_scope: "loopback",
port: 43121,
status: "ready",
}));
app.get(
"/api/v1/assets/public/:resourceVersion/:assetId",
{ schema: { hide: true } },
async (request, reply) => {
const { assetId, resourceVersion } = request.params as { assetId?: string; resourceVersion?: string };
const resource = assetId && resourceVersion
? options.publicAssets?.read(resourceVersion, assetId)
: undefined;
if (!resource) return reply.code(404).send();
reply.type(resource.mimeType);
reply.header("Cache-Control", "public, max-age=31536000, immutable");
reply.header("Content-Disposition", "inline");
reply.header("ETag", `"sha256-${resource.sha256}"`);
return resource.bytes;
},
);
app.post(
"/api/v1/admin-auth/login/send",
{
attachValidation: true,
schema: {
body: Type.Ref(AdminLoginSendRequestSchema),
operationId: "sendAdminLoginCode",
response: {
200: Type.Ref(RegistrationSendResponseSchema),
400: Type.Ref(ErrorEnvelopeSchema),
409: Type.Ref(ErrorEnvelopeSchema),
429: Type.Ref(ErrorEnvelopeSchema),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Admin Authentication"],
},
},
async (request, reply) => {
if (request.validationError) return registrationValidationFailure(reply, request.id);
if (!options.registration) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
const existingFlow = cookieValue(headerValue(request.headers.cookie), adminAuthFlowCookieName);
const clientKey = existingFlow ?? randomBytes(32).toString("base64url");
try {
const body = request.body as AdminLoginSendRequest;
const result = await options.registration.sendAdminLoginCode({ clientKey, email: body.email });
if (!existingFlow) {
reply.header(
"Set-Cookie",
`${adminAuthFlowCookieName}=${clientKey}; Max-Age=${10 * 60}; Path=/; HttpOnly; SameSite=Strict`,
);
}
return {
challenge_expires_at: new Date(result.challengeExpiresAt).toISOString(),
registration_id: result.registrationId,
resend_available_at: new Date(result.resendAvailableAt).toISOString(),
status: result.status,
};
} catch (error) {
return registrationFailure(reply, request.id, error);
}
},
);
app.post(
"/api/v1/admin-auth/login/complete",
{
attachValidation: true,
schema: {
body: Type.Ref(AdminLoginCompleteRequestSchema),
headers: Type.Ref(RegistrationCompleteHeadersSchema),
operationId: "completeAdminLogin",
response: {
200: Type.Ref(AdminLoginCompleteResponseSchema),
400: Type.Ref(ErrorEnvelopeSchema),
409: Type.Ref(ErrorEnvelopeSchema),
429: Type.Ref(ErrorEnvelopeSchema),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Admin Authentication"],
},
},
async (request, reply) => {
if (request.validationError) return registrationValidationFailure(reply, request.id);
if (!options.registration) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
const clientKey = cookieValue(headerValue(request.headers.cookie), adminAuthFlowCookieName);
const idempotencyKey = headerValue(request.headers["idempotency-key"]);
if (!clientKey || !idempotencyKey) return registrationValidationFailure(reply, request.id);
try {
const body = request.body as AdminLoginCompleteRequest;
const result = options.registration.completeAdminLogin({
clientKey,
code: body.verification_code,
idempotencyKey,
registrationId: body.registration_id,
});
reply.header(
"Set-Cookie",
`${adminSessionCookieName}=${result.sessionToken}; Max-Age=${30 * 24 * 60 * 60}; Path=/; HttpOnly; SameSite=Strict`,
);
return {
admin: {
role: result.admin.role,
status: result.admin.status,
user_id: result.admin.userId,
},
audience: result.audience,
session_expires_at: new Date(result.sessionExpiresAt).toISOString(),
status: result.status,
};
} catch (error) {
return registrationFailure(reply, request.id, error);
}
},
);
app.get(
"/api/v1/admin-auth/session",
{
schema: {
operationId: "getAdminSession",
response: {
200: Type.Ref(AdminSessionResponseSchema),
401: Type.Ref(ErrorEnvelopeSchema),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Admin Authentication"],
},
},
async (request, reply) => {
if (!options.registration) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
const token = cookieValue(headerValue(request.headers.cookie), adminSessionCookieName);
const session = token ? options.registration.readAdminSession(token) : undefined;
if (!session) {
return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
}
return {
acknowledged_private_content_notice_version: null,
admin: { role: "super_admin" as const, status: "active" as const, user_id: session.user_id },
audience: "admin" as const,
authenticated: true as const,
csrf_token: options.registration.issueAdminCsrfToken(token!),
current_private_content_notice_version: null,
expires_at: new Date(session.expires_at).toISOString(),
notice_acknowledged: false,
};
},
);
app.post(
"/api/v1/auth/login/send",
{
attachValidation: true,
schema: {
body: Type.Ref(LoginSendRequestSchema),
operationId: "sendLoginCode",
response: {
200: Type.Ref(RegistrationSendResponseSchema),
400: Type.Ref(ErrorEnvelopeSchema),
409: Type.Ref(ErrorEnvelopeSchema),
429: Type.Ref(ErrorEnvelopeSchema),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Authentication"],
},
},
async (request, reply) => {
if (request.validationError) return registrationValidationFailure(reply, request.id);
if (!options.registration) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
const existingFlow = cookieValue(headerValue(request.headers.cookie), authFlowCookieName);
const clientKey = existingFlow ?? randomBytes(32).toString("base64url");
try {
const body = request.body as LoginSendRequest;
const result = await options.registration.sendLoginCode({ clientKey, email: body.email });
if (!existingFlow) {
reply.header(
"Set-Cookie",
`${authFlowCookieName}=${clientKey}; Max-Age=${10 * 60}; Path=/; HttpOnly; SameSite=Strict`,
);
}
return {
challenge_expires_at: new Date(result.challengeExpiresAt).toISOString(),
registration_id: result.registrationId,
resend_available_at: new Date(result.resendAvailableAt).toISOString(),
status: result.status,
};
} catch (error) {
return registrationFailure(reply, request.id, error);
}
},
);
app.post(
"/api/v1/auth/login/complete",
{
attachValidation: true,
schema: {
body: Type.Ref(LoginCompleteRequestSchema),
headers: Type.Ref(RegistrationCompleteHeadersSchema),
operationId: "completeLogin",
response: {
200: Type.Ref(LoginCompleteResponseSchema),
400: Type.Ref(ErrorEnvelopeSchema),
409: Type.Ref(ErrorEnvelopeSchema),
429: Type.Ref(ErrorEnvelopeSchema),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Authentication"],
},
},
async (request, reply) => {
if (request.validationError) return registrationValidationFailure(reply, request.id);
if (!options.registration) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
const clientKey = cookieValue(headerValue(request.headers.cookie), authFlowCookieName);
const idempotencyKey = headerValue(request.headers["idempotency-key"]);
if (!clientKey || !idempotencyKey) return registrationValidationFailure(reply, request.id);
try {
const body = request.body as LoginCompleteRequest;
const result = options.registration.completeLogin({
clientKey,
code: body.verification_code,
idempotencyKey,
registrationId: body.registration_id,
});
reply.header(
"Set-Cookie",
`${userSessionCookieName}=${result.sessionToken}; Max-Age=${30 * 24 * 60 * 60}; Path=/; HttpOnly; SameSite=Strict`,
);
return {
audience: result.audience,
credits: {
available_balance: result.credits.availableBalance,
reserved_balance: result.credits.reservedBalance,
},
session_expires_at: new Date(result.sessionExpiresAt).toISOString(),
status: result.status,
user: {
creator_name: result.user.creatorName,
role: result.user.role,
social_id: result.user.socialId,
status: result.user.status,
user_id: result.user.userId,
},
};
} catch (error) {
return registrationFailure(reply, request.id, error);
}
},
);
app.post(
"/api/v1/auth/register/send",
{
attachValidation: true,
schema: {
body: Type.Ref(RegistrationSendRequestSchema),
operationId: "sendRegistrationCode",
response: {
200: Type.Ref(RegistrationSendResponseSchema),
400: Type.Ref(ErrorEnvelopeSchema),
409: Type.Ref(ErrorEnvelopeSchema),
429: Type.Ref(ErrorEnvelopeSchema),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Authentication"],
},
},
async (request, reply) => {
if (request.validationError) return registrationValidationFailure(reply, request.id);
if (!options.registration) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
try {
const body = request.body as RegistrationSendRequest;
const result = await options.registration.sendRegistrationCode({ email: body.email, inviteCode: body.invite_code });
return {
challenge_expires_at: new Date(result.challengeExpiresAt).toISOString(),
registration_id: result.registrationId,
resend_available_at: new Date(result.resendAvailableAt).toISOString(),
status: result.status,
};
} catch (error) {
return registrationFailure(reply, request.id, error);
}
},
);
app.post(
"/api/v1/auth/register/complete",
{
attachValidation: true,
schema: {
body: Type.Ref(RegistrationCompleteRequestSchema),
headers: Type.Ref(RegistrationCompleteHeadersSchema),
operationId: "completeRegistration",
response: {
200: Type.Ref(RegistrationCompleteResponseSchema),
400: Type.Ref(ErrorEnvelopeSchema),
409: Type.Ref(ErrorEnvelopeSchema),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Authentication"],
},
},
async (request, reply) => {
if (request.validationError) return registrationValidationFailure(reply, request.id);
if (!options.registration) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
try {
const body = request.body as RegistrationCompleteRequest;
const idempotencyKey = headerValue(request.headers["idempotency-key"]);
if (!idempotencyKey) return registrationValidationFailure(reply, request.id);
const result = options.registration.completeRegistration({
code: body.verification_code,
creatorName: body.creator_name,
idempotencyKey,
privacyConsentAccepted: body.privacy_consent_accepted,
privacyNoticeVersion: body.privacy_notice_version,
registrationId: body.registration_id,
socialId: body.social_id,
});
reply.header(
"Set-Cookie",
`${userSessionCookieName}=${result.sessionToken}; Max-Age=${30 * 24 * 60 * 60}; Path=/; HttpOnly; SameSite=Strict`,
);
return {
credits: {
available_balance: result.credits.availableBalance,
reserved_balance: result.credits.reservedBalance,
},
session_expires_at: new Date(result.sessionExpiresAt).toISOString(),
status: result.status,
user: {
creator_name: result.user.creatorName,
role: result.user.role,
social_id: result.user.socialId,
status: result.user.status,
user_id: result.user.userId,
},
};
} catch (error) {
return registrationFailure(reply, request.id, error);
}
},
);
app.get(
"/api/v1/auth/session",
{
schema: {
operationId: "getUserSession",
response: {
200: Type.Ref(UserSessionResponseSchema),
401: Type.Ref(ErrorEnvelopeSchema),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Authentication"],
},
},
async (request, reply) => {
if (!options.registration) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
const token = cookieValue(headerValue(request.headers.cookie), userSessionCookieName);
const session = token ? options.registration.readUserSession(token) : undefined;
if (!session) {
return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
}
return {
audience: session.audience,
authenticated: true as const,
credits: {
available_balance: session.credits.availableBalance,
reserved_balance: session.credits.reservedBalance,
},
csrf_token: options.registration.issueUserCsrfToken(token!),
expires_at: new Date(session.expiresAt).toISOString(),
user: {
creator_name: session.user.creatorName,
role: session.user.role,
social_id: session.user.socialId,
status: session.user.status,
user_id: session.user.userId,
},
};
},
);
app.post(
"/api/v1/auth/logout",
{
attachValidation: true,
schema: {
headers: Type.Ref(LogoutHeadersSchema),
operationId: "logoutUser",
response: {
200: Type.Ref(LogoutResponseSchema),
400: Type.Ref(ErrorEnvelopeSchema),
401: Type.Ref(ErrorEnvelopeSchema),
403: Type.Ref(ErrorEnvelopeSchema),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Authentication"],
},
},
async (request, reply) => {
if (request.validationError) return registrationValidationFailure(reply, request.id);
if (!options.registration) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
const token = cookieValue(headerValue(request.headers.cookie), userSessionCookieName);
const csrfToken = headerValue(request.headers["x-csrf-token"]);
if (!token || !csrfToken) {
return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
}
try {
options.registration.logoutUser({ csrfToken, sessionToken: token });
reply.header("Set-Cookie", `${userSessionCookieName}=; Max-Age=0; Path=/; HttpOnly; SameSite=Strict`);
return { status: "logged_out" as const };
} catch (error) {
return registrationFailure(reply, request.id, error);
}
},
);
app.get(
"/api/v1/account/settings",
{
schema: {
operationId: "getAccountSettings",
response: {
200: Type.Ref(AccountSettingsResponseSchema),
401: Type.Ref(ErrorEnvelopeSchema),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Account"],
},
},
async (request, reply) => {
if (!options.registration) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
const token = cookieValue(headerValue(request.headers.cookie), userSessionCookieName);
if (!token) return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
try {
const settings = options.registration.readAccountSettings(token);
const csrfToken = options.registration.issueUserCsrfToken(token);
return {
account: settings.account,
csrf_token: csrfToken,
local_data: {
backup_enabled: settings.localData.backupEnabled,
capacity_status: settings.localData.capacityStatus,
hard_limit_bytes: settings.localData.hardLimitBytes,
location: settings.localData.location,
managed_content_bytes: settings.localData.managedContentBytes,
migration_supported: settings.localData.migrationSupported,
},
profile: {
creator_name: settings.profile.creatorName,
social_id: settings.profile.socialId,
},
};
} catch (error) {
return registrationFailure(reply, request.id, error);
}
},
);
app.put(
"/api/v1/account/settings/profile",
{
attachValidation: true,
schema: {
body: Type.Ref(AccountProfileUpdateRequestSchema),
headers: Type.Ref(CsrfHeadersSchema),
operationId: "updateAccountProfile",
response: {
200: Type.Ref(AccountProfileUpdateResponseSchema),
400: Type.Ref(ErrorEnvelopeSchema),
401: Type.Ref(ErrorEnvelopeSchema),
403: Type.Ref(ErrorEnvelopeSchema),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Account"],
},
},
async (request, reply) => {
if (request.validationError) return registrationValidationFailure(reply, request.id);
if (!options.registration) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
const token = cookieValue(headerValue(request.headers.cookie), userSessionCookieName);
const csrfToken = headerValue(request.headers["x-csrf-token"]);
if (!token || !csrfToken) return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
try {
const body = request.body as AccountProfileUpdateRequest;
const saved = options.registration.updateAccountProfile({
creatorName: body.creator_name,
csrfToken,
sessionToken: token,
socialId: body.social_id,
});
return { profile: { creator_name: saved.creatorName, social_id: saved.socialId }, status: saved.status };
} catch (error) {
return registrationFailure(reply, request.id, error);
}
},
);
app.post(
"/api/v1/account/deletion/send",
{
attachValidation: true,
schema: {
headers: Type.Ref(CsrfHeadersSchema),
operationId: "sendAccountDeletionCode",
response: {
200: Type.Ref(AccountDeletionSendResponseSchema),
400: Type.Ref(ErrorEnvelopeSchema),
401: Type.Ref(ErrorEnvelopeSchema),
403: Type.Ref(ErrorEnvelopeSchema),
429: Type.Ref(ErrorEnvelopeSchema),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Account"],
},
},
async (request, reply) => {
if (request.validationError) return registrationValidationFailure(reply, request.id);
if (!options.registration) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
const token = cookieValue(headerValue(request.headers.cookie), userSessionCookieName);
const csrfToken = headerValue(request.headers["x-csrf-token"]);
if (!token || !csrfToken) return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
try {
const sent = await options.registration.sendAccountDeletionCode({ csrfToken, sessionToken: token });
return {
challenge_expires_at: new Date(sent.challengeExpiresAt).toISOString(),
deletion_id: sent.deletionId,
resend_available_at: new Date(sent.resendAvailableAt).toISOString(),
status: sent.status,
};
} catch (error) {
return registrationFailure(reply, request.id, error);
}
},
);
app.post(
"/api/v1/account/deletion/complete",
{
attachValidation: true,
schema: {
body: Type.Ref(AccountDeletionCompleteRequestSchema),
headers: Type.Ref(LogoutHeadersSchema),
operationId: "completeAccountDeletion",
response: {
200: Type.Ref(AccountDeletionResponseSchema),
400: Type.Ref(ErrorEnvelopeSchema),
401: Type.Ref(ErrorEnvelopeSchema),
403: Type.Ref(ErrorEnvelopeSchema),
409: Type.Ref(ErrorEnvelopeSchema),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Account"],
},
},
async (request, reply) => {
if (request.validationError) return registrationValidationFailure(reply, request.id);
if (!options.registration) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
const token = cookieValue(headerValue(request.headers.cookie), userSessionCookieName);
const csrfToken = headerValue(request.headers["x-csrf-token"]);
const idempotencyKey = headerValue(request.headers["idempotency-key"]);
if (!token || !csrfToken || !idempotencyKey) {
return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
}
try {
const body = request.body as AccountDeletionCompleteRequest;
const deleted = options.registration.completeAccountDeletion({
code: body.verification_code,
confirmation: body.confirmation,
csrfToken,
deletionId: body.deletion_id,
idempotencyKey,
sessionToken: token,
});
reply.header("Set-Cookie", `${userSessionCookieName}=; Max-Age=0; Path=/; HttpOnly; SameSite=Strict`);
return deleted;
} catch (error) {
return registrationFailure(reply, request.id, error);
}
},
);
app.post(
"/api/v1/support/check",
{
attachValidation: true,
schema: {
body: BrowserSupportRequestSchema,
operationId: "checkBrowserSupport",
response: {
200: BrowserSupportSuccessSchema,
426: ErrorEnvelopeSchema,
},
tags: ["Browser support"],
},
},
async (request, reply) => {
const checked = checkBrowserSupport(
request.validationError ? undefined : request.body,
{
secChUa: headerValue(request.headers["sec-ch-ua"]),
secChUaFullVersionList: headerValue(request.headers["sec-ch-ua-full-version-list"]),
secChUaPlatform: headerValue(request.headers["sec-ch-ua-platform"]),
},
browserSupportRelease,
);
if (!checked.supported || !browserSupportRelease) {
return sendBrowserUnsupported(
reply,
request.id,
checked.supported ? "version_unsupported" : checked.reason,
browserSupportRelease,
);
}
const cookie = createBrowserSupportCookie(
browserSupportSecret,
browserSupportRelease,
checked.identity,
);
reply.header(
"Set-Cookie",
`${browserSupportCookieName}=${cookie}; Max-Age=${browserSupportCookieMaxAgeSeconds}; Path=/; HttpOnly; SameSite=Strict`,
);
return {
app_version: browserSupportRelease.appVersion,
browser: checked.identity,
status: "supported" as const,
supported_browsers: supportedBrowserSummary(browserSupportRelease),
};
},
);
app.get(
"/api/v1/bootstrap",
{
schema: {
operationId: "getBootstrap",
response: {
200: BootstrapResponseSchema,
426: ErrorEnvelopeSchema,
},
tags: ["Bootstrap"],
},
},
async () => bootstrap(),
);
app.get(
"/api/v1/events",
{
schema: {
operationId: "getEvents",
response: {
200: {
content: {
"text/event-stream": {
schema: SseEventSchema,
},
},
description: "Non-sensitive state change hints. REST remains authoritative.",
},
426: ErrorEnvelopeSchema,
},
tags: ["State events"],
},
},
(request, reply) => {
reply.hijack();
reply.raw.setHeader("Cache-Control", "no-store");
reply.raw.setHeader("Connection", "keep-alive");
reply.raw.setHeader("Content-Type", "text/event-stream; charset=utf-8");
reply.raw.setHeader("X-Correlation-Id", request.id);
reply.raw.writeHead(200);
reply.raw.write(": connected\n\n");
const unsubscribe = eventHub.connect(
(event) => {
reply.raw.write(`id: ${event.event_id}\n`);
reply.raw.write(`data: ${JSON.stringify(event)}\n\n`);
},
() => reply.raw.end(),
);
request.raw.once("close", unsubscribe);
},
);
return app;
}