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; 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) { 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; }