Harden control plane boundaries
CI / verify (pull_request) Canceled after 0s

This commit is contained in:
2026-07-25 10:13:22 -07:00
parent a0a4a21962
commit 72e8b2de91
24 changed files with 1507 additions and 1655 deletions
+128
View File
@@ -0,0 +1,128 @@
import express from "express";
import fs from "node:fs";
import path from "node:path";
import { cachedRepositoryPath, listRepositoryFiles } from "../gitOperations";
import { store } from "../store";
const router = express.Router();
router.get("/style-packages", (_req, res) => {
res.json(store.stylePackages);
});
router.get("/style-instances", (_req, res) => {
res.json(store.styleInstances);
});
router.post("/style-instances", (req, res) => {
const instance = req.body;
store.styleInstances.push(instance);
store.addAudit(
"Add Style Instance",
"config",
`Created style instance '${instance.id}' using '${instance.uses}'`,
);
res.json({ success: true, instance });
});
router.put("/style-instances/:id", (req, res) => {
const index = store.styleInstances.findIndex(
(instance) => instance.id === req.params.id,
);
if (index === -1) return res.status(404).json({ error: "Instance not found" });
store.styleInstances[index] = { ...store.styleInstances[index], ...req.body };
store.addAudit(
"Update Style Instance",
"config",
`Updated style instance '${req.params.id}'`,
);
return res.json({ success: true, instance: store.styleInstances[index] });
});
router.get("/content", (_req, res) => {
res.json(store.contentItems);
});
router.post("/content/discover", (_req, res) => {
store.addAudit(
"Discover Content",
"config",
"Executed content discovery scan across all Git sources",
);
const discovered = [];
for (const source of store.gitSources) {
if (source.health !== "Healthy" || !source.projectionEnabled) continue;
let root: string;
try {
root = cachedRepositoryPath(source.id);
} catch {
continue;
}
if (!fs.existsSync(root)) continue;
const markdownFiles = listRepositoryFiles(
source.id,
(relative) => relative.toLowerCase().endsWith(".md"),
10_000,
);
for (const [index, file] of markdownFiles.entries()) {
const title = path.posix.basename(file.path, ".md").trim() || "Untitled";
const declaredModels = source.produces || [];
const defaultModel = store.siteConfig.contentModels[0]?.id ?? "page";
const artifactType = declaredModels[0] ?? defaultModel;
const slug = title.toLowerCase();
const model = store.siteConfig.contentModels.find(
(candidate) => candidate.id === artifactType,
);
const navigation = store.siteConfig.navigation.find(
(entry) => entry.contentModel === artifactType,
);
const routePattern =
model?.routing?.detail ??
(navigation
? `${navigation.route}/{slug}/`
: `/${artifactType}s/{slug}/`);
discovered.push({
id: `art-${source.id}-${index}`,
title: title
.replace(/-/g, " ")
.replace(/\b\w/g, (letter) => letter.toUpperCase()),
slug,
published: new Date().toISOString(),
status: "published" as const,
artifactType,
summary: `Discovered artifact from ${file.path}`,
tags: [source.id],
aliases: [],
sourceRepo: source.id,
path: file.path,
contentMarkdown: fs.readFileSync(
path.join(root, ...file.path.split("/")),
"utf8",
),
mediaReferences: [],
youtubeDirectives: [],
wikipediaLinks: [],
validationStatus: "valid" as const,
validationMessages: [],
route: routePattern.replace("{slug}", slug).replace("//", "/"),
styleInstanceId: "default",
});
}
}
store.contentItems = discovered;
res.json({
success: true,
discoveredCount: discovered.length,
mediaCount: store.mediaAssets.length,
items: discovered,
});
});
router.get("/media", (_req, res) => {
res.json(store.mediaAssets);
});
export { router as contentRouter };
+124
View File
@@ -0,0 +1,124 @@
import express from "express";
import { CredentialRef } from "../../types";
import { store } from "../store";
const router = express.Router();
router.get("/health", (_req, res) => {
res.json({ status: "ok", service: "labyricorn-site-builder" });
});
router.get("/site-config", (_req, res) => {
res.json(store.siteConfig);
});
router.put("/site-config", (_req, res) => {
res.status(405).json({
code: "E_CONFIG_READ_ONLY",
message: "Site configuration is Git-owned and read-only in v1.",
});
});
router.get("/theme", (_req, res) => {
res.json(store.themeConfig);
});
router.post("/theme/validate", (_req, res) => {
const theme = store.validateTheme();
res.status(theme.status === "valid" ? 200 : 422).json({
success: theme.status === "valid",
theme,
commits: theme.commit ? { [theme.sourceId ?? "theme"]: theme.commit } : {},
});
});
router.get("/builds", (_req, res) => {
res.json(store.builds);
});
router.post("/builds/trigger", (_req, res) => {
res.json(store.runBuild());
});
router.get("/nginx", (_req, res) => {
res.json(store.nginxStatus);
});
router.post("/nginx/test", (_req, res) => {
store.nginxStatus.configValid = true;
store.addAudit(
"Test Nginx Config",
"nginx",
"Control-plane simulation reported a valid Nginx configuration.",
);
res.json({
success: true,
simulated: true,
message: "Nginx validation is simulated by the control plane.",
});
});
router.post("/nginx/activate", (req, res) => {
res.json(store.activateReleaseLocally(req.body.buildId));
});
router.get("/push-targets", (_req, res) => {
res.json(store.pushTargets);
});
router.post("/push-targets/:id/push", (req, res) => {
const result = store.pushToTarget(req.params.id, req.body.buildId);
res.status(result.success ? 200 : 501).json(result);
});
router.post("/push-targets/:id/test", (_req, res) => {
res.status(501).json({
success: false,
code: "E_REMOTE_PUBLICATION_NOT_IMPLEMENTED",
message: "Remote SSH and rsync checks are not implemented.",
});
});
router.get("/credentials", (_req, res) => {
res.json(store.credentials);
});
router.post("/credentials", (req, res) => {
const { name, type, secretValue } = req.body;
if (
typeof name !== "string" ||
!["ssh_key", "token", "password"].includes(type) ||
typeof secretValue !== "string"
) {
return res.status(400).json({
error: "name, type, and secretValue are required.",
});
}
const id = `secret:${name.toLowerCase().replace(/[^a-z0-9]/g, "-")}`;
const masked =
type === "ssh_key"
? `${secretValue.substring(0, 20)}...`
: `${secretValue.substring(0, 4)}********************`;
const credential: CredentialRef = {
id,
name,
type: type as CredentialRef["type"],
maskedValue: masked,
createdAt: new Date().toISOString(),
lastUsedAt: new Date().toISOString(),
usageCount: 0,
};
store.credentials.push(credential);
store.addAudit(
"Add Credential",
"credential",
`Stored credential reference ${id}`,
);
return res.json({ success: true, credential });
});
router.get("/audit-logs", (_req, res) => {
res.json(store.auditLogs);
});
export { router as controlRouter };
+230
View File
@@ -0,0 +1,230 @@
import express from "express";
import fs from "node:fs";
import { GitSource } from "../../types";
import {
cachedRepositoryPath,
listRepositoryFiles,
resolveRemote,
synchronizeRepository,
validateRepository,
} from "../gitOperations";
import { store } from "../store";
import {
fetchGiteaRepositories,
FixedWindowRateLimiter,
loadGiteaDiscoveryConfig,
toGitSource,
} from "./giteaDiscovery";
const router = express.Router();
const discoveryLimiter = new FixedWindowRateLimiter();
let discoveryInFlight: Promise<ReturnType<typeof toGitSource>[]> | null = null;
const errorMessage = (error: unknown): string =>
error instanceof Error ? error.message : String(error);
router.post("/gitea/discover", async (req, res) => {
const limit = discoveryLimiter.take(req.ip || req.socket.remoteAddress || "unknown");
if (!limit.allowed) {
res.setHeader("Retry-After", String(limit.retryAfter));
return res.status(429).json({
code: "E_GITEA_DISCOVERY_RATE_LIMIT",
error: "Gitea discovery rate limit exceeded.",
success: false,
});
}
let config: ReturnType<typeof loadGiteaDiscoveryConfig>;
try {
config = loadGiteaDiscoveryConfig();
} catch (error) {
return res.status(503).json({
code: "E_GITEA_DISCOVERY_CONFIG",
error: errorMessage(error),
success: false,
});
}
store.addAudit(
"Gitea Discovery",
"config",
`Initiating repository discovery from ${config.apiUrl.origin}`,
);
try {
discoveryInFlight ??= fetchGiteaRepositories(config)
.then((repositories) => repositories.map(toGitSource))
.finally(() => {
discoveryInFlight = null;
});
const discovered = await discoveryInFlight;
const added = store.mergeDiscoveredGitSources(discovered);
return res.json({
success: true,
discoveredCount: discovered.length,
addedCount: added,
message: `Successfully discovered ${discovered.length} repositories from Gitea.`,
});
} catch (error) {
const message = errorMessage(error);
store.addAudit("Gitea Discovery Failed", "config", message, "failure");
return res.status(502).json({ error: message, success: false });
}
});
router.get("/git-sources", (_req, res) => {
res.json(store.gitSources);
});
router.post("/git-sources", async (req, res) => {
const body = req.body as Partial<GitSource>;
if (
typeof body.id !== "string" ||
typeof body.repository !== "string" ||
typeof body.ref !== "string"
) {
return res.status(400).json({ error: "id, repository, and ref are required." });
}
try {
validateRepository(body.repository);
} catch (error) {
return res.status(400).json({ error: errorMessage(error) });
}
const source: GitSource = {
id: body.id,
repository: body.repository,
ref: body.ref,
branch: body.branch || body.ref,
type: body.type || "hybrid",
required: body.required ?? false,
credential: body.credential,
health: "Warning",
reachability: "Unreachable",
configurationState: "Not Configured",
visibility: "Private in Gitea",
projectionEnabled: false,
produces: [],
explainWhy: "Awaiting initial synchronization.",
status: "syncing",
};
try {
const commit = await resolveRemote(source.repository, source.ref);
if (commit) {
source.status = "connected";
source.reachability = "Healthy";
source.lastResolvedCommit = commit;
source.lastSyncedAt = new Date().toISOString();
source.explainWhy =
"Connected to repository, but missing .labyricorn/site.yml metadata.";
} else {
source.status = "error";
source.explainWhy = "Ref not found in repository.";
}
} catch (error) {
source.status = "error";
source.explainWhy = `Git ls-remote failed: ${errorMessage(error)}`;
}
store.gitSources.push(source);
store.addAudit(
"Add Git Source",
"source",
`Added Git source ${source.id} (${source.repository})`,
);
return res.json({ success: true, source });
});
router.put("/git-sources/:id", (req, res) => {
const index = store.gitSources.findIndex((source) => source.id === req.params.id);
if (index === -1) return res.status(404).json({ error: "Source not found" });
store.gitSources[index] = { ...store.gitSources[index], ...req.body };
store.addAudit(
"Update Git Source",
"source",
`Updated Git source ${req.params.id}`,
);
return res.json({ success: true, source: store.gitSources[index] });
});
router.delete("/git-sources/:id", (req, res) => {
store.gitSources = store.gitSources.filter(
(source) => source.id !== req.params.id,
);
store.addAudit(
"Delete Git Source",
"source",
`Removed Git source ${req.params.id}`,
);
res.json({ success: true });
});
router.post("/git-sources/:id/test", async (req, res) => {
const source = store.gitSources.find((item) => item.id === req.params.id);
if (!source) return res.status(404).json({ error: "Source not found" });
source.status = "syncing";
source.explainWhy = "Synchronization running...";
try {
const commit = await resolveRemote(source.repository, source.ref || "HEAD");
if (!commit) {
source.status = "error";
source.explainWhy = "Ref not found in repository.";
return res.status(400).json({ error: source.explainWhy });
}
source.status = "connected";
source.reachability = "Healthy";
source.lastResolvedCommit = commit;
source.lastSyncedAt = new Date().toISOString();
const target = await synchronizeRepository(
source.id,
source.repository,
source.ref || "main",
);
if (fs.existsSync(`${target}/.labyricorn/site.yml`)) {
source.configurationState = "Valid";
source.projectionEnabled = true;
source.explainWhy =
"Repository synchronized and valid site.yml detected.";
source.health = "Healthy";
} else {
source.configurationState = "Not Configured";
source.projectionEnabled = false;
source.explainWhy =
"Repository synchronized, but missing .labyricorn/site.yml metadata.";
source.health = "Warning";
}
return res.json({
success: true,
message: `Successfully connected and synchronized ${source.repository}!`,
resolvedCommit: source.lastResolvedCommit,
syncedAt: source.lastSyncedAt,
});
} catch (error) {
source.status = "error";
source.health = "Invalid";
source.explainWhy = `Git sync failed: ${errorMessage(error)}`;
return res.status(400).json({ error: source.explainWhy });
}
});
router.get("/git-sources/:id/tree", (req, res) => {
if (!store.gitSources.some((source) => source.id === req.params.id)) {
return res.status(404).json({ error: "Source not found" });
}
try {
cachedRepositoryPath(req.params.id);
const files = listRepositoryFiles(req.params.id).map((file) => ({
path: file.path,
type: "file",
size: `${file.sizeBytes} B`,
}));
return res.json({ sourceId: req.params.id, files });
} catch {
return res.json({ sourceId: req.params.id, files: [] });
}
});
export { router as gitSourceRouter };
+198
View File
@@ -0,0 +1,198 @@
import { GitSource } from "../../types";
const DEFAULT_TIMEOUT_MS = 5_000;
const DEFAULT_RATE_LIMIT = 5;
const DEFAULT_RATE_WINDOW_MS = 60_000;
export interface GiteaDiscoveryConfig {
apiUrl: URL;
token: string;
timeoutMs: number;
}
interface GiteaRepository {
clone_url?: unknown;
default_branch?: unknown;
full_name?: unknown;
name?: unknown;
private?: unknown;
ssh_url?: unknown;
}
export interface DiscoveredRepository {
cloneUrl: string;
defaultBranch: string;
fullName: string;
name: string;
private: boolean;
sshUrl?: string;
}
const positiveInteger = (value: string | undefined, fallback: number): number => {
if (value === undefined) return fallback;
const parsed = Number.parseInt(value, 10);
if (!Number.isInteger(parsed) || parsed < 1) {
throw new Error(`Invalid positive integer configuration value '${value}'.`);
}
return parsed;
};
const allowedOrigins = (value: string | undefined): Set<string> =>
new Set(
(value ?? "")
.split(",")
.map((entry) => entry.trim())
.filter(Boolean)
.map((entry) => new URL(entry).origin),
);
export const loadGiteaDiscoveryConfig = (
env: NodeJS.ProcessEnv = process.env,
): GiteaDiscoveryConfig => {
const token = env.GITEA_TOKEN?.trim();
const baseUrl = env.GITEA_BASE_URL?.trim();
if (!token || !baseUrl) {
throw new Error("Gitea discovery requires GITEA_TOKEN and GITEA_BASE_URL.");
}
const base = new URL(baseUrl);
if (base.protocol !== "https:" || base.username || base.password) {
throw new Error("GITEA_BASE_URL must be an HTTPS origin without credentials.");
}
const allowlist = allowedOrigins(env.GITEA_ALLOWED_ORIGINS);
if (!allowlist.has(base.origin)) {
throw new Error(
`GITEA_BASE_URL origin '${base.origin}' is not in GITEA_ALLOWED_ORIGINS.`,
);
}
const apiUrl = new URL("/api/v1/repos/search", base);
apiUrl.searchParams.set("limit", "50");
return {
apiUrl,
token,
timeoutMs: positiveInteger(
env.GITEA_DISCOVERY_TIMEOUT_MS,
DEFAULT_TIMEOUT_MS,
),
};
};
const repository = (value: GiteaRepository): DiscoveredRepository | null => {
if (
typeof value.name !== "string" ||
typeof value.clone_url !== "string" ||
value.name.length === 0 ||
value.clone_url.length === 0
) {
return null;
}
const fullName =
typeof value.full_name === "string" && value.full_name.length > 0
? value.full_name
: value.name;
return {
cloneUrl: value.clone_url,
defaultBranch:
typeof value.default_branch === "string" && value.default_branch.length > 0
? value.default_branch
: "main",
fullName,
name: value.name,
private: value.private === true,
sshUrl: typeof value.ssh_url === "string" ? value.ssh_url : undefined,
};
};
export const fetchGiteaRepositories = async (
config: GiteaDiscoveryConfig,
fetchImplementation: typeof fetch = fetch,
): Promise<DiscoveredRepository[]> => {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), config.timeoutMs);
try {
const response = await fetchImplementation(config.apiUrl, {
headers: {
Accept: "application/json",
Authorization: `token ${config.token}`,
},
redirect: "error",
signal: controller.signal,
});
if (!response.ok) throw new Error(`Gitea returned HTTP ${response.status}.`);
const document = (await response.json()) as { data?: unknown };
if (!Array.isArray(document.data)) {
throw new Error("Gitea returned an invalid repository search response.");
}
return document.data
.map((entry) => repository(entry as GiteaRepository))
.filter((entry): entry is DiscoveredRepository => entry !== null);
} catch (error) {
if (controller.signal.aborted) {
throw new Error(`Gitea discovery timed out after ${config.timeoutMs}ms.`);
}
throw error;
} finally {
clearTimeout(timeout);
}
};
const sourceId = (repo: DiscoveredRepository): string =>
repo.fullName
.toLowerCase()
.replace(/[^a-z0-9._-]+/g, "--")
.replace(/^-+|-+$/g, "")
.slice(0, 128) || repo.name.toLowerCase();
export const toGitSource = (repo: DiscoveredRepository): GitSource => ({
id: sourceId(repo),
repository: repo.cloneUrl,
ref: repo.defaultBranch,
branch: repo.defaultBranch,
type: "hybrid",
health: "Warning",
reachability: "Unreachable",
configurationState: "Not Configured",
visibility: repo.private ? "Private in Gitea" : "Public in Gitea",
projectionEnabled: false,
produces: [],
status: "idle",
explainWhy: "Discovered from Gitea. Awaiting initial synchronization.",
required: false,
credential: "none",
});
export class FixedWindowRateLimiter {
private readonly clients = new Map<
string,
{ count: number; startedAt: number }
>();
constructor(
private readonly maximum = positiveInteger(
process.env.GITEA_DISCOVERY_RATE_LIMIT,
DEFAULT_RATE_LIMIT,
),
private readonly windowMs = positiveInteger(
process.env.GITEA_DISCOVERY_RATE_WINDOW_MS,
DEFAULT_RATE_WINDOW_MS,
),
) {}
take(client: string, now = Date.now()): { allowed: boolean; retryAfter: number } {
const current = this.clients.get(client);
if (!current || now - current.startedAt >= this.windowMs) {
this.clients.set(client, { count: 1, startedAt: now });
return { allowed: true, retryAfter: 0 };
}
if (current.count >= this.maximum) {
return {
allowed: false,
retryAfter: Math.ceil((this.windowMs - (now - current.startedAt)) / 1_000),
};
}
current.count += 1;
return { allowed: true, retryAfter: 0 };
}
}
+58
View File
@@ -0,0 +1,58 @@
import express from "express";
import fs from "node:fs";
import path from "node:path";
import { store } from "../store";
const router = express.Router();
router.get("/live-site/html", (req, res) => {
const rootDir = store.getPreviewDirectory();
if (!rootDir) {
return res.status(503).json({
code: "E_RELEASE_UNAVAILABLE",
message: "No verified staging or live release is selected.",
});
}
let requested: string;
try {
requested = decodeURIComponent(
String(req.query.route ?? "/").split(/[?#]/, 1)[0],
);
} catch {
return res.status(400).json({ code: "E_ROUTE_INVALID" });
}
if (
!requested.startsWith("/") ||
requested.includes("\\") ||
requested.split("/").includes("..")
) {
return res.status(400).json({ code: "E_ROUTE_INVALID" });
}
const relative = requested.replace(/^\/+/, "");
const outputFile =
relative && path.posix.extname(relative)
? relative
: path.posix.join(relative, "index.html");
const rootPath = `${path.resolve(rootDir)}${path.sep}`;
const target = path.resolve(rootDir, ...outputFile.split("/"));
if (!target.startsWith(rootPath)) {
return res.status(400).json({ code: "E_ROUTE_INVALID" });
}
res.setHeader(
"Content-Security-Policy",
"default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'self'",
);
if (fs.existsSync(target) && fs.statSync(target).isFile()) {
return res.type(path.extname(target)).send(fs.readFileSync(target));
}
const notFound = path.join(rootDir, "404.html");
if (fs.existsSync(notFound)) {
return res.status(404).type("html").send(fs.readFileSync(notFound));
}
return res.status(404).json({ code: "E_ROUTE_NOT_FOUND" });
});
export { router as previewRouter };