diff --git a/src/app/api/discovery/check-duplicates/route.ts b/src/app/api/discovery/check-duplicates/route.ts index 3f22956..c988246 100644 --- a/src/app/api/discovery/check-duplicates/route.ts +++ b/src/app/api/discovery/check-duplicates/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' -import crypto from 'crypto' import { prisma } from '@/lib/prisma' import { CheckTaskDuplicatesSchema } from '@/lib/validations' +import { isValidApiKey } from '@/lib/auth' /** * 检查 URL 是否应该创建新任务 @@ -194,14 +194,7 @@ export async function POST(request: NextRequest) { const { apiKey, urls, sourceType } = validationResult.data // 验证 API Key - const validApiKey = process.env.WEBHOOK_API_KEY - if ( - !validApiKey || - !crypto.timingSafeEqual( - Buffer.from(apiKey), - Buffer.from(validApiKey) - ) - ) { + if (!isValidApiKey(apiKey)) { return NextResponse.json( { success: false, diff --git a/src/app/api/discovery/tasks/[id]/complete/route.ts b/src/app/api/discovery/tasks/[id]/complete/route.ts index 5c61f37..7e5ea09 100644 --- a/src/app/api/discovery/tasks/[id]/complete/route.ts +++ b/src/app/api/discovery/tasks/[id]/complete/route.ts @@ -1,11 +1,11 @@ import { NextRequest, NextResponse } from 'next/server' -import crypto from 'crypto' import { prisma } from '@/lib/prisma' import { generateSlug } from '@/lib/slug' import { ProjectInputSchema, type ProjectInput, } from '@/lib/validations' +import { isValidApiKey } from '@/lib/auth' import { findExistingProject, resolveFixedProjectTypeTag, @@ -28,11 +28,7 @@ export async function POST( const { apiKey, explorationData } = body // 验证API密钥 - const validApiKey = process.env.WEBHOOK_API_KEY - if ( - !validApiKey || - !crypto.timingSafeEqual(Buffer.from(apiKey), Buffer.from(validApiKey)) - ) { + if (!isValidApiKey(apiKey)) { return NextResponse.json( { success: false, error: 'Unauthorized' }, { status: 401 } diff --git a/src/app/api/discovery/tasks/[id]/route.ts b/src/app/api/discovery/tasks/[id]/route.ts index 4d28ce7..c8e8822 100644 --- a/src/app/api/discovery/tasks/[id]/route.ts +++ b/src/app/api/discovery/tasks/[id]/route.ts @@ -1,8 +1,8 @@ import { NextRequest, NextResponse } from 'next/server' -import crypto from 'crypto' import { prisma } from '@/lib/prisma' import { UpdateDiscoveryTaskSchema, TaskStatus } from '@/lib/validations' import type { Prisma } from '@prisma/client' +import { isValidApiKey } from '@/lib/auth' /** * 有效的任务状态转换规则 @@ -87,11 +87,7 @@ export async function PATCH( validation.data // 验证API密钥 - const validApiKey = process.env.WEBHOOK_API_KEY - if ( - !validApiKey || - !crypto.timingSafeEqual(Buffer.from(apiKey), Buffer.from(validApiKey)) - ) { + if (!isValidApiKey(apiKey)) { return NextResponse.json( { success: false, error: 'Unauthorized' }, { status: 401 } @@ -143,7 +139,9 @@ export async function PATCH( updateData.completedAt = new Date() } - if (explorationData !== undefined) updateData.explorationData = explorationData as Prisma.InputJsonObject + if (explorationData !== undefined) { + updateData.explorationData = explorationData as Prisma.InputJsonObject + } if (explorationSummary !== undefined) updateData.explorationSummary = explorationSummary if (errorMessage !== undefined) updateData.errorMessage = errorMessage diff --git a/src/app/api/discovery/tasks/batch-reset/route.ts b/src/app/api/discovery/tasks/batch-reset/route.ts index 51c2ed4..40989ad 100644 --- a/src/app/api/discovery/tasks/batch-reset/route.ts +++ b/src/app/api/discovery/tasks/batch-reset/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' -import crypto from 'crypto' import { prisma } from '@/lib/prisma' import { BatchResetTasksSchema } from '@/lib/validations' +import { isValidApiKey } from '@/lib/auth' /** * POST /api/discovery/tasks/batch-reset @@ -31,11 +31,7 @@ export async function POST(request: NextRequest) { const { apiKey, taskIds, statuses } = validation.data // 验证API密钥 - const validApiKey = process.env.WEBHOOK_API_KEY - if ( - !validApiKey || - !crypto.timingSafeEqual(Buffer.from(apiKey), Buffer.from(validApiKey)) - ) { + if (!isValidApiKey(apiKey)) { return NextResponse.json( { success: false, error: 'Unauthorized' }, { status: 401 } diff --git a/src/app/api/discovery/tasks/route.ts b/src/app/api/discovery/tasks/route.ts index 4a82d0e..42ba7f9 100644 --- a/src/app/api/discovery/tasks/route.ts +++ b/src/app/api/discovery/tasks/route.ts @@ -1,10 +1,10 @@ import { NextRequest, NextResponse } from 'next/server' -import crypto from 'crypto' import { prisma } from '@/lib/prisma' import { CreateDiscoveryTaskSchema, GetDiscoveryTasksQuerySchema, } from '@/lib/validations' +import { isValidApiKey } from '@/lib/auth' /** * POST /api/discovery/tasks @@ -29,11 +29,7 @@ export async function POST(request: NextRequest) { const { apiKey, tasks } = validation.data // 验证API密钥 - const validApiKey = process.env.WEBHOOK_API_KEY - if ( - !validApiKey || - !crypto.timingSafeEqual(Buffer.from(apiKey), Buffer.from(validApiKey)) - ) { + if (!isValidApiKey(apiKey)) { return NextResponse.json( { success: false, error: 'Unauthorized' }, { status: 401 } @@ -101,12 +97,7 @@ export async function GET(request: NextRequest) { // 验证 API Key(只读权限) // 支持两种方式:1. 请求头 x-api-key 2. 查询参数 apiKey const apiKey = request.headers.get('x-api-key') || searchParams.get('apiKey') - const validApiKey = process.env.WEBHOOK_API_KEY - if ( - !validApiKey || - !apiKey || - !crypto.timingSafeEqual(Buffer.from(apiKey), Buffer.from(validApiKey)) - ) { + if (!isValidApiKey(apiKey)) { return NextResponse.json( { success: false, error: 'Unauthorized' }, { status: 401 } diff --git a/src/app/api/projects/[slug]/route.ts b/src/app/api/projects/[slug]/route.ts index 353f598..486dc7c 100644 --- a/src/app/api/projects/[slug]/route.ts +++ b/src/app/api/projects/[slug]/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import { prisma } from '@/lib/prisma' +import { isValidApiKey } from '@/lib/auth' /** * DELETE /api/projects/[slug] @@ -21,10 +22,8 @@ export async function DELETE( const { slug } = await params // Verify API Key - const apiKey = request.headers.get('x-api-key') || process.env.WEBHOOK_API_KEY - const validApiKey = process.env.WEBHOOK_API_KEY - - if (apiKey !== validApiKey) { + const apiKey = request.headers.get('x-api-key') + if (!isValidApiKey(apiKey)) { return NextResponse.json( { success: false, diff --git a/src/app/api/tags/maintenance/route.ts b/src/app/api/tags/maintenance/route.ts index 73ecc2c..aeb6191 100644 --- a/src/app/api/tags/maintenance/route.ts +++ b/src/app/api/tags/maintenance/route.ts @@ -1,8 +1,8 @@ import { NextRequest, NextResponse } from "next/server"; -import crypto from "crypto"; import { revalidatePath } from "next/cache"; import { prisma } from "@/lib/prisma"; import { TagMaintenanceRequestSchema } from "@/lib/validations"; +import { isValidApiKey } from "@/lib/auth"; import { executeTagMaintenance, TagMaintenanceApiError } from "./service"; export async function POST(request: NextRequest) { @@ -26,14 +26,7 @@ export async function POST(request: NextRequest) { const { apiKey, updates, merges } = validation.data; // 3. Authenticate with timing-safe comparison - const expectedApiKey = process.env.WEBHOOK_API_KEY; - const providedBuf = Buffer.from(apiKey); - const expectedBuf = Buffer.from(expectedApiKey || ""); - if ( - !expectedApiKey || - providedBuf.length !== expectedBuf.length || - !crypto.timingSafeEqual(providedBuf, expectedBuf) - ) { + if (!isValidApiKey(apiKey)) { return NextResponse.json( { success: false, diff --git a/src/app/api/webhook/check-duplicates/route.ts b/src/app/api/webhook/check-duplicates/route.ts index dd21026..6244544 100644 --- a/src/app/api/webhook/check-duplicates/route.ts +++ b/src/app/api/webhook/check-duplicates/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import { prisma } from '@/lib/prisma' +import { isValidApiKey } from '@/lib/auth' import { z } from 'zod' /** @@ -179,8 +180,7 @@ export async function POST(request: NextRequest) { const payload = validationResult.data // Verify API Key - const apiKey = process.env.WEBHOOK_API_KEY - if (payload.apiKey !== apiKey) { + if (!isValidApiKey(payload.apiKey)) { return NextResponse.json( { success: false, diff --git a/src/app/api/webhook/projects/route.ts b/src/app/api/webhook/projects/route.ts index 76cb145..edaf6f3 100644 --- a/src/app/api/webhook/projects/route.ts +++ b/src/app/api/webhook/projects/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' -import crypto from 'crypto' import { prisma } from '@/lib/prisma' import type { ProjectStatus, LinkType } from '@prisma/client' +import { isValidApiKey } from '@/lib/auth' import { WebhookPayloadSchema, ProjectInputSchema, @@ -38,14 +38,7 @@ export async function POST(request: NextRequest) { const payload = validationResult.data as WebhookPayload // Verify API Key using timing-safe comparison to prevent timing attacks - const apiKey = process.env.WEBHOOK_API_KEY - if ( - !apiKey || - !crypto.timingSafeEqual( - Buffer.from(payload.apiKey), - Buffer.from(apiKey) - ) - ) { + if (!isValidApiKey(payload.apiKey)) { return NextResponse.json( { success: false, @@ -66,7 +59,7 @@ export async function POST(request: NextRequest) { index: number field: string message: string - value: any + value: unknown }>, } @@ -123,42 +116,44 @@ export async function POST(request: NextRequest) { `[Webhook] Updating project "${validProject.name}" (matched by ${matchMethod}, id: ${existingProject.id})` ) - // Update tags (delete old ones, create new ones) - await prisma.projectTag.deleteMany({ - where: { projectId: existingProject.id }, - }) + await prisma.$transaction(async (tx) => { + // Update tags (delete old ones, create new ones) + await tx.projectTag.deleteMany({ + where: { projectId: existingProject.id }, + }) - await prisma.project.update({ - where: { id: existingProject.id }, - data: { - name: validProject.name, - nameEn: validProject.nameEn || null, - description: validProject.description, - descriptionEn: validProject.descriptionEn || null, - content: validProject.content || null, - contentEn: validProject.contentEn || null, - status: validProject.status as ProjectStatus, - source: validProject.source || null, - tags: { - create: tagConnections.map((t) => ({ - tag: { connect: { id: t.id } }, - })), + await tx.project.update({ + where: { id: existingProject.id }, + data: { + name: validProject.name, + nameEn: validProject.nameEn || null, + description: validProject.description, + descriptionEn: validProject.descriptionEn || null, + content: validProject.content || null, + contentEn: validProject.contentEn || null, + status: validProject.status as ProjectStatus, + source: validProject.source || null, + tags: { + create: tagConnections.map((t) => ({ + tag: { connect: { id: t.id } }, + })), + }, }, - }, - }) + }) - // Update links (delete old ones, create new ones) - await prisma.externalLink.deleteMany({ - where: { projectId: existingProject.id }, - }) + // Update links (delete old ones, create new ones) + await tx.externalLink.deleteMany({ + where: { projectId: existingProject.id }, + }) - await prisma.externalLink.createMany({ - data: validProject.links.map((link) => ({ - type: link.type as LinkType, - url: link.url, - title: link.title || null, - projectId: existingProject.id, - })), + await tx.externalLink.createMany({ + data: validProject.links.map((link) => ({ + type: link.type as LinkType, + url: link.url, + title: link.title || null, + projectId: existingProject.id, + })), + }) }) results.updated++ diff --git a/src/app/globals.css b/src/app/globals.css index 2e4c1d9..73aa239 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,4 +1,5 @@ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Space+Mono:wght@400;700&display=swap'); +@import url('https://fonts.googleapis.com/icon?family=Material+Icons'); @tailwind base; @tailwind components; diff --git a/src/app/icon.svg b/src/app/icon.svg new file mode 100644 index 0000000..d50a79d --- /dev/null +++ b/src/app/icon.svg @@ -0,0 +1,15 @@ + + + + + AP + + diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2be2960..fb157ed 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -13,12 +13,6 @@ export default function RootLayout({ }>) { return ( - - {/* Load fonts via CSS */} - - {/* Material Icons */} - - {children} diff --git a/src/components/project/MarkdownContent.tsx b/src/components/project/MarkdownContent.tsx index cfe0b56..c9801ce 100644 --- a/src/components/project/MarkdownContent.tsx +++ b/src/components/project/MarkdownContent.tsx @@ -9,6 +9,23 @@ interface MarkdownContentProps { className?: string } +const BLOCK_LEVEL_TAGS = new Set([ + 'pre', + 'table', + 'blockquote', + 'ul', + 'ol', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'div', + 'img', + 'hr', +]) + // Generate heading ID from text function generateHeadingId(text: string): string { return text @@ -22,14 +39,6 @@ function generateHeadingId(text: string): string { .replace(/-+$/, '') // Trim - from end } -// Helper function to escape HTML -function escapeHtml(code: string): string { - return code - .replace(/&/g, '&') - .replace(//g, '>') -} - // Custom GitHub-style components const components: Components = { // Headings with anchor links @@ -87,13 +96,13 @@ const components: Components = { ), // Paragraphs - skip wrapping if contains block-level elements like pre - p: ({ children, node, ...props }: any) => { + p: ({ children, node, ...props }) => { // Check if the paragraph node contains block-level elements in its children // This uses the AST node data from react-markdown - const hasBlockElement = node?.children?.some((child: any) => { - const tagName = child?.tagName + const hasBlockElement = node?.children?.some((child) => { + const tagName = 'tagName' in child ? child.tagName : undefined // Check if any direct child is a block-level element - return ['pre', 'table', 'blockquote', 'ul', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'img', 'hr'].includes(tagName) + return typeof tagName === 'string' && BLOCK_LEVEL_TAGS.has(tagName) }) if (hasBlockElement) { @@ -131,7 +140,7 @@ const components: Components = { ), // Code blocks (pre element wrapper) - pre: ({ children, ...props }: any) => { + pre: ({ children, ...props }) => { return (
         {children}
@@ -140,12 +149,12 @@ const components: Components = {
   },
 
   // Code elements (both inline and in code blocks)
-  code: ({ inline, className, children, ...props }: any) => {
-    // If inline is explicitly true, or if there's no language class and no newlines, treat as inline
+  code: ({ className, children, ...props }) => {
+    // If there's no language class and no newlines, treat as inline code.
     const hasLanguageClass = className && typeof className === 'string' && className.startsWith('language-')
     const childStr = String(children)
     const hasNewlines = childStr.includes('\n')
-    const isInline = inline === true || (!hasLanguageClass && !hasNewlines)
+    const isInline = !hasLanguageClass && !hasNewlines
 
     if (isInline) {
       return (
@@ -217,6 +226,8 @@ const components: Components = {
   img: ({ src, alt, ...props }) => (
     
+ {/* External markdown images use arbitrary remote hosts; Next/Image is not suitable here. */} + {/* eslint-disable-next-line @next/next/no-img-element */} {alt} { @@ -105,7 +104,7 @@ export function ProjectCard({ project, locale, featured = false, translations }: {/* GitHub Stars Badge */} {badges.stars && (
- +
)} diff --git a/src/components/project/ProjectDetail.tsx b/src/components/project/ProjectDetail.tsx index 5c0f033..cd1d889 100644 --- a/src/components/project/ProjectDetail.tsx +++ b/src/components/project/ProjectDetail.tsx @@ -1,9 +1,5 @@ -import Link from 'next/link' import { getTranslations } from 'next-intl/server' import { MarkdownContent } from './MarkdownContent' -import { ShareButtons } from './ShareButtons' -import { GitHubBadges } from './GitHubBadges' -import { getGitHubBadgesFromLinks } from '@/lib/github/badges' import { isFixedProjectTypeSlug } from '@/lib/tag-taxonomy' interface ProjectDetailProps { @@ -57,9 +53,6 @@ export async function ProjectDetail({ project, locale }: ProjectDetailProps) { locale === 'en' && project.descriptionEn ? project.descriptionEn : project.description const displayContent = locale === 'en' && project.contentEn ? project.contentEn : project.content - // Generate GitHub badge URLs - const badges = getGitHubBadgesFromLinks(project.links as any) - const fixedTypeTag = project.tags.find((tag) => isFixedProjectTypeSlug(tag.slug)) const category = fixedTypeTag?.name || project.tags[0]?.name || 'AI Agent' const categoryEn = diff --git a/src/hooks/useProjects.ts b/src/hooks/useProjects.ts index 52cadcf..5c37184 100644 --- a/src/hooks/useProjects.ts +++ b/src/hooks/useProjects.ts @@ -92,6 +92,9 @@ export type FixedProjectTypeFilter = { export const PROJECT_SORT_OPTIONS = ['latest', 'stars_desc', 'stars_asc'] as const export type ProjectSortOption = (typeof PROJECT_SORT_OPTIONS)[number] +const DEFAULT_PAGE = 1 +const DEFAULT_LIMIT = 20 +const MAX_LIMIT = 100 export function normalizeProjectSort(value?: string): ProjectSortOption { const candidate = String(value || '').trim() @@ -101,6 +104,16 @@ export function normalizeProjectSort(value?: string): ProjectSortOption { return 'latest' } +function normalizePositiveInteger( + value: number | undefined, + fallback: number, + max?: number +): number { + const normalized = Number.isFinite(value) ? Math.trunc(value as number) : fallback + const bounded = normalized > 0 ? normalized : fallback + return typeof max === 'number' ? Math.min(bounded, max) : bounded +} + export async function getProjects(options?: { search?: string tag?: string @@ -130,10 +143,13 @@ export async function getProjects(options?: { projectType, sort = 'latest', status = 'ACTIVE', - page = 1, - limit = 20, + page = DEFAULT_PAGE, + limit = DEFAULT_LIMIT, } = options || {} + const safePage = normalizePositiveInteger(page, DEFAULT_PAGE) + const safeLimit = normalizePositiveInteger(limit, DEFAULT_LIMIT, MAX_LIMIT) + const where: Prisma.ProjectWhereInput = { status, } @@ -251,8 +267,8 @@ export async function getProjects(options?: { links: true, }, orderBy, - skip: (page - 1) * limit, - take: limit, + skip: (safePage - 1) * safeLimit, + take: safeLimit, }), prisma.project.count({ where }), ]) @@ -273,10 +289,10 @@ export async function getProjects(options?: { return { projects: transformedProjects, pagination: { - page, - limit, + page: safePage, + limit: safeLimit, total, - totalPages: Math.ceil(total / limit), + totalPages: Math.ceil(total / safeLimit), }, } } diff --git a/src/lib/auth.test.ts b/src/lib/auth.test.ts new file mode 100644 index 0000000..6318017 --- /dev/null +++ b/src/lib/auth.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest' +import { isValidApiKey } from './auth' + +describe('isValidApiKey', () => { + it('returns false when provided key is missing', () => { + expect(isValidApiKey(undefined, 'a'.repeat(32))).toBe(false) + expect(isValidApiKey(null, 'a'.repeat(32))).toBe(false) + }) + + it('returns false when expected key is missing', () => { + expect(isValidApiKey('a'.repeat(32), undefined)).toBe(false) + }) + + it('returns false when key lengths differ', () => { + expect(isValidApiKey('a'.repeat(31), 'a'.repeat(32))).toBe(false) + }) + + it('returns false when keys have same length but different value', () => { + expect(isValidApiKey('b'.repeat(32), 'a'.repeat(32))).toBe(false) + }) + + it('returns true when keys match exactly', () => { + expect(isValidApiKey('a'.repeat(32), 'a'.repeat(32))).toBe(true) + }) +}) diff --git a/src/lib/auth.ts b/src/lib/auth.ts new file mode 100644 index 0000000..811555c --- /dev/null +++ b/src/lib/auth.ts @@ -0,0 +1,23 @@ +import crypto from 'crypto' + +/** + * Compare API keys with constant-time semantics. + * Returns false when either key is missing or lengths differ. + */ +export function isValidApiKey( + providedApiKey: string | null | undefined, + expectedApiKey: string | undefined = process.env.WEBHOOK_API_KEY +): boolean { + if (!providedApiKey || !expectedApiKey) { + return false + } + + const providedBuffer = Buffer.from(providedApiKey) + const expectedBuffer = Buffer.from(expectedApiKey) + + if (providedBuffer.length !== expectedBuffer.length) { + return false + } + + return crypto.timingSafeEqual(providedBuffer, expectedBuffer) +} diff --git a/src/lib/github/badges.ts b/src/lib/github/badges.ts index 81b1f05..7055bd6 100644 --- a/src/lib/github/badges.ts +++ b/src/lib/github/badges.ts @@ -1,4 +1,7 @@ -import { ExternalLink, LinkType } from '@prisma/client' +type LinkLike = { + type: string + url: string +} /** * 从 GitHub URL 提取 owner 和 repo @@ -60,7 +63,7 @@ export function getAllGitHubBadgeUrls(owner: string, repo: string): { * @returns stars 徽章 URL 或 null */ export function getGitHubBadgesFromLinks( - links: ExternalLink[] + links: ReadonlyArray ): { stars: string | null } { const githubLink = links.find(link => link.type === 'GITHUB') diff --git a/src/lib/validations.ts b/src/lib/validations.ts index 3f25677..e52d557 100644 --- a/src/lib/validations.ts +++ b/src/lib/validations.ts @@ -71,6 +71,19 @@ export const WebhookPayloadSchema = WebhookAuthSchema.extend({ export const TaskStatusEnum = z.enum(["PENDING", "IN_PROGRESS", "COMPLETED", "FAILED"]); +const JsonValueSchema: z.ZodType = z.lazy(() => + z.union([ + z.string(), + z.number(), + z.boolean(), + z.null(), + z.array(JsonValueSchema), + z.record(JsonValueSchema), + ]) +); + +export const JsonObjectSchema = z.record(JsonValueSchema); + export const CreateDiscoveryTaskSchema = WebhookAuthSchema.extend({ tasks: z .array( @@ -85,7 +98,7 @@ export const CreateDiscoveryTaskSchema = WebhookAuthSchema.extend({ export const UpdateDiscoveryTaskSchema = z.object({ apiKey: z.string().min(32), status: TaskStatusEnum, - explorationData: z.record(z.unknown()).optional(), + explorationData: JsonObjectSchema.optional(), explorationSummary: z.string().max(1000).optional(), errorMessage: z.string().max(2000).optional(), }); @@ -118,6 +131,20 @@ export const CheckTaskDuplicatesSchema = WebhookAuthSchema.extend({ sourceType: z.string().max(50).optional(), }); +// ================================ +// AI Timeline Schemas +// ================================ + +export const AIEventInputSchema = z.object({ + title: z.string().min(1).max(200), + titleEn: z.string().max(200).optional(), + eventDate: z.string().datetime({ offset: true }), + description: z.string().min(10).max(500), + descriptionEn: z.string().max(500).optional(), + imageUrl: z.string().url().max(2000), + sourceUrl: z.string().url().max(2000).optional(), +}); + // ================================ // Query Schemas // ================================ @@ -146,6 +173,8 @@ export type CreateDiscoveryTask = z.infer; export type UpdateDiscoveryTask = z.infer; export type GetDiscoveryTasksQuery = z.infer; export type BatchResetTasks = z.infer; +export type AIEventInput = z.infer; +export type JsonObject = z.infer; // ================================ // Tags API Schemas