packages/contracts/index.ts
Rama main · réplica de lectura del repositorio original.
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";