import { Type, type Static } from "@sinclair/typebox"; import { Value } from "@sinclair/typebox/value"; const uuidPattern = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$"; const isoTimestampPattern = "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3}Z$"; const stableResourcePattern = "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,119}$"; export const CanvasBackgroundAdjustmentsSchema = Type.Object( { brightness: Type.Number({ maximum: 100, minimum: -100 }), contrast: Type.Number({ maximum: 100, minimum: -100 }), crop: Type.Union([ Type.Object({ height: Type.Number({ maximum: 1, minimum: 0 }), width: Type.Number({ maximum: 1, minimum: 0 }), x: Type.Number({ maximum: 1, minimum: 0 }), y: Type.Number({ maximum: 1, minimum: 0 }), }, { additionalProperties: false }), Type.Null(), ]), filter: Type.String({ maxLength: 64, pattern: "^[A-Za-z0-9_-]+$" }), fit: Type.Union([Type.Literal("fill"), Type.Literal("fit"), Type.Literal("crop")]), saturation: Type.Number({ maximum: 100, minimum: -100 }), sharpness: Type.Number({ maximum: 100, minimum: 0 }), temperature: Type.Number({ maximum: 100, minimum: -100 }), }, { additionalProperties: false, $id: "CanvasBackgroundAdjustments" }, ); const CanvasScalarSchema = Type.Union([ Type.String({ maxLength: 4_000 }), Type.Number(), Type.Boolean(), Type.Null(), ]); export const CanvasElementSchema = Type.Object( { colors: Type.Optional(Type.Array(Type.String({ pattern: "^#[0-9A-Fa-f]{6}$" }), { maxItems: 5, minItems: 5 })), content: Type.Optional(Type.String({ maxLength: 4_000 })), coordinates: Type.Optional(Type.Object({ latitude: Type.Number({ maximum: 90, minimum: -90 }), longitude: Type.Number({ maximum: 180, minimum: -180 }), }, { additionalProperties: false })), created_at: Type.String({ pattern: isoTimestampPattern }), dynamic_fields: Type.Optional(Type.Record(Type.String({ pattern: "^[A-Za-z][A-Za-z0-9_]{0,63}$" }), CanvasScalarSchema)), element_id: Type.String({ pattern: uuidPattern }), font_override: Type.Optional(Type.String({ maxLength: 120, pattern: stableResourcePattern })), font_size: Type.Optional(Type.Number({ maximum: 512, minimum: 1 })), formatted_value: Type.Optional(Type.String({ maxLength: 4_000 })), opacity: Type.Number({ maximum: 1, minimum: 0 }), position: Type.Object({ x: Type.Number(), y: Type.Number() }, { additionalProperties: false }), resource_version: Type.String({ maxLength: 120, pattern: stableResourcePattern }), rotation: Type.Number({ maximum: 360, minimum: -360 }), scale: Type.Object({ x: Type.Number({ maximum: 100, minimum: 0.01 }), y: Type.Number({ maximum: 100, minimum: 0.01 }), }, { additionalProperties: false }), style_id: Type.Optional(Type.String({ maxLength: 120, pattern: stableResourcePattern })), style_parameters: Type.Optional(Type.Record(Type.String({ pattern: "^[A-Za-z][A-Za-z0-9_]{0,63}$" }), CanvasScalarSchema)), template_or_asset_id: Type.String({ maxLength: 120, pattern: stableResourcePattern }), type: Type.Union([ Type.Literal("text_template"), Type.Literal("static_sticker"), Type.Literal("color_card"), Type.Literal("dynamic_sticker"), ]), z_index: Type.Integer({ maximum: 49, minimum: 0 }), }, { additionalProperties: false, $id: "CanvasElement" }, ); export const CanvasStateSchema = Type.Object( { background: Type.Object({ adjustments: Type.Ref(CanvasBackgroundAdjustmentsSchema), asset_id: Type.Union([Type.String({ pattern: uuidPattern }), Type.Null()]), }, { additionalProperties: false }), elements: Type.Array(Type.Ref(CanvasElementSchema), { maxItems: 50 }), pixel_height: Type.Integer({ minimum: 1 }), pixel_width: Type.Integer({ minimum: 1 }), ratio: Type.Union([Type.Literal("3:4"), Type.Literal("1:1"), Type.Literal("4:3"), Type.Literal("9:16")]), schema_version: Type.Literal(1), }, { additionalProperties: false, $id: "CanvasState" }, ); export const ProjectEditableStateSchema = Type.Object( { canvas_state: Type.Ref(CanvasStateSchema), name: Type.String({ maxLength: 80, minLength: 1 }), }, { additionalProperties: false, $id: "ProjectEditableState" }, ); export const ProjectStateSaveHeadersSchema = Type.Object( { "idempotency-key": Type.String({ maxLength: 200, minLength: 32, pattern: "^[A-Za-z0-9_-]+$" }), "if-match": Type.String({ pattern: "^[1-9][0-9]*$" }), "x-csrf-token": Type.String({ maxLength: 512, minLength: 32 }), }, { additionalProperties: true, $id: "ProjectStateSaveHeaders" }, ); export const ProjectStateSaveResponseSchema = Type.Object( { save_status: Type.Literal("saved"), state_version: Type.Integer({ minimum: 2 }) }, { additionalProperties: false, $id: "ProjectStateSaveResponse" }, ); export const ProjectStateConflictResponseSchema = Type.Object( { latest_state_version: Type.Integer({ minimum: 1 }), save_status: Type.Literal("conflicted") }, { additionalProperties: false, $id: "ProjectStateConflictResponse" }, ); export type CanvasState = Static; export type ProjectEditableState = Static; export type ProjectStateSaveHeaders = Static; export function isCanvasState(value: unknown): value is CanvasState { if (!Value.Check(CanvasStateSchema, [CanvasBackgroundAdjustmentsSchema, CanvasElementSchema], value)) return false; const state = value as CanvasState; const ids = new Set(state.elements.map((element) => element.element_id)); const zIndexes = new Set(state.elements.map((element) => element.z_index)); return ids.size === state.elements.length && zIndexes.size === state.elements.length; } export function isProjectEditableState(value: unknown): value is ProjectEditableState { return Value.Check( ProjectEditableStateSchema, [CanvasBackgroundAdjustmentsSchema, CanvasElementSchema, CanvasStateSchema], value, ) && isCanvasState((value as ProjectEditableState).canvas_state); }