Skip to the content

Capsid menu

src/contract/mentions.ts

101 lines and 0 functions in DrDustinEdwards/site-api, at commit d3b340f on 2026-10-24. This file on GitHub.

Functions

None: it only declares types or values.

Imports

It imports src/contract/base.ts. Imported by src/contract.ts.

Source

// The mentions group (v0.5.0).

import { z } from "zod";
import { ChangeId, ContentId, Timestamp, Version } from "./base.js";

/** A site's id for one received mention, as text: a row id such as 41. Never a path. */
export const MentionId = ContentId;

/**
 * Where a mention is in the site's moderation queue. unverified: received, the sender's page not yet
 * fetched. pending: the link is really there, awaiting a decision. approved: shown on the post.
 * rejected: kept, so a re-sender does not reappear as new. failed: verification found no link.
 */
export const MentionStatus = z.enum(["unverified", "pending", "approved", "rejected", "failed"]);

export const MentionItem = z.object({
  id: MentionId,
  status: MentionStatus,
  /** The page the sender says links to us. Text from a stranger: never a link. */
  sourceUrl: z.string().min(1).max(2000),
  /** The content id the mention targets, such as a post slug. */
  targetId: z.string().min(1).max(300),
  authorName: z.string().max(500).nullable(),
  authorUrl: z.string().max(2000).nullable(),
  excerpt: z.string().max(5000).nullable(),
  /** Why verification failed, in the site's fixed words; null unless failed. */
  failureReason: z.string().max(500).nullable(),
  receivedAt: Timestamp,
  verifiedAt: Timestamp.nullable(),
  decidedAt: Timestamp.nullable(),
  /** Opaque, like a content version: it must move whenever the status, the verification or the decision does. */
  version: Version,
});

export const MentionListQuery = z.object({
  status: MentionStatus.optional(),
  /** v0.6.0: words in the source address, the author or the excerpt. */
  q: z.string().max(200).optional(),
  /** v0.6.0: only the mentions of this content id, such as a post slug. */
  targetId: z.string().min(1).max(300).optional(),
  cursor: z.string().max(500).optional(),
  limit: z.coerce.number().int().min(1).max(200).default(50),
});

/** How many mentions the site holds in each status. */
export const MentionCounts = z.object({
  unverified: z.number().int().min(0),
  pending: z.number().int().min(0),
  approved: z.number().int().min(0),
  rejected: z.number().int().min(0),
  failed: z.number().int().min(0),
});

export const MentionList = z.object({
  items: z.array(MentionItem),
  nextCursor: z.string().nullable(),
  /** Across the whole queue, not the page, so a filter can show its count. */
  counts: MentionCounts,
  /** What a sweep would remove now, by status (the site's retention windows). */
  expiring: z.object({ failed: z.number().int().min(0), rejected: z.number().int().min(0) }),
  /** The filters of v0.6.0 the site applied, echoed. Absent: the site did not filter by q or targetId. */
  filtered: z.object({ q: z.string().max(200).optional(), targetId: z.string().max(300).optional() }).optional(),
});

/** `reset` (v0.6.0, optional per site) takes an approved or rejected mention back to pending, so a decision can be undone. */
export const MENTION_DECISIONS = ["approve", "reject", "reset"] as const;

export const MentionDecideInput = z.object({
  decision: z.enum(MENTION_DECISIONS),
  expectedVersion: Version,
  changeId: ChangeId,
});

export const MentionWriteResult = z.object({
  id: MentionId,
  status: MentionStatus,
  /** The mention's version after the write. */
  version: Version,
  changeId: ChangeId,
  /** Whether the site purged the target's cached page: false when the purge failed, null when there was none to run. */
  purged: z.boolean().nullable(),
});

export const MentionDeleteQuery = z.object({
  expectedVersion: Version,
  changeId: ChangeId,
});

export const MentionDeleteResult = z.object({
  id: MentionId,
  deleted: z.literal(true),
  changeId: ChangeId,
  purged: z.boolean().nullable(),
});

/** Sweeping removes the mentions past the site's retention windows: failed and rejected ones. It names no version, since it is not about one mention. */
export const MentionSweepInput = z.object({
  changeId: ChangeId,
});

export const MentionSweepResult = z.object({
  changeId: ChangeId,
  removed: z.object({ failed: z.number().int().min(0), rejected: z.number().int().min(0) }),
});

export type MentionStatus = z.infer<typeof MentionStatus>;
export type MentionItem = z.infer<typeof MentionItem>;
export type MentionListQuery = z.infer<typeof MentionListQuery>;
export type MentionCounts = z.infer<typeof MentionCounts>;
export type MentionList = z.infer<typeof MentionList>;
export type MentionDecideInput = z.infer<typeof MentionDecideInput>;
export type MentionWriteResult = z.infer<typeof MentionWriteResult>;
export type MentionDeleteQuery = z.infer<typeof MentionDeleteQuery>;
export type MentionDeleteResult = z.infer<typeof MentionDeleteResult>;
export type MentionSweepInput = z.infer<typeof MentionSweepInput>;
export type MentionSweepResult = z.infer<typeof MentionSweepResult>;