Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m38s
CI / Build container images (pull_request) Successful in 4m14s
CI / Auth e2e pack (pull_request) Successful in 9m7s
CI / Import/export fidelity gate (pull_request) Successful in 1m6s
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
CI / Lint, typecheck, test (push) Has been cancelled
Feeds: classified entries carry a standard Atom <category> (term=level, scheme=urn:dorfteich:classification, label=the fixed wording); the feed document states the highest contained level once; all-open feeds carry none. Public API: page representations (list+get) gain the classification field, OpenAPI + public-api.md documented. Search: every hit carries the level and the palette renders the marking with the snippet (compact form of the banner, text token only). No-JS shell: banner above and below the content, own markup for the separate render path; unclassified pages unchanged everywhere. One test per channel (feed categories + count, public API list/get with the switch on, search hit levels, shell top+bottom). Also: fidelity CI sidecars get per-job container names — the fixed names collided across parallel runs on the shared host (run 547's red fidelity job; a fixed-name cleanup could even kill a sibling's live sidecars). Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
133 lines
4.7 KiB
TypeScript
133 lines
4.7 KiB
TypeScript
import { z } from 'zod';
|
|
|
|
import type { ApiTokenScope } from './api-tokens';
|
|
import type { CommentView } from './comments';
|
|
import type { LabelTreeNode, LabelView } from './labels';
|
|
import type { PageClassification } from './pages';
|
|
|
|
/**
|
|
* 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;
|
|
/** VS-NfD level (issue #211, ADR 0022) — see `docs/self-hosting/public-api.md`. */
|
|
classification: PageClassification;
|
|
/** 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;
|
|
/** VS-NfD level (issue #211, ADR 0022) — see `docs/self-hosting/public-api.md`. */
|
|
classification: PageClassification;
|
|
/** 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<typeof publicCreatePageInputSchema>;
|
|
|
|
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<typeof publicUpdatePageInputSchema>;
|
|
|
|
/**
|
|
* 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<typeof publicUpdateLabelInputSchema>;
|
|
|
|
/** An ISO 8601 instant (date or date-time) for `…Since` filters (issue #148). */
|
|
const sinceInstant = z
|
|
.string()
|
|
.trim()
|
|
.regex(/^\d{4}-\d{2}-\d{2}([T ].+)?$/, 'validation.invalid')
|
|
.refine((value) => !Number.isNaN(Date.parse(value)), 'validation.invalid')
|
|
.transform((value) => new Date(value));
|
|
|
|
/** Optional time filters for page listings (issue #148), applied as `>=`. */
|
|
export const pageListQuerySchema = z.object({
|
|
createdSince: sinceInstant.optional(),
|
|
updatedSince: sinceInstant.optional(),
|
|
});
|
|
export type PageListQuery = z.infer<typeof pageListQuerySchema>;
|
|
|
|
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<typeof publicSearchQuerySchema>;
|
|
|
|
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;
|