import { z } from "zod"; // The zod-free constants live in constants.ts so the browser's initial chunk // does not pull in zod; this module re-exports them. export { MAX_BYTES, PROJECT_MAX_BYTES, PROJECT_MAX_FILES, PROJECT_VIDEO_MAX_BYTES, PROJECT_VIDEO_MAX_FILE_BYTES, VIDEO_MIME, isVideoMime, MAX_TITLE, MIME, SINGLE_FILE_FORMATS, singleFileFormat, isTextMime, isDrawnMime, mimeForFilename, PDF_MIME, isPdfMime, looksLikeHtml, REPORT_REASONS, AGENT_SCOPES, LINK_MIME, MAX_LINK_NOTE, MAX_LINK_URL, savedLinkUrl, SEARCH_MATCH_START, SEARCH_MATCH_END, } from "./constants.ts"; export type { UploadMime, ReportReason, SingleFileFormat, SingleFileView } from "./constants.ts"; import { MAX_BYTES, MAX_TITLE, MIME, REPORT_REASONS, AGENT_SCOPES, LINK_MIME, MAX_LINK_NOTE, MAX_LINK_URL, savedLinkUrl, } from "./constants.ts"; // How a saved HTML page may be shown. "static" and "limited" render in a // scriptless, networkless sandbox; "unsupported" needs a runtime profile that // this build does not have, so it gets no link. export const HTML_PROFILES = ["static", "limited", "unsupported"] as const; export type HtmlProfile = (typeof HTML_PROFILES)[number]; export type InlineBuildStatus = { state: "pending" | "ready" | "unsupported" | "failed"; runtimeProfile: string | null; reason: string | null; path: string | null; }; export const uuid = z.string().uuid(); export const agentScopeSchema = z.enum(AGENT_SCOPES); export type AgentScope = z.infer; export const updateArtifactMetadataFields = { title: z.string().trim().min(1).max(MAX_TITLE).optional(), folderId: uuid.nullable().optional(), expectedTitle: z.string().max(200), expectedFolderId: uuid.nullable(), } as const; export const updateArtifactMetadataSchema = z .object(updateArtifactMetadataFields) .strict() .refine((value) => value.title !== undefined || value.folderId !== undefined, { message: "At least one metadata field must be changed", }); export type UpdateArtifactMetadata = z.infer; export const issueAgentConnectionSchema = z .object({ name: z.string().trim().min(1).max(80), scopes: z.array(agentScopeSchema).min(1).max(AGENT_SCOPES.length), audience: z.string().url().max(2048), ttlDays: z.number().int().min(1).max(30).default(7), /** A department shelf the account is a member of; absent: its own. */ shelfId: z.string().uuid().optional(), /** Other department shelves this token may search (read); at most 10. */ allowedShelfIds: z.array(z.string().uuid()).max(10).optional(), }) .strict() .transform((value) => ({ ...value, allowedShelfIds: [...new Set(value.allowedShelfIds ?? [])], scopes: [...new Set(value.scopes)].sort() as AgentScope[], })); export type AgentConnection = { id: string; name: string; scopes: AgentScope[]; audience: string; status: "issued" | "seen" | "expired" | "revoked"; /** token: issued on the agents page; oauth: granted to a chat connector. */ kind: "token" | "oauth"; /** OAuth only: the agent may hand its owner a one-time sign-in link. */ signInLinks?: boolean; createdAt: string; expiresAt: string; lastSeenAt: string | null; /** A department shelf the agent works on (docs/specs/TEAM_SHELVES.md); absent: one's own. */ shelf?: { id: string; name: string }; }; /** What the consent page shows for one pending connector authorization. */ export type OAuthConsentDetails = { /** The shelf the connector will save to and how its owner signs in. */ account?: { name: string; methods: string[]; provisional: boolean }; /** Shelves the agent may work on: name null is one's own (docs/specs/TEAM_SHELVES.md). */ shelves?: { id: string; name: string | null; role: string }[]; requestId: string; client: { name: string; redirectHost: string; /** Set when the answer goes to a Chrome extension (chromiumapp.org). */ extension: { id: string; official: boolean } | null; }; scopes: AgentScope[]; /** Checked at first for one's own shelf. */ defaultScopes: AgentScope[]; /** Checked at first for a department shelf: narrower (docs/specs/MCP_ONBOARDING_SPEC.md). */ teamDefaultScopes: AgentScope[]; accessMinutes: number; refreshDays: number; maxDays: number; replaces: boolean; expiresAt: string; }; /** A provenance address: https, no credentials, query or fragment. */ export const sourceUrlSchema = z .string() .max(2048) .refine((value) => { if ( !value.startsWith("https://") || /[\s\\\u0000-\u001f\u007f]/.test(value) || value.includes("?") || value.includes("#") ) return false; try { const url = new URL(value); return url.protocol === "https:" && !url.username && !url.password && !url.search && !url.hash; } catch { return false; } }, "sourceUrl must be an HTTPS URL without credentials, query, or fragment"); export const beginUploadSchema = z .object({ key: uuid, title: z.string().trim().min(1).max(MAX_TITLE), filename: z.string().trim().min(1).max(200), mime: z.enum([...MIME, LINK_MIME]), size: z.number().int().min(1).max(MAX_BYTES), sha256: z.string().regex(/^[a-f0-9]{64}$/), artifactId: uuid.optional(), baseRevisionId: uuid.optional(), folderId: uuid.nullable().optional(), /** The page an HTML file was saved from (the «На Полку» bookmark); kept in provenance. */ sourceUrl: sourceUrlSchema.optional(), }) .strict() .refine((x) => !!x.artifactId === !!x.baseRevisionId, "A revision needs its base") .refine((x) => x.sourceUrl === undefined || x.mime === "text/html", "Only an HTML page keeps its source address"); export type UploadInput = z.infer; /** «Сохранить как ссылку»: the web form, the MCP tool and the API take this. */ export const saveLinkSchema = z .object({ key: uuid, url: z .string() .trim() .min(1) .max(MAX_LINK_URL) .refine((value) => !!savedLinkUrl(value), "An http(s) link without credentials"), title: z.string().trim().min(1).max(MAX_TITLE).optional(), note: z.string().trim().max(MAX_LINK_NOTE).optional(), folderId: uuid.nullable().optional(), }) .strict(); export type SaveLinkInput = z.infer; /** The stored file of a link work. */ export const linkDocumentSchema = z .object({ v: z.literal(1), url: z .string() .max(MAX_LINK_URL) .refine((value) => savedLinkUrl(value)?.href === value, "A normalised http(s) link"), note: z.string().max(MAX_LINK_NOTE).nullable(), }) .strict(); export type LinkDocument = z.infer; export const shareSchema = z .object({ expectedRevisionId: uuid, expiresInDays: z.union([z.literal(1), z.literal(7), z.literal(30)]), }) .strict(); export const publishSchema = z.object({ revisionId: uuid, expectedPublishedRevisionId: uuid }).strict(); export const artifactLifecycleSchema = z .object({ expectedLifecycleVersion: z.number().int().min(0), expectedRevisionId: uuid, }) .strict(); export type ArtifactLifecycleInput = z.infer; export type ArtifactLifecycleSnapshot = { id: string; trashedAt: string | null; lifecycleVersion: number; }; export const reportSchema = z .object({ key: uuid, token: z.string().regex(/^[A-Za-z0-9_-]{43}$/), reason: z.enum(REPORT_REASONS), comment: z.string().trim().max(1000).optional(), // A report about one comment of the link rather than the page. commentId: uuid.optional(), }) .strict(); export interface Revision { manifest?: import("./bundle.ts").BundleManifest | null; manifestSha256?: string | null; id: string; number: number; filename: string; mime: string; size: number; sha256: string; storageKind: "single" | "bundle"; totalSize: number; htmlProfile: HtmlProfile | null; inlineBuild: InlineBuildStatus | null; createdAt: string; /** A link work (LINK_MIME): its host and service; the URL itself is in its file. */ link?: { host: string; service: import("./link-providers.ts").LinkProviderId | null } | null; /** * The owner's shelf only (docs/specs/SHELF_COVERS.md): how the card looks. * null: not decided yet, the card asks GET /api/revisions/:id/cover. */ cover?: import("./cover.ts").RevisionCover | null; } export interface Share { id: string; revisionId: string; number: number; status: "active" | "behind" | "expired" | "revoked"; /** * held: waits for the operator before its first open; paused: after * reports; blocked: closed by the operator or the content filter. */ moderation?: ShareModeration; /** Blocked: where the owner may appeal (an e-mail address). */ appeal?: string | null; url: string | null; expiresAt: string; /** * How often recipients opened the link (not counting the owner): a total, the * number of days it was opened on and the last time. Nothing about who. */ opens?: { total: number; days: number; lastOpenedAt: string | null }; /** held or paused: since when the link waits for review. */ reviewSince?: string; /** Whether an unattended agent may move the link to new versions. */ followMode?: "pinned" | "follows"; } export interface Artifact { id: string; title: string; folderId: string | null; updatedAt: string; trashedAt: string | null; lifecycleVersion: number; revision: Revision; share: Share | null; /** The version a curator accepted; null or absent: no mark. */ acceptedRevisionId?: string | null; /** Who answers for the work, while still a member of the shelf. */ ownerAccountId?: string | null; /** Who saved it, on a department shelf (docs/specs/TEAM_SHELVES.md). */ author?: { id: string; name: string }; /** * Who saved the latest version: a person (web app, or the API with a * session) or an agent connection, with its name while the connection * exists. Absent when the audit row is gone (docs/specs/ONBOARDING_V2.md). */ savedBy?: { type: "owner" | "agent"; name?: string }; /** * Found by its text (docs/specs/CONTENT_SEARCH.md): a fragment with the * found words between SEARCH_MATCH_START and SEARCH_MATCH_END. */ snippet?: string; } export interface Folder { id: string; name: string; } export interface Receipt { // Absent on older receipts and non-HTML uploads. manifestSha256?: string | null; uploadId: string; artifactId: string; revisionId: string; number: number; sha256: string; storageKind?: "single" | "bundle"; totalSize?: number; // Absent on receipts saved before HTML support. htmlProfile?: HtmlProfile | null; } // Legacy browser-only URL classification shapes. These are NOT the durable // /api/imports job contract implemented in server/url-import; no receipt here. export type ImportStatus = "not_https" | "closed" | "unsupported_host" | "ready" | "provider"; export interface ImportProvenance { sourceUrl: string; sourceHost: string; fetchedAt: string; } export interface ImportPreview { status: ImportStatus; title: string | null; htmlProfile: HtmlProfile | null; provenance: ImportProvenance | null; } export type ShareModeration = "none" | "held" | "paused" | "blocked"; export interface Viewer { title: string; revision: Revision; grant: string; expiresAt: string; /** editorial: «Редакция Полки»; user: a page someone published. */ publisher: "editorial" | "user"; /** The author's account is younger than NEW_ACCOUNT_DAYS. Never the name. */ authorIsNew: boolean; /** * The viewed version has fields for a password, a card, a one-time code or * a seed phrase (content filter, save time). null: saved before this was * recorded, or not readable: unknown. */ sensitiveInput: boolean | null; /** The automatic check (rules and models) answered and found nothing. */ autoChecked: boolean; } /** A held or paused link: no title, no content, no grant. */ export interface ViewerUnderReview { review: true; } /** A blocked link: «Ссылка недоступна», nothing about the work. */ export interface ViewerBlocked { blocked: true; } export type Resolved = Viewer | ViewerUnderReview | ViewerBlocked; export interface Account { id: string; name: string; /** From /api/session only; null for accounts made before it was recorded. */ createdAt?: string | null; /** * From /api/session only: a provisional shelf (no sign-in method yet; it * lives in this browser and cannot share until claimed). */ provisional?: boolean; /** Days without use after which a provisional shelf is deleted. */ idleDays?: number; /** The session came from an agent's sign-in link (browse only). */ assurance?: "agent_link"; /** * From /api/session only: when the owner hid the shelf's «Подключите * агента» block (docs/specs/ONBOARDING_V2.md); null: not hidden. */ onboardingHiddenAt?: string | null; } export type ErrorCode = | "invalid" | "unauthorized" | "not_found" | "conflict" | "expired" | "quota" | "forbidden" | "unsupported" // 503 with Retry-After: the server is busy, nothing was done; retry later. | "busy" | "internal";