Ctrl K
ComunidadGuías…

packages/contracts/index.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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<typeof agentScopeSchema>;
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<typeof updateArtifactMetadataSchema>;
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<typeof beginUploadSchema>;
/** «Сохранить как ссылку»: 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<typeof saveLinkSchema>;
/** 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<typeof linkDocumentSchema>;
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<typeof artifactLifecycleSchema>;
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";