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

2873 lines
114 KiB
TypeScript

import { randomBytes, randomUUID } from "node:crypto";
import { createReadStream, readFileSync } from "node:fs";
import { resolve } from "node:path";
import {
AccountDeletionCompleteRequestSchema,
AccountDeletionResponseSchema,
AccountDeletionSendResponseSchema,
AccountProfileUpdateRequestSchema,
AccountProfileUpdateResponseSchema,
AccountSettingsResponseSchema,
AdminAuthenticatedUserSchema,
AdminCreditParamsSchema,
AdminLoginCompleteRequestSchema,
AdminLoginCompleteResponseSchema,
AdminLoginSendRequestSchema,
AdminSessionResponseSchema,
BootstrapResponseSchema,
CanvasBackgroundAdjustmentsSchema,
CanvasElementSchema,
CanvasStateSchema,
CorrelationIdSchema,
AuthenticatedUserSchema,
CreditSummarySchema,
CreditAdjustmentHeadersSchema,
CreditAdjustmentRequestSchema,
CreditAdjustmentResponseSchema,
CreditBalanceResponseSchema,
CreditEntryStatusSchema,
CreditEntryTypeSchema,
CreditLedgerEntrySchema,
CreditLedgerQuerySchema,
CreditLedgerResponseSchema,
CreditReferenceTypeSchema,
CsrfHeadersSchema,
ErrorDetailsSchema,
ErrorEnvelopeSchema,
GenerationErrorCategorySchema,
GenerationCreateHeadersSchema,
GenerationCreateResponseSchema,
GenerationParamsSchema,
GenerationMultipartBodySchema,
GenerationTaskResponseSchema,
GenerationTaskStatusSchema,
LoginCompleteRequestSchema,
LoginCompleteResponseSchema,
LoginSendRequestSchema,
LogoutHeadersSchema,
LogoutResponseSchema,
ModelConfigSseEventSchema,
ModelRuntimeSseEventSchema,
ModelIdSchema,
ModelContractValidationStatusSchema,
ModelRuntimeReasonSchema,
ModelReferenceLimitsSchema,
ModelRuntimeAvailabilitySchema,
ModelConfigurationResponseSchema,
ModelConfigSchema,
ModelConfigCandidateSchema,
ModelConfigUpdateRequestSchema,
ModelParamsSchema,
ModelConfigUpdateHeadersSchema,
FailedEmptyTrashRequestSchema,
FailedEmptyTrashResponseSchema,
ExportFormatSchema,
GenerationProjectItemSchema,
LatestExportItemSchema,
LatestExportMultipartBodySchema,
LatestExportParamsSchema,
LatestExportSaveResponseSchema,
ProjectDetailResponseSchema,
ProjectEditableStateSchema,
ProjectIdSchema,
ProjectImageItemSchema,
ProjectImageParamsSchema,
ProjectListQuerySchema,
ProjectListResponseSchema,
ProjectParamsSchema,
ProjectRatioSchema,
ProjectRenameRequestSchema,
ProjectRenameResponseSchema,
ProjectRestoreResponseSchema,
ProjectPurgeResponseSchema,
ProjectTrashResponseSchema,
ProjectStateConflictResponseSchema,
ProjectStateSaveHeadersSchema,
ProjectStateSaveResponseSchema,
ProjectSummarySchema,
ProjectViewStatusSchema,
RecentAssetItemSchema,
RecentAssetKindSchema,
RecentAssetListResponseSchema,
RecentAssetQuerySchema,
RecentAssetRecordRequestSchema,
RecentAssetRecordResponseSchema,
ReverseGeocodeRequestSchema,
ReverseGeocodeResponseSchema,
RegistrationCompleteHeadersSchema,
RegistrationCompleteRequestSchema,
RegistrationCompleteResponseSchema,
RegistrationSendRequestSchema,
RegistrationSendResponseSchema,
SseEventSchema,
StableEngineeringErrorCodeSchema,
StateSseEventSchema,
Type,
UserSessionResponseSchema,
createErrorEnvelope,
isCorrelationId,
type BootstrapResponse,
type AdminLoginCompleteRequest,
type AdminLoginSendRequest,
type AccountDeletionCompleteRequest,
type AccountProfileUpdateRequest,
type AdminCreditParams,
type CreditAdjustmentHeaders,
type CreditAdjustmentRequest,
type CreditLedgerQuery,
type LoginCompleteRequest,
type LoginSendRequest,
type FailedEmptyTrashRequest,
type LatestExportParams,
type ProjectImageParams,
type GenerationCreateHeaders,
type GenerationParams,
type ProjectListQuery,
type ProjectParams,
type ProjectRenameRequest,
type ProjectEditableState,
type ProjectStateSaveHeaders,
type RecentAssetQuery,
type RecentAssetRecordRequest,
type ReverseGeocodeRequest,
type RegistrationCompleteRequest,
type RegistrationSendRequest,
} from "@dada/shared-contracts";
import type { AssetReleaseReader } from "@dada/asset-release-manifest";
import swagger from "@fastify/swagger";
import multipart from "@fastify/multipart";
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 { CreditError } from "./credit-errors.js";
import type { CreditService } from "./credits.js";
import type { PublicAssetResolver } from "./local-data-root.js";
import { GenerationSubmissionError } from "./generation-submission-errors.js";
import type {
GenerationSubmissionFields,
GenerationSubmissionService,
GenerationTaskView,
GenerationUploadSession,
NewGenerationReference,
} from "./generation-submission.js";
import { isAllowedNetworkRequest, type NetworkBoundaryOptions } from "./network-boundary.js";
import { ProjectError } from "./project-errors.js";
import type { ProjectService } from "./projects.js";
import { LatestExportError } from "./latest-export-errors.js";
import type { LatestExportService } from "./latest-exports.js";
import {
RegistrationError,
registrationFieldError,
} from "./registration-errors.js";
import type { RegistrationService } from "./registration.js";
import type { AssetPreviewGrantService } from "./preview-grants.js";
import type { RecentAssetService } from "./recent-assets.js";
import type { AmapAdapter } from "./amap-adapter.js";
import { ModelConfigurationError } from "./model-configuration.js";
import type { ModelConfigurationService } from "./model-configuration.js";
import { StickerReleaseError } from "./sticker-release-errors.js";
import type { StickerReleaseService } from "./sticker-releases.js";
import type { ManagedStorage } from "./managed-storage.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 {
amap?: AmapAdapter;
assetReleases?: AssetReleaseReader;
bootstrap?: () => BootstrapResponse | Promise<BootstrapResponse>;
browserGate?: boolean;
browserSupportRelease?: BrowserSupportRelease;
browserSupportSecret?: Buffer;
credits?: CreditService;
eventHub?: EventHub;
generations?: GenerationSubmissionService;
latestExports?: LatestExportService;
models?: ModelConfigurationService;
networkBoundary?: NetworkBoundaryOptions;
publicAssets?: PublicAssetResolver;
recentAssets?: RecentAssetService;
projects?: ProjectService;
storage?: ManagedStorage;
previewAssetAuthorizer?: (input: {
releaseVersion: string;
resourceId: string;
userId: string;
}) => boolean | Promise<boolean>;
previewGrants?: AssetPreviewGrantService;
privateAssetAdminAuthorizer?: (input: {
adminUserId: string;
ownerId: string;
releaseVersion: string;
resourceId: string;
}) => boolean | Promise<boolean>;
registration?: RegistrationService;
stickers?: StickerReleaseService;
}
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 projectFailure(reply: FastifyReply, correlationId: string, error: unknown) {
if (!(error instanceof ProjectError)) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId }));
}
const mapping = {
generation_state_invalid: 400,
project_active_limit: 409,
project_history_limit: 409,
project_name_invalid: 400,
project_not_found: 404,
project_ratio_fixed: 409,
project_retry_not_allowed: 409,
project_state_conflict: 412,
project_state_idempotency_conflict: 409,
project_state_invalid: 400,
} as const;
return reply.code(mapping[error.code]).send(null);
}
function latestExportResponse(item: ReturnType<LatestExportService["getLatest"]> | Awaited<ReturnType<LatestExportService["saveLatest"]>>) {
return {
byte_size: item.byteSize,
created_at: item.createdAt,
download_url: item.downloadUrl,
export_id: item.exportId,
format: item.format,
pixel_height: item.pixelHeight,
pixel_width: item.pixelWidth,
sha256: item.sha256,
state_version: item.stateVersion,
status: "saved" as const,
};
}
function latestExportFailure(reply: FastifyReply, correlationId: string, error: unknown) {
if (error instanceof LatestExportError) {
return reply.code(error.code === "not_found" ? 404 : error.code === "conflict" ? 409 : 400).send(null);
}
if (error && typeof error === "object" && "code" in error && error.code === "STORAGE_CAPACITY_EXCEEDED") {
const details = "details" in error && error.details && typeof error.details === "object"
? error.details as { capacity_status?: "normal" | "warning" | "critical" | "full" | "unavailable"; remaining_bytes?: number }
: undefined;
return reply.code(507).send(createErrorEnvelope({
code: "STORAGE_CAPACITY_EXCEEDED",
correlationId,
details: { capacity_status: details?.capacity_status ?? "full", remaining_bytes: details?.remaining_bytes ?? 0 },
}));
}
if (error && typeof error === "object" && "code" in error && error.code === "storage_unavailable") {
return reply.code(507).send(createErrorEnvelope({
code: "STORAGE_CAPACITY_EXCEEDED",
correlationId,
details: { capacity_status: "unavailable", remaining_bytes: 0 },
}));
}
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId }));
}
function creditFailure(reply: FastifyReply, correlationId: string, error: unknown) {
if (!(error instanceof CreditError)) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId }));
}
if (error.code === "credit_operation_conflict") {
return reply.code(409).send(createErrorEnvelope({ code: "IDEMPOTENCY_KEY_CONFLICT", correlationId }));
}
const status = error.code === "credit_request_invalid"
? 400
: error.code === "credit_insufficient" || error.code === "credit_invariant_failed"
? 409
: 404;
return reply.code(status).send(null);
}
function modelConfigurationFailure(reply: FastifyReply, correlationId: string, error: unknown) {
if (!(error instanceof ModelConfigurationError)) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId }));
}
const status = error.code === "MODEL_CONFIG_VERSION_CONFLICT" ? 412
: error.code === "MODEL_RECOMMENDATION_PRIORITY_INVALID" ? 400
: error.code === "IDEMPOTENCY_KEY_CONFLICT" ? 409 : 409;
const details = {
...(error.code === "MODEL_CONFIG_VERSION_CONFLICT" && typeof error.details.latest_version === "number"
? { latest_version: error.details.latest_version } : {}),
...(Array.isArray(error.details.conflict_model_ids) ? { conflict_model_ids: error.details.conflict_model_ids as string[] } : {}),
...(Array.isArray(error.details.field_errors) ? { field_errors: error.details.field_errors as Array<{ field: string; message_key: string }> } : {}),
};
return reply.code(status).send(createErrorEnvelope({ code: error.code, correlationId, details }));
}
function stickerReleaseFailure(reply: FastifyReply, correlationId: string, error: unknown) {
if (error instanceof StickerReleaseError) return reply.code(error.httpStatus).send(null);
return latestExportFailure(reply, correlationId, error);
}
function assetCleanupFailure(reply: FastifyReply, correlationId: string, error: unknown) {
const code = error instanceof Error ? error.message : "";
if (code === "ASSET_HISTORY_REFERENCE_CONFLICT" || code === "ASSET_CLEANUP_CANDIDATE_STALE") {
return reply.code(409).send(createErrorEnvelope({ code, correlationId }));
}
if (code === "IDEMPOTENCY_KEY_CONFLICT") {
return reply.code(409).send(createErrorEnvelope({ code: "IDEMPOTENCY_KEY_CONFLICT", correlationId }));
}
if (code === "cleanup_candidates_invalid") return reply.code(400).send(null);
if (code === "cleanup_uncommitted") return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId }));
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId }));
}
function generationTaskResponse(task: GenerationTaskView) {
return {
confirmed_credit_cost: task.confirmedCreditCost,
created_at: task.createdAt,
error_category: task.errorCategory,
generation_id: task.generationId,
model_config_version: task.modelConfigVersion,
model_id: task.modelId,
project_id: task.projectId,
prompt: task.prompt,
ratio: task.ratio,
reference_asset_ids: task.referenceAssetIds,
reference_count: task.referenceCount,
reserved_credits: task.reservedCredits,
status: task.status,
updated_at: task.updatedAt,
};
}
function generationFailure(reply: FastifyReply, correlationId: string, error: unknown) {
if (error instanceof GenerationSubmissionError) {
if (error.code === "model_config_stale") {
return reply.code(412).send(createErrorEnvelope({
code: "MODEL_CONFIG_VERSION_CONFLICT",
correlationId,
details: { latest_version: error.latest?.configVersion ?? 0 },
}));
}
if (error.code === "generation_idempotency_conflict") {
return reply.code(409).send(createErrorEnvelope({ code: "IDEMPOTENCY_KEY_CONFLICT", correlationId }));
}
if (error.code === "generation_blocked") {
return reply.code(503).send(createErrorEnvelope({
code: "AUTH_SERVICE_UNAVAILABLE",
correlationId,
...(error.errorCategory ? { errorCategory: error.errorCategory } : {}),
}));
}
if (error.code === "generation_storage_unavailable") {
return reply.code(507).send(createErrorEnvelope({
code: "STORAGE_CAPACITY_EXCEEDED",
correlationId,
details: {
capacity_status: error.storage?.capacityStatus ?? "unavailable",
remaining_bytes: error.storage?.remainingBytes ?? 0,
},
}));
}
return reply.code(error.code === "generation_not_found" ? 404 : 400).send(null);
}
if (error && typeof error === "object" && "code" in error) {
if (typeof error.code === "string" && error.code.startsWith("FST_")) {
return reply.code(400).send(null);
}
if (error.code === "STORAGE_CAPACITY_EXCEEDED") {
const details = "details" in error && error.details && typeof error.details === "object"
? error.details as { capacity_status?: "normal" | "warning" | "critical" | "full" | "unavailable"; remaining_bytes?: number }
: {};
return reply.code(507).send(createErrorEnvelope({
code: "STORAGE_CAPACITY_EXCEEDED",
correlationId,
details: {
capacity_status: details.capacity_status ?? "full",
remaining_bytes: details.remaining_bytes ?? 0,
},
}));
}
if (error.code === "storage_unavailable") {
return reply.code(507).send(createErrorEnvelope({
code: "STORAGE_CAPACITY_EXCEEDED",
correlationId,
details: { capacity_status: "unavailable", remaining_bytes: 0 },
}));
}
}
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId }));
}
const generationFieldNames = new Set([
"client_submission_id",
"confirmed_credit_cost",
"creation_mode",
"existing_reference_asset_ids",
"model_config_version",
"model_id",
"project_id",
"prompt",
"ratio",
"reference_manifest",
]);
interface ReferenceManifestEntry {
fileName: string;
mimeType: NewGenerationReference["mimeType"];
projectedBytes: number;
}
function positiveIntegerField(value: string | undefined) {
if (!value || !/^[1-9][0-9]*$/.test(value)) throw new GenerationSubmissionError("generation_request_invalid");
const parsed = Number(value);
if (!Number.isSafeInteger(parsed)) throw new GenerationSubmissionError("generation_request_invalid");
return parsed;
}
function jsonStringArray(value: string | undefined) {
if (value === undefined) return [];
let parsed: unknown;
try {
parsed = JSON.parse(value);
} catch {
throw new GenerationSubmissionError("generation_request_invalid");
}
if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string")) {
throw new GenerationSubmissionError("generation_request_invalid");
}
return parsed;
}
function referenceManifest(value: string | undefined): ReferenceManifestEntry[] {
if (value === undefined) return [];
let parsed: unknown;
try {
parsed = JSON.parse(value);
} catch {
throw new GenerationSubmissionError("generation_request_invalid");
}
if (!Array.isArray(parsed)) throw new GenerationSubmissionError("generation_request_invalid");
return parsed.map((entry) => {
if (!entry || typeof entry !== "object") throw new GenerationSubmissionError("generation_request_invalid");
const item = entry as Record<string, unknown>;
const keys = Object.keys(item).toSorted();
if (keys.join(",") !== "file_name,mime_type,size" || typeof item.file_name !== "string"
|| !["image/jpeg", "image/png", "image/webp"].includes(String(item.mime_type))
|| !Number.isSafeInteger(item.size) || Number(item.size) <= 0) {
throw new GenerationSubmissionError("generation_request_invalid");
}
return {
fileName: item.file_name,
mimeType: item.mime_type as NewGenerationReference["mimeType"],
projectedBytes: Number(item.size),
};
});
}
function generationFields(
values: Map<string, string>,
input: { idempotencyKey: string; userId: string },
): { fields: GenerationSubmissionFields; manifest: ReferenceManifestEntry[] } {
for (const name of values.keys()) {
if (!generationFieldNames.has(name)) throw new GenerationSubmissionError("generation_request_invalid");
}
const creationMode = values.get("creation_mode");
if (creationMode !== "new_project" && creationMode !== "existing_project") {
throw new GenerationSubmissionError("generation_request_invalid");
}
const fields: GenerationSubmissionFields = {
clientSubmissionId: values.get("client_submission_id") ?? "",
confirmedCreditCost: positiveIntegerField(values.get("confirmed_credit_cost")),
existingReferenceAssetIds: jsonStringArray(values.get("existing_reference_asset_ids")),
idempotencyKey: input.idempotencyKey,
mode: creationMode,
modelConfigVersion: positiveIntegerField(values.get("model_config_version")),
modelId: values.get("model_id") ?? "",
...(values.has("project_id") ? { projectId: values.get("project_id")! } : {}),
prompt: values.get("prompt") ?? "",
ratio: values.get("ratio") as GenerationSubmissionFields["ratio"],
userId: input.userId,
};
return { fields, manifest: referenceManifest(values.get("reference_manifest")) };
}
type ProjectSummaryView = ReturnType<ProjectService["listProjects"]>[number];
type ProjectDetailView = ReturnType<ProjectService["getProject"]>;
function projectSummaryResponse(project: ProjectSummaryView) {
return {
current_image_id: project.currentImageId,
deleted_at: project.deletedAt,
name: project.name,
project_id: project.projectId,
purge_at: project.purgeAt,
ratio: project.ratio,
state_version: project.stateVersion,
status: project.status,
successful_image_count: project.successfulImageCount,
updated_at: project.updatedAt,
};
}
function projectDetailResponse(project: ProjectDetailView) {
return {
...projectSummaryResponse(project),
canvas_state: project.canvasState,
created_at: project.createdAt,
draft_prompt: project.draftPrompt,
generations: project.generations.map((generation) => ({
created_at: generation.createdAt,
error_category: generation.errorCategory,
generation_id: generation.generationId,
prompt: generation.prompt,
ratio: generation.ratio,
status: generation.status,
updated_at: generation.updatedAt,
})),
images: project.images.map((image) => ({
created_at: image.createdAt,
generation_id: image.generationId,
image_id: image.imageId,
})),
latest_exports: project.latestExports.map((item) => ({
byte_size: item.byteSize,
created_at: item.createdAt,
download_url: item.downloadUrl,
export_id: item.exportId,
format: item.format,
pixel_height: item.pixelHeight,
pixel_width: item.pixelWidth,
sha256: item.sha256,
state_version: item.stateVersion,
})),
pixel_height: project.pixelHeight,
pixel_width: project.pixelWidth,
save_status: project.saveStatus,
};
}
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}`,
},
});
await app.register(multipart, {
limits: {
fieldNameSize: 120,
fieldSize: 16 * 1024,
fields: 20,
fileSize: 100 * 1024 * 1024,
files: 16,
parts: 36,
},
});
for (const schema of [
CorrelationIdSchema,
GenerationErrorCategorySchema,
GenerationTaskStatusSchema,
GenerationTaskResponseSchema,
GenerationCreateResponseSchema,
GenerationParamsSchema,
GenerationCreateHeadersSchema,
GenerationMultipartBodySchema,
StableEngineeringErrorCodeSchema,
ErrorDetailsSchema,
ErrorEnvelopeSchema,
AuthenticatedUserSchema,
AdminAuthenticatedUserSchema,
AdminLoginSendRequestSchema,
AdminLoginCompleteRequestSchema,
AdminLoginCompleteResponseSchema,
AdminSessionResponseSchema,
CreditSummarySchema,
CreditEntryTypeSchema,
CreditEntryStatusSchema,
CreditReferenceTypeSchema,
CreditBalanceResponseSchema,
CreditLedgerEntrySchema,
CreditLedgerQuerySchema,
CreditLedgerResponseSchema,
AdminCreditParamsSchema,
CreditAdjustmentHeadersSchema,
CreditAdjustmentRequestSchema,
CreditAdjustmentResponseSchema,
CsrfHeadersSchema,
AccountSettingsResponseSchema,
AccountProfileUpdateRequestSchema,
AccountProfileUpdateResponseSchema,
AccountDeletionSendResponseSchema,
AccountDeletionCompleteRequestSchema,
AccountDeletionResponseSchema,
RegistrationSendRequestSchema,
RegistrationSendResponseSchema,
RegistrationCompleteRequestSchema,
RegistrationCompleteHeadersSchema,
RegistrationCompleteResponseSchema,
LoginSendRequestSchema,
LoginCompleteRequestSchema,
LoginCompleteResponseSchema,
LogoutHeadersSchema,
LogoutResponseSchema,
UserSessionResponseSchema,
BrowserUnsupportedReasonSchema,
BrowserSupportRequestSchema,
BrowserSupportSuccessSchema,
BootstrapResponseSchema,
CanvasBackgroundAdjustmentsSchema,
CanvasElementSchema,
CanvasStateSchema,
StateSseEventSchema,
ModelConfigSseEventSchema,
ModelRuntimeSseEventSchema,
SseEventSchema,
ProjectIdSchema,
ProjectRatioSchema,
ProjectViewStatusSchema,
ProjectSummarySchema,
ProjectListQuerySchema,
ProjectListResponseSchema,
ProjectParamsSchema,
GenerationProjectItemSchema,
ProjectImageItemSchema,
ExportFormatSchema,
LatestExportItemSchema,
LatestExportMultipartBodySchema,
LatestExportParamsSchema,
LatestExportSaveResponseSchema,
ProjectImageParamsSchema,
ProjectDetailResponseSchema,
ProjectEditableStateSchema,
ProjectRenameRequestSchema,
ProjectRenameResponseSchema,
ProjectTrashResponseSchema,
ProjectRestoreResponseSchema,
ProjectPurgeResponseSchema,
ProjectStateSaveHeadersSchema,
ProjectStateSaveResponseSchema,
ProjectStateConflictResponseSchema,
RecentAssetKindSchema,
RecentAssetItemSchema,
RecentAssetQuerySchema,
RecentAssetListResponseSchema,
RecentAssetRecordRequestSchema,
RecentAssetRecordResponseSchema,
ReverseGeocodeRequestSchema,
ReverseGeocodeResponseSchema,
FailedEmptyTrashRequestSchema,
FailedEmptyTrashResponseSchema,
ModelIdSchema,
ModelContractValidationStatusSchema,
ModelRuntimeReasonSchema,
ModelReferenceLimitsSchema,
ModelRuntimeAvailabilitySchema,
ModelConfigSchema,
ModelConfigCandidateSchema,
ModelConfigurationResponseSchema,
ModelParamsSchema,
ModelConfigUpdateRequestSchema,
ModelConfigUpdateHeadersSchema,
]) {
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/static-stickers/current",
{ schema: { hide: true } },
async (_request, reply) => {
if (!options.stickers) return reply.code(503).send();
reply.header("Cache-Control", "no-cache");
return options.stickers.listPublic();
},
);
app.get(
"/api/v1/static-stickers/:resourceVersion",
{ schema: { hide: true } },
async (request, reply) => {
if (!options.stickers) return reply.code(404).send();
const { resourceVersion } = request.params as { resourceVersion: string };
const catalog = options.stickers.listPublic(resourceVersion);
if (!catalog.release_version) return reply.code(404).send();
reply.header("Cache-Control", "public, max-age=31536000, immutable");
return catalog;
},
);
app.get(
"/api/v1/admin/assets/static-stickers",
{ schema: { hide: true } },
async (request, reply) => {
if (!options.registration || !options.stickers) {
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 }));
reply.header("Cache-Control", "private, no-store");
return options.stickers.adminView();
},
);
app.post(
"/api/v1/admin/assets/static-stickers",
{ schema: { hide: true } },
async (request, reply) => {
if (!request.isMultipart()) return reply.code(400).send(null);
if (!options.registration || !options.stickers) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
const token = cookieValue(headerValue(request.headers.cookie), adminSessionCookieName);
const idempotencyKey = headerValue(request.headers["idempotency-key"]);
const csrfToken = headerValue(request.headers["x-csrf-token"]);
if (!token) return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
if (!idempotencyKey || !/^[A-Za-z0-9_-]{32,200}$/.test(idempotencyKey)
|| !csrfToken || !/^[A-Za-z0-9_-]{43,64}$/.test(csrfToken)) return reply.code(400).send(null);
try {
const admin = options.registration.authorizeAdminMutation({ csrfToken, sessionToken: token });
const values = new Map<string, string>();
const allowedFields = new Set(["enabled", "order", "original_byte_size", "original_sha256", "part", "stable_id"]);
let result: Awaited<ReturnType<StickerReleaseService["upload"]>> | undefined;
for await (const part of request.parts({ limits: { fileSize: 20 * 1024 * 1024, files: 1, fields: 8, parts: 9 } })) {
if (part.type === "field") {
if (result || !allowedFields.has(part.fieldname) || values.has(part.fieldname) || typeof part.value !== "string") throw new StickerReleaseError("sticker_upload_invalid");
values.set(part.fieldname, part.value);
continue;
}
if (result || part.fieldname !== "sticker_file" || !part.filename
|| !new Set(["image/png", "image/webp"]).has(part.mimetype)) throw new StickerReleaseError("sticker_upload_invalid");
const stableId = values.get("stable_id");
const partValue = Number(values.get("part"));
const order = Number(values.get("order"));
const enabled = values.get("enabled");
const expectedByteSize = Number(values.get("original_byte_size"));
const expectedSha256 = values.get("original_sha256");
if (!stableId || !expectedSha256 || !new Set(["true", "false"]).has(enabled ?? "")) throw new StickerReleaseError("sticker_upload_invalid");
result = await options.stickers.upload({
actorId: admin.userId,
content: part.file,
enabled: enabled === "true",
expectedByteSize,
expectedMimeType: part.mimetype as "image/png" | "image/webp",
expectedSha256,
fileName: part.filename,
idempotencyKey,
order,
part: partValue,
stableId,
});
if (part.file.truncated) throw new StickerReleaseError("sticker_upload_invalid");
}
if (!result) throw new StickerReleaseError("sticker_upload_invalid");
return reply.code(result.created ? 201 : 200).send(result);
} catch (error) {
return error instanceof RegistrationError
? registrationFailure(reply, request.id, error)
: stickerReleaseFailure(reply, request.id, error);
}
},
);
app.patch(
"/api/v1/admin/assets/static-stickers/:stableId",
{ schema: { hide: true } },
async (request, reply) => {
if (!options.registration || !options.stickers) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
const token = cookieValue(headerValue(request.headers.cookie), adminSessionCookieName);
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 admin = options.registration.authorizeAdminMutation({ csrfToken, sessionToken: token });
const body = request.body as { enabled?: boolean; order?: number; part?: number } | undefined;
if (!body || Object.keys(body).length === 0 || Object.keys(body).some((key) => !new Set(["enabled", "order", "part"]).has(key))) {
throw new StickerReleaseError("sticker_update_invalid");
}
return options.stickers.update({
actorId: admin.userId,
...(typeof body.enabled === "boolean" ? { enabled: body.enabled } : {}),
...(typeof body.order === "number" ? { order: body.order } : {}),
...(typeof body.part === "number" ? { part: body.part } : {}),
stableId: (request.params as { stableId: string }).stableId,
});
} catch (error) {
return error instanceof RegistrationError
? registrationFailure(reply, request.id, error)
: stickerReleaseFailure(reply, request.id, error);
}
},
);
app.get(
"/api/v1/admin/assets/static-stickers/cleanup/candidates",
{ schema: { hide: true } },
async (request, reply) => {
if (!options.registration || !options.storage) {
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 }));
try {
reply.header("Cache-Control", "private, no-store");
return options.storage.listAssetCleanupCandidates();
} catch (error) {
return assetCleanupFailure(reply, request.id, error);
}
},
);
app.post(
"/api/v1/admin/assets/static-stickers/cleanup/intents",
{ schema: { hide: true } },
async (request, reply) => {
if (!options.registration || !options.storage) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
const token = cookieValue(headerValue(request.headers.cookie), adminSessionCookieName);
const csrfToken = headerValue(request.headers["x-csrf-token"]);
const idempotencyKey = headerValue(request.headers["idempotency-key"]);
if (!token || !csrfToken) return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
if (!idempotencyKey || !/^[A-Za-z0-9_-]{32,200}$/.test(idempotencyKey) || !/^[A-Za-z0-9_-]{43,64}$/.test(csrfToken)) return reply.code(400).send(null);
try {
const admin = options.registration.authorizeAdminMutation({ csrfToken, sessionToken: token });
const body = request.body as { file_ids?: string[]; snapshot_version?: string } | undefined;
return reply.code(201).send(options.storage.createAssetCleanupIntent({
actorId: admin.userId,
fileIds: body?.file_ids ?? [],
idempotencyKey,
snapshotVersion: body?.snapshot_version ?? "",
}));
} catch (error) {
return assetCleanupFailure(reply, request.id, error);
}
},
);
app.post(
"/api/v1/admin/assets/static-stickers/cleanup/intents/:requestId/confirm",
{ schema: { hide: true } },
async (request, reply) => {
if (!options.registration || !options.storage) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
const token = cookieValue(headerValue(request.headers.cookie), adminSessionCookieName);
const csrfToken = headerValue(request.headers["x-csrf-token"]);
if (!token || !csrfToken) return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
if (!/^[A-Za-z0-9_-]{43,64}$/.test(csrfToken)) return reply.code(400).send(null);
try {
const admin = options.registration.authorizeAdminMutation({ csrfToken, sessionToken: token });
const body = request.body as { confirmation_token?: string } | undefined;
const requestId = (request.params as { requestId: string }).requestId;
return reply.send(options.storage.confirmAssetCleanupIntent({
actorId: admin.userId,
confirmationToken: body?.confirmation_token ?? "",
requestId,
}));
} catch (error) {
return assetCleanupFailure(reply, request.id, error);
}
},
);
app.get(
"/api/v1/assets/public/:resourceVersion/manifest",
{ schema: { hide: true } },
async (request, reply) => {
const { resourceVersion } = request.params as { resourceVersion: string };
const manifest = options.assetReleases?.project("public_release_asset", resourceVersion);
if (!manifest) return reply.code(404).send();
reply.header("Cache-Control", "public, max-age=31536000, immutable");
reply.header("ETag", `"sha256-${manifest.manifest_sha256}"`);
return manifest;
},
);
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.assetReleases?.read("public_release_asset", resourceVersion, assetId)
?? options.publicAssets?.read(resourceVersion, assetId)
?? options.stickers?.readPublicAsset(
resourceVersion,
assetId,
(request.query as { variant?: string }).variant === "thumbnail" ? "thumbnail" : "original",
)
: 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.get(
"/api/v1/assets/preview/:resourceVersion/manifest",
{ schema: { hide: true } },
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 }));
const { resourceVersion } = request.params as { resourceVersion: string };
if (options.previewGrants) {
const manifest = options.previewGrants.projectManifest({ releaseVersion: resourceVersion, userId: session.userId });
if (!manifest) return reply.code(404).send();
reply.header("Cache-Control", "private, no-store");
reply.header("Vary", "Cookie");
return manifest;
}
const available = options.assetReleases?.project("internal_preview_asset", resourceVersion);
if (!available || !options.previewAssetAuthorizer) return reply.code(404).send();
const authorizedIds: string[] = [];
for (const item of available.items) {
if (await options.previewAssetAuthorizer({
releaseVersion: resourceVersion,
resourceId: item.resource_id,
userId: session.userId,
})) authorizedIds.push(item.resource_id);
}
if (authorizedIds.length === 0) return reply.code(404).send();
const manifest = options.assetReleases?.project("internal_preview_asset", resourceVersion, { resourceIds: authorizedIds });
if (!manifest) return reply.code(404).send();
reply.header("Cache-Control", "private, no-store");
reply.header("Vary", "Cookie");
return manifest;
},
);
app.get(
"/api/v1/assets/preview/:resourceVersion/:assetId",
{ schema: { hide: true } },
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 }));
const { assetId, resourceVersion } = request.params as { assetId: string; resourceVersion: string };
if (options.previewGrants) {
const resource = options.previewGrants.readManifestItem({
manifestItemId: assetId,
releaseVersion: resourceVersion,
userId: session.userId,
});
if (!resource) return reply.code(404).send();
reply.type(resource.mimeType);
reply.header("Cache-Control", "private, no-store");
reply.header("Content-Disposition", "inline");
reply.header("Vary", "Cookie");
return resource.bytes;
}
const authorized = await options.previewAssetAuthorizer?.({ resourceId: assetId, releaseVersion: resourceVersion, userId: session.userId });
const resource = authorized ? options.assetReleases?.read("internal_preview_asset", resourceVersion, assetId) : undefined;
if (!resource) return reply.code(404).send();
reply.type(resource.mimeType);
reply.header("Cache-Control", "private, no-store");
reply.header("Content-Disposition", "inline");
reply.header("Vary", "Cookie");
return resource.bytes;
},
);
app.get(
"/api/v1/private-assets/:resourceVersion/manifest",
{ schema: { hide: true } },
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 }));
const { resourceVersion } = request.params as { resourceVersion: string };
const manifest = options.assetReleases?.project("private_user_asset", resourceVersion, { ownerId: session.userId });
if (!manifest) return reply.code(404).send();
reply.header("Cache-Control", "private, no-store");
reply.header("Vary", "Cookie");
return manifest;
},
);
app.get(
"/api/v1/private-assets/:resourceVersion/:assetId",
{ schema: { hide: true } },
async (request, reply) => {
if (!options.registration) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
const userToken = cookieValue(headerValue(request.headers.cookie), userSessionCookieName);
const userSession = userToken ? options.registration.readUserSession(userToken) : undefined;
const adminToken = cookieValue(headerValue(request.headers.cookie), adminSessionCookieName);
const adminSession = adminToken ? options.registration.readAdminSession(adminToken) : undefined;
if (!userSession && !adminSession) {
return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
}
const { assetId, resourceVersion } = request.params as { assetId: string; resourceVersion: string };
const resource = options.assetReleases?.read("private_user_asset", resourceVersion, assetId);
if (!resource?.ownerId) return reply.code(404).send();
const controlledAdmin = adminSession
? await options.privateAssetAdminAuthorizer?.({
adminUserId: adminSession.user_id,
ownerId: resource.ownerId,
releaseVersion: resource.releaseVersion,
resourceId: resource.resourceId,
})
: false;
if (userSession?.userId !== resource.ownerId && !controlledAdmin) return reply.code(404).send();
reply.type(resource.mimeType);
reply.header("Cache-Control", "private, no-store");
reply.header("Content-Disposition", "inline");
reply.header("Vary", "Cookie");
return resource.bytes;
},
);
app.get(
"/api/v1/assets/recent",
{
attachValidation: true,
schema: {
operationId: "listRecentAssets",
querystring: Type.Ref(RecentAssetQuerySchema),
response: {
200: Type.Ref(RecentAssetListResponseSchema),
400: Type.Null(),
401: Type.Ref(ErrorEnvelopeSchema),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Assets"],
},
},
async (request, reply) => {
if (request.validationError) return reply.code(400).send(null);
if (!options.registration || !options.recentAssets) {
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 }));
const query = request.query as RecentAssetQuery;
return { items: options.recentAssets.list(session.userId, query.asset_kind) };
},
);
app.post(
"/api/v1/assets/recent",
{
attachValidation: true,
schema: {
body: Type.Ref(RecentAssetRecordRequestSchema),
headers: Type.Ref(CsrfHeadersSchema),
operationId: "recordRecentAsset",
response: {
200: Type.Ref(RecentAssetRecordResponseSchema),
400: Type.Null(),
401: Type.Ref(ErrorEnvelopeSchema),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Assets"],
},
},
async (request, reply) => {
if (request.validationError) return reply.code(400).send(null);
if (!options.registration || !options.recentAssets) {
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 owner = options.registration.authorizeUserMutation({ csrfToken, sessionToken: token });
const body = request.body as RecentAssetRecordRequest;
options.recentAssets.recordSuccessfulUse({
assetId: body.asset_id,
assetKind: body.asset_kind,
resourceVersion: body.resource_version,
userId: owner.userId,
});
return { status: "recorded" as const };
} catch (error) {
return registrationFailure(reply, request.id, error);
}
},
);
app.post(
"/api/v1/location/reverse-geocode",
{
attachValidation: true,
schema: {
body: Type.Ref(ReverseGeocodeRequestSchema),
headers: Type.Ref(CsrfHeadersSchema),
operationId: "reverseGeocodeLocation",
response: {
200: Type.Ref(ReverseGeocodeResponseSchema),
400: Type.Null(),
401: Type.Ref(ErrorEnvelopeSchema),
503: Type.Null(),
},
tags: ["Location"],
},
},
async (request, reply) => {
if (request.validationError) return reply.code(400).send(null);
if (!options.registration || !options.amap) return reply.code(503).send(null);
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.authorizeUserMutation({ csrfToken, sessionToken: token });
const result = await options.amap.reverseGeocode(request.body as ReverseGeocodeRequest);
return { formatted_value: result.formattedValue, service_mode: result.serviceMode, status: "resolved" as const };
} catch (error) {
if (error instanceof RegistrationError) return registrationFailure(reply, request.id, error);
return reply.code(503).send(null);
}
},
);
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.get(
"/api/v1/me/credits",
{
schema: {
operationId: "getMyCredits",
response: {
200: Type.Ref(CreditBalanceResponseSchema),
401: Type.Ref(ErrorEnvelopeSchema),
404: Type.Null(),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Credits"],
},
},
async (request, reply) => {
if (!options.registration || !options.credits) {
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 }));
try {
const account = options.credits.readAccount(session.userId);
return {
available_balance: account.availableBalance,
reserved_balance: account.reservedBalance,
updated_at: account.updatedAt,
};
} catch (error) {
return creditFailure(reply, request.id, error);
}
},
);
app.get(
"/api/v1/me/credit-ledger",
{
attachValidation: true,
schema: {
operationId: "getMyCreditLedger",
querystring: Type.Ref(CreditLedgerQuerySchema),
response: {
200: Type.Ref(CreditLedgerResponseSchema),
400: Type.Null(),
401: Type.Ref(ErrorEnvelopeSchema),
404: Type.Null(),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Credits"],
},
},
async (request, reply) => {
if (request.validationError) return reply.code(400).send(null);
if (!options.registration || !options.credits) {
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 }));
try {
const query = request.query as CreditLedgerQuery;
const ledger = options.credits.listLedger({
...(query.cursor ? { cursor: query.cursor } : {}),
...(query.event_type ? { eventType: query.event_type } : {}),
...(query.from ? { from: query.from } : {}),
...(query.limit ? { limit: query.limit } : {}),
...(query.to ? { to: query.to } : {}),
userId: session.userId,
});
return {
credits: {
available_balance: ledger.account.availableBalance,
reserved_balance: ledger.account.reservedBalance,
},
entries: ledger.entries.map((entry) => ({
amount: entry.amount,
available_after: entry.availableAfter,
available_before: entry.availableBefore,
created_at: entry.createdAt,
entry_id: entry.entryId,
entry_type: entry.entryType,
model_id: entry.modelId,
reason: entry.reason,
reference_id: entry.referenceId,
reference_type: entry.referenceType,
reserved_after: entry.reservedAfter,
reserved_before: entry.reservedBefore,
status: entry.status,
})),
next_cursor: ledger.nextCursor,
updated_at: ledger.account.updatedAt,
};
} catch (error) {
return creditFailure(reply, request.id, error);
}
},
);
app.get(
"/api/v1/admin/users/:userId/credits",
{
attachValidation: true,
schema: {
operationId: "getAdminUserCredits",
params: Type.Ref(AdminCreditParamsSchema),
response: {
200: Type.Ref(CreditBalanceResponseSchema),
400: Type.Null(),
401: Type.Ref(ErrorEnvelopeSchema),
404: Type.Null(),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Admin Credits"],
},
},
async (request, reply) => {
if (request.validationError) return reply.code(400).send(null);
if (!options.registration || !options.credits) {
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 }));
try {
const account = options.credits.readAccount((request.params as AdminCreditParams).userId);
return {
available_balance: account.availableBalance,
reserved_balance: account.reservedBalance,
updated_at: account.updatedAt,
};
} catch (error) {
return creditFailure(reply, request.id, error);
}
},
);
app.post(
"/api/v1/admin/users/:userId/credit-adjustments",
{
attachValidation: true,
schema: {
body: Type.Ref(CreditAdjustmentRequestSchema),
headers: Type.Ref(CreditAdjustmentHeadersSchema),
operationId: "adjustAdminUserCredits",
params: Type.Ref(AdminCreditParamsSchema),
response: {
200: Type.Ref(CreditAdjustmentResponseSchema),
400: Type.Null(),
401: Type.Ref(ErrorEnvelopeSchema),
403: Type.Ref(ErrorEnvelopeSchema),
404: Type.Null(),
409: Type.Union([Type.Ref(ErrorEnvelopeSchema), Type.Null()]),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Admin Credits"],
},
},
async (request, reply) => {
if (request.validationError) return reply.code(400).send(null);
if (!options.registration || !options.credits) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
const token = cookieValue(headerValue(request.headers.cookie), adminSessionCookieName);
const headers = request.headers as CreditAdjustmentHeaders;
if (!token) return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
try {
const admin = options.registration.authorizeAdminMutation({
csrfToken: headers["x-csrf-token"],
sessionToken: token,
});
const body = request.body as CreditAdjustmentRequest;
const adjusted = options.credits.adjustAvailable({
adjustmentId: body.adjustment_id,
adminId: admin.userId,
amount: body.amount,
idempotencyKey: headers["idempotency-key"],
reason: body.reason,
userId: (request.params as AdminCreditParams).userId,
});
return {
adjustment_id: adjusted.adjustmentId,
available_balance: adjusted.availableBalance,
reserved_balance: adjusted.reservedBalance,
status: adjusted.status,
};
} catch (error) {
return error instanceof RegistrationError
? registrationFailure(reply, request.id, error)
: creditFailure(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.get(
"/api/v1/generations/current",
{
schema: {
operationId: "getCurrentGeneration",
response: {
200: Type.Ref(GenerationTaskResponseSchema),
401: Type.Ref(ErrorEnvelopeSchema),
404: Type.Null(),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Generations"],
},
},
async (request, reply) => {
if (!options.registration || !options.generations) {
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 }));
const task = options.generations.readCurrentTask(session.userId);
return task ? generationTaskResponse(task) : reply.code(404).send(null);
},
);
app.get(
"/api/v1/generations/:generationId",
{
attachValidation: true,
schema: {
operationId: "getGeneration",
params: Type.Ref(GenerationParamsSchema),
response: {
200: Type.Ref(GenerationTaskResponseSchema),
400: Type.Null(),
401: Type.Ref(ErrorEnvelopeSchema),
404: Type.Null(),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Generations"],
},
},
async (request, reply) => {
if (request.validationError) return reply.code(400).send(null);
if (!options.registration || !options.generations) {
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 }));
try {
return generationTaskResponse(options.generations.readTask(session.userId, (request.params as GenerationParams).generationId));
} catch (error) {
return generationFailure(reply, request.id, error);
}
},
);
app.post(
"/api/v1/generations",
{
attachValidation: true,
schema: {
consumes: ["multipart/form-data"],
body: Type.Optional(Type.Ref(GenerationMultipartBodySchema)),
headers: Type.Ref(GenerationCreateHeadersSchema),
operationId: "createGeneration",
response: {
200: Type.Ref(GenerationCreateResponseSchema),
201: Type.Ref(GenerationCreateResponseSchema),
400: Type.Null(),
401: Type.Ref(ErrorEnvelopeSchema),
403: Type.Ref(ErrorEnvelopeSchema),
409: Type.Ref(ErrorEnvelopeSchema),
412: Type.Ref(ErrorEnvelopeSchema),
503: Type.Ref(ErrorEnvelopeSchema),
507: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Generations"],
},
validatorCompiler: () => (data) => ({ value: data }),
},
async (request, reply) => {
if (request.validationError || !request.isMultipart()) return reply.code(400).send(null);
if (!options.registration || !options.generations) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
const token = cookieValue(headerValue(request.headers.cookie), userSessionCookieName);
const idempotencyKey = headerValue(request.headers["idempotency-key"]);
const csrfToken = headerValue(request.headers["x-csrf-token"]);
if (!idempotencyKey || !/^[A-Za-z0-9_-]{32,200}$/.test(idempotencyKey)
|| !csrfToken || !/^[A-Za-z0-9_-]{43,64}$/.test(csrfToken)) return reply.code(400).send(null);
const headers = { "idempotency-key": idempotencyKey, "x-csrf-token": csrfToken } satisfies GenerationCreateHeaders;
if (!token) return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
let upload: GenerationUploadSession | undefined;
try {
const owner = options.registration.authorizeUserMutation({ csrfToken: headers["x-csrf-token"], sessionToken: token });
const current = options.generations.readCurrentTask(owner.userId);
if (current) return { created: false, task: generationTaskResponse(current) };
const values = new Map<string, string>();
let manifest: ReferenceManifestEntry[] | undefined;
let fileIndex = 0;
for await (const part of request.parts()) {
if (part.type === "field") {
if (upload || values.has(part.fieldname) || typeof part.value !== "string") {
throw new GenerationSubmissionError("generation_request_invalid");
}
values.set(part.fieldname, part.value);
continue;
}
if (part.fieldname !== "reference_files" || !part.filename) {
throw new GenerationSubmissionError("reference_invalid", { errorCategory: "reference_invalid" });
}
if (!upload) {
const parsed = generationFields(values, { idempotencyKey: headers["idempotency-key"], userId: owner.userId });
manifest = parsed.manifest;
upload = options.generations.beginUpload(parsed.fields);
}
const expected = manifest?.[fileIndex];
if (!expected || expected.fileName !== part.filename || expected.mimeType !== part.mimetype) {
throw new GenerationSubmissionError("reference_invalid", { errorCategory: "reference_invalid" });
}
await upload.stageReference({
content: part.file,
fileName: part.filename,
mimeType: expected.mimeType,
projectedBytes: expected.projectedBytes,
});
if (part.file.truncated) throw new GenerationSubmissionError("reference_invalid", { errorCategory: "reference_invalid" });
fileIndex += 1;
}
if (!upload) {
const parsed = generationFields(values, { idempotencyKey: headers["idempotency-key"], userId: owner.userId });
manifest = parsed.manifest;
upload = options.generations.beginUpload(parsed.fields);
}
if (fileIndex !== (manifest?.length ?? 0)) {
throw new GenerationSubmissionError("reference_invalid", { errorCategory: "reference_invalid" });
}
const result = await upload.commit();
return reply.code(result.created ? 201 : 200).send({ created: result.created, task: generationTaskResponse(result.task) });
} catch (error) {
upload?.abort();
return error instanceof RegistrationError
? registrationFailure(reply, request.id, error)
: error instanceof CreditError
? creditFailure(reply, request.id, error)
: generationFailure(reply, request.id, error);
}
},
);
app.get(
"/api/v1/projects",
{
attachValidation: true,
schema: {
operationId: "listProjects",
querystring: Type.Ref(ProjectListQuerySchema),
response: {
200: Type.Ref(ProjectListResponseSchema),
400: Type.Null(),
401: Type.Ref(ErrorEnvelopeSchema),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Projects"],
},
},
async (request, reply) => {
if (request.validationError) {
return reply.code(400).send(null);
}
if (!options.registration || !options.projects) {
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 }));
const query = request.query as ProjectListQuery;
const status = query.status ?? "active";
return {
active_count: options.projects.activeProjectCount(session.userId),
active_limit: 20 as const,
projects: options.projects.listProjects(session.userId, status).slice(0, 20).map(projectSummaryResponse),
};
},
);
app.get(
"/api/v1/projects/:projectId",
{
attachValidation: true,
schema: {
operationId: "getProject",
params: Type.Ref(ProjectParamsSchema),
response: {
200: Type.Ref(ProjectDetailResponseSchema),
400: Type.Null(),
401: Type.Ref(ErrorEnvelopeSchema),
404: Type.Null(),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Projects"],
},
},
async (request, reply) => {
if (request.validationError) {
return reply.code(400).send(null);
}
if (!options.registration || !options.projects) {
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 }));
try {
return projectDetailResponse(options.projects.getProject(session.userId, (request.params as ProjectParams).projectId));
} catch (error) {
return projectFailure(reply, request.id, error);
}
},
);
app.put(
"/api/v1/projects/:projectId/latest-exports/:format",
{
attachValidation: true,
schema: {
body: Type.Optional(Type.Ref(LatestExportMultipartBodySchema)),
consumes: ["multipart/form-data"],
headers: Type.Ref(CsrfHeadersSchema),
operationId: "saveLatestExport",
params: Type.Ref(LatestExportParamsSchema),
response: {
200: Type.Ref(LatestExportSaveResponseSchema),
400: Type.Null(),
401: Type.Ref(ErrorEnvelopeSchema),
403: Type.Ref(ErrorEnvelopeSchema),
404: Type.Null(),
409: Type.Null(),
503: Type.Ref(ErrorEnvelopeSchema),
507: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Projects"],
},
validatorCompiler: () => (data) => ({ value: data }),
},
async (request, reply) => {
if (request.validationError || !request.isMultipart()) return reply.code(400).send(null);
if (!options.registration || !options.latestExports) {
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 owner = options.registration.authorizeUserMutation({ csrfToken, sessionToken: token });
const params = request.params as LatestExportParams;
const fields = new Map<string, string>();
let saved: Awaited<ReturnType<LatestExportService["saveLatest"]>> | undefined;
for await (const part of request.parts()) {
if (part.type === "field") {
if (saved || fields.has(part.fieldname) || typeof part.value !== "string") throw new LatestExportError("invalid");
fields.set(part.fieldname, part.value);
continue;
}
if (saved || part.fieldname !== "export_file" || !part.filename) throw new LatestExportError("invalid");
const format = fields.get("format");
const expectedMime = params.format === "png" ? "image/png" : "image/jpeg";
if (format !== params.format || part.mimetype !== expectedMime) throw new LatestExportError("invalid");
saved = await options.latestExports.saveLatest({
byteSize: Number(fields.get("byte_size")),
content: part.file,
exportId: fields.get("export_id") ?? "",
format: params.format,
ownerId: owner.userId,
pixelHeight: Number(fields.get("pixel_height")),
pixelWidth: Number(fields.get("pixel_width")),
projectId: params.projectId,
sha256: fields.get("sha256") ?? "",
stateVersion: Number(fields.get("state_version")),
});
if (part.file.truncated) throw new LatestExportError("invalid");
}
if (!saved) throw new LatestExportError("invalid");
return latestExportResponse(saved);
} catch (error) {
return error instanceof RegistrationError
? registrationFailure(reply, request.id, error)
: latestExportFailure(reply, request.id, error);
}
},
);
app.get(
"/api/v1/projects/:projectId/latest-exports/:format",
{
attachValidation: true,
schema: {
operationId: "downloadLatestExport",
params: Type.Ref(LatestExportParamsSchema),
produces: ["application/octet-stream"],
response: { 200: Type.String({ format: "binary" }), 400: Type.Null(), 401: Type.Ref(ErrorEnvelopeSchema), 404: Type.Null(), 503: Type.Ref(ErrorEnvelopeSchema) },
tags: ["Projects"],
},
},
async (request, reply) => {
if (request.validationError) return reply.code(400).send(null);
if (!options.registration || !options.latestExports) 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 }));
try {
const params = request.params as LatestExportParams;
const item = options.latestExports.getLatest(session.userId, params.projectId, params.format);
reply.header("Cache-Control", "private, no-store");
reply.header("Content-Disposition", `attachment; filename="dada-latest.${params.format === "jpg" ? "jpg" : "png"}"`);
reply.type(item.mimeType);
return reply.send(createReadStream(item.path));
} catch (error) {
return latestExportFailure(reply, request.id, error);
}
},
);
app.get(
"/api/v1/private-assets/projects/:projectId/images/:imageId",
{
attachValidation: true,
schema: {
operationId: "downloadOriginalGeneration",
params: Type.Ref(ProjectImageParamsSchema),
produces: ["application/octet-stream"],
response: { 200: Type.String({ format: "binary" }), 400: Type.Null(), 401: Type.Ref(ErrorEnvelopeSchema), 404: Type.Null(), 503: Type.Ref(ErrorEnvelopeSchema) },
tags: ["Projects"],
},
},
async (request, reply) => {
if (request.validationError) return reply.code(400).send(null);
if (!options.registration || !options.latestExports) 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 }));
try {
const params = request.params as ProjectImageParams;
const item = options.latestExports.getOriginal(session.userId, params.projectId, params.imageId);
const extension = item.mime_type === "image/jpeg" ? "jpg" : item.mime_type === "image/webp" ? "webp" : "png";
reply.header("Cache-Control", "private, no-store");
reply.header("Content-Disposition", `attachment; filename="dada-original.${extension}"`);
reply.type(item.mime_type);
return reply.send(createReadStream(item.path));
} catch (error) {
return latestExportFailure(reply, request.id, error);
}
},
);
app.put(
"/api/v1/projects/:projectId/state",
{
attachValidation: true,
schema: {
body: Type.Ref(ProjectEditableStateSchema),
headers: Type.Ref(ProjectStateSaveHeadersSchema),
operationId: "saveProjectState",
params: Type.Ref(ProjectParamsSchema),
response: {
200: Type.Ref(ProjectStateSaveResponseSchema),
400: Type.Null(),
401: Type.Ref(ErrorEnvelopeSchema),
403: Type.Ref(ErrorEnvelopeSchema),
404: Type.Null(),
409: Type.Ref(ErrorEnvelopeSchema),
412: Type.Ref(ProjectStateConflictResponseSchema),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Projects"],
},
},
async (request, reply) => {
if (request.validationError) return reply.code(400).send(null);
if (!options.registration || !options.projects) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
const token = cookieValue(headerValue(request.headers.cookie), userSessionCookieName);
const headers = request.headers as ProjectStateSaveHeaders;
if (!token) return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
try {
const owner = options.registration.authorizeUserMutation({ csrfToken: headers["x-csrf-token"], sessionToken: token });
const saved = options.projects.saveProjectState({
expectedStateVersion: Number(headers["if-match"]),
idempotencyKey: headers["idempotency-key"],
ownerId: owner.userId,
projectId: (request.params as ProjectParams).projectId,
state: request.body as ProjectEditableState,
});
return { save_status: "saved" as const, state_version: saved.stateVersion };
} catch (error) {
if (error instanceof RegistrationError) return registrationFailure(reply, request.id, error);
if (error instanceof ProjectError && error.code === "project_state_conflict") {
return reply.code(412).send({
latest_state_version: error.latestStateVersion ?? 1,
save_status: "conflicted" as const,
});
}
if (error instanceof ProjectError && error.code === "project_state_idempotency_conflict") {
return reply.code(409).send(createErrorEnvelope({ code: "IDEMPOTENCY_KEY_CONFLICT", correlationId: request.id }));
}
return projectFailure(reply, request.id, error);
}
},
);
app.patch(
"/api/v1/projects/:projectId",
{
attachValidation: true,
schema: {
body: Type.Ref(ProjectRenameRequestSchema),
headers: Type.Ref(CsrfHeadersSchema),
operationId: "renameProject",
params: Type.Ref(ProjectParamsSchema),
response: {
200: Type.Ref(ProjectRenameResponseSchema),
400: Type.Null(),
401: Type.Ref(ErrorEnvelopeSchema),
403: Type.Ref(ErrorEnvelopeSchema),
404: Type.Null(),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Projects"],
},
},
async (request, reply) => {
if (request.validationError) {
return reply.code(400).send(null);
}
if (!options.registration || !options.projects) {
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 owner = options.registration.authorizeUserMutation({ csrfToken, sessionToken: token });
const renamed = options.projects.renameProject(
owner.userId,
(request.params as ProjectParams).projectId,
(request.body as ProjectRenameRequest).name,
);
return { name: renamed.name, state_version: renamed.stateVersion, status: "renamed" as const };
} catch (error) {
return error instanceof RegistrationError
? registrationFailure(reply, request.id, error)
: projectFailure(reply, request.id, error);
}
},
);
app.post(
"/api/v1/projects/failed-empty/trash",
{
attachValidation: true,
schema: {
body: Type.Ref(FailedEmptyTrashRequestSchema),
headers: Type.Ref(CsrfHeadersSchema),
operationId: "trashFailedEmptyProjects",
response: {
200: Type.Ref(FailedEmptyTrashResponseSchema),
400: Type.Null(),
401: Type.Ref(ErrorEnvelopeSchema),
403: Type.Ref(ErrorEnvelopeSchema),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Projects"],
},
},
async (request, reply) => {
if (request.validationError) {
return reply.code(400).send(null);
}
if (!options.registration || !options.projects) {
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 owner = options.registration.authorizeUserMutation({ csrfToken, sessionToken: token });
const result = options.projects.trashFailedEmpty(owner.userId, (request.body as FailedEmptyTrashRequest).project_ids);
return { ignored_project_ids: result.ignoredProjectIds, trashed_project_ids: result.trashedProjectIds };
} catch (error) {
return error instanceof RegistrationError
? registrationFailure(reply, request.id, error)
: projectFailure(reply, request.id, error);
}
},
);
const projectLifecycle = (
action: "trash" | "restore" | "purge",
operationId: string,
responseSchema: typeof ProjectTrashResponseSchema | typeof ProjectRestoreResponseSchema | typeof ProjectPurgeResponseSchema,
) => {
app.post(
`/api/v1/projects/:projectId/${action}`,
{
attachValidation: true,
schema: {
headers: Type.Ref(CsrfHeadersSchema),
operationId,
params: Type.Ref(ProjectParamsSchema),
response: {
200: Type.Ref(responseSchema),
400: Type.Null(),
401: Type.Ref(ErrorEnvelopeSchema),
403: Type.Ref(ErrorEnvelopeSchema),
404: Type.Null(),
409: Type.Null(),
503: Type.Ref(ErrorEnvelopeSchema),
},
tags: ["Projects"],
},
},
async (request, reply) => {
if (request.validationError) return reply.code(400).send(null);
if (!options.registration || !options.projects) {
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 owner = options.registration.authorizeUserMutation({ csrfToken, sessionToken: token });
const projectId = (request.params as ProjectParams).projectId;
if (action === "trash") {
const project = options.projects.trashProject(owner.userId, projectId);
return { deleted_at: project.deletedAt, project_id: projectId, purge_at: project.purgeAt, status: "trashed" as const };
}
if (action === "restore") {
options.projects.restoreProject(owner.userId, projectId);
return { deleted_at: null, project_id: projectId, purge_at: null, status: "active" as const };
}
options.projects.purgeProject(owner.userId, projectId);
return { project_id: projectId, status: "purged" as const };
} catch (error) {
return error instanceof RegistrationError
? registrationFailure(reply, request.id, error)
: projectFailure(reply, request.id, error);
}
},
);
};
projectLifecycle("trash", "trashProject", ProjectTrashResponseSchema);
projectLifecycle("restore", "restoreProject", ProjectRestoreResponseSchema);
projectLifecycle("purge", "purgeProject", ProjectPurgeResponseSchema);
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/models",
{
schema: {
operationId: "getModels",
response: { 200: ModelConfigurationResponseSchema, 503: ErrorEnvelopeSchema, 426: ErrorEnvelopeSchema },
tags: ["Models"],
},
},
async (request, reply) => {
if (!options.models) return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
try {
return options.models.read();
} catch {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
},
);
app.get(
"/api/v1/models/:model_id",
{
schema: {
operationId: "getModel",
params: ModelParamsSchema,
response: { 200: ModelConfigSchema, 404: Type.Null(), 503: ErrorEnvelopeSchema, 426: ErrorEnvelopeSchema },
tags: ["Models"],
},
},
async (request, reply) => {
if (!options.models) return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
const model = options.models.readModel((request.params as { model_id: string }).model_id);
return model ?? reply.code(404).send(null);
},
);
app.put(
"/api/v1/admin/models/configuration",
{
attachValidation: true,
schema: {
operationId: "replaceModelConfiguration",
body: ModelConfigUpdateRequestSchema,
headers: ModelConfigUpdateHeadersSchema,
response: {
200: ModelConfigurationResponseSchema,
400: ErrorEnvelopeSchema,
401: ErrorEnvelopeSchema,
403: ErrorEnvelopeSchema,
409: ErrorEnvelopeSchema,
412: ErrorEnvelopeSchema,
429: ErrorEnvelopeSchema,
503: ErrorEnvelopeSchema,
426: ErrorEnvelopeSchema,
},
tags: ["Admin Models"],
},
},
async (request, reply) => {
if (request.validationError) {
const validation = JSON.stringify(request.validationError.validation ?? []);
const priority = validation.includes("recommendation_priority");
return reply.code(priority ? 400 : 409).send(createErrorEnvelope({
code: priority ? "MODEL_RECOMMENDATION_PRIORITY_INVALID" : "MODEL_DEFAULT_REPLACEMENT_INVALID",
correlationId: request.id,
...(priority ? { details: { field_errors: [{ field: "models.recommendation_priority", message_key: "model.priority.invalid" }] } } : {}),
}));
}
if (!options.models || !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 (!token || !session) return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
try {
const headers = request.headers as { "idempotency-key": string; "x-csrf-token": string };
const admin = options.registration.authorizeAdminMutation({ csrfToken: headers["x-csrf-token"], sessionToken: token });
const body = request.body as {
expected_config_set_version: number;
models: Array<Record<string, unknown>>;
};
const result = options.models.replace({
actorId: admin.userId,
expectedConfigSetVersion: body.expected_config_set_version,
idempotencyKey: headers["idempotency-key"],
models: body.models as never,
});
return result;
} catch (error) {
if (error instanceof RegistrationError) {
return reply.code(error.httpStatus).send(createErrorEnvelope({
code: error.code,
correlationId: request.id,
details: { field_errors: [registrationFieldError(error.reason)] },
}));
}
return modelConfigurationFailure(reply, request.id, error);
}
},
);
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;
}