import { z } from 'zod'; import type { ApiTokenScope } from './api-tokens'; import type { CommentView } from './comments'; import type { LabelTreeNode, LabelView } from './labels'; /** * Wire types of the public REST API (`/api/public/v1`, issue #104). The * shapes are deliberately independent of the internal views: this surface * is versioned and consumed by scripts and MCP clients, so it exposes * slugs and stable ids, never internal implementation details. */ export interface PublicMeView { user: { id: string; username: string; displayName: string }; scope: ApiTokenScope; /** Pond restriction (slugs); empty = every pond the user may access. */ pondSlugs: string[]; } export interface PublicPondView { slug: string; name: string; description: string; type: 'personal' | 'shared'; createdAt: string; } export interface PublicPageListItemView { slug: string; title: string; /** Parent page slug in the tree (issue #110), or null at the root — nulled * as well when the token's user may not read the parent (no existence leak). */ parent: string | null; labels: string[]; createdAt: string; updatedAt: string; } export interface PublicPageView { slug: string; title: string; pondSlug: string; /** Parent page slug (issue #110); see {@link PublicPageListItemView.parent}. */ parent: string | null; markdown: string; html: string; labels: string[]; createdAt: string; updatedAt: string; } /** Bounded well above every realistic page (the editor caps documents far * lower); the limit only stops abuse of the raw endpoint. */ const MARKDOWN_MAX_BYTES = 2 * 1024 * 1024; export const publicCreatePageInputSchema = z.object({ title: z.string().trim().min(1, 'validation.required').max(200, 'validation.tooLong'), markdown: z.string().max(MARKDOWN_MAX_BYTES).default(''), /** Parent page slug (issue #110) — nests the new page under it. */ parent: z.string().min(1).optional(), }); export type PublicCreatePageInput = z.infer; export const publicUpdatePageInputSchema = z .object({ title: z.string().trim().min(1, 'validation.required').max(200, 'validation.tooLong'), /** Replace semantics: the whole content becomes this Markdown. */ markdown: z.string().max(MARKDOWN_MAX_BYTES), /** Move in the tree (issue #110): a page slug nests, `null` moves to the * root; the page lands at the end of its new sibling group. */ parent: z.string().min(1).nullable(), }) .partial() .refine( (input) => input.title !== undefined || input.markdown !== undefined || input.parent !== undefined, { message: 'validation.required' }, ); export type PublicUpdatePageInput = z.infer; /** * One PATCH covers rename, recolour, and move (the internal API splits * update and move); `parentId: null` moves the label to the root. */ export const publicUpdateLabelInputSchema = z .object({ name: z.string().trim().min(1, 'validation.required').max(60, 'validation.tooLong'), color: z.string().regex(/^#[0-9a-fA-F]{6}$/, 'validation.invalid'), parentId: z.string().min(1).nullable(), }) .partial(); export type PublicUpdateLabelInput = z.infer; export const publicSearchQuerySchema = z.object({ q: z.string().trim().min(1, 'validation.required').max(200), pond: z.string().trim().optional(), label: z.string().trim().optional(), }); export type PublicSearchQuery = z.infer; export interface PublicSearchResultView { pondSlug: string; pageSlug: string; title: string; snippet: string; } /** Re-exported internal shapes the public surface serves verbatim. */ export type PublicLabelTree = LabelTreeNode[]; export type PublicLabelView = LabelView; export type PublicCommentView = CommentView;