Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
import { getAuth } from "@/lib/community/server/auth";
import { originBlocked, preflight, withCors } from "@/lib/community/cors";
import { communityEnabled, getDb } from "@/lib/community/server/db";
import { MAX_FEED_PAGE_SIZE } from "@/lib/community/feedView";
import { notifyForkPublished } from "@/lib/community/server/notify";
import {
countFeed,
getPatternStub,
likedPatternIds,
listFeed,
newId,
parseFeedSort,
} from "@/lib/community/server/queries";
import { rateLimit } from "@/lib/community/ratelimit";
import { patterns } from "@/lib/community/server/schema";
import { toCardItem } from "@/lib/community/server/serialize";
import {
CODE_MAX,
cleanCode,
cleanCpp,
cleanDescription,
cleanMadeHow,
cleanTitle,
} from "@/lib/community/validate";
import { cleanVisibility, forkBlocked } from "@/lib/community/visibility";
import { buildStoredPatternCode, lineageFrom } from "@/lib/community/license";
import { LICENSE_OPTIONS, forkLicenseAllowed, stripShareWrapping } from "@/lib/pattern/share";
// POST /api/community/patterns — publish (or fork-publish) a pattern.
// GET /api/community/patterns — one batch of the feed, for infinite scroll.
//
// Reads otherwise happen in server components; this GET is the one exception,
// because "load more as you scroll" is a client act by nature. Same filters
// and ordering as the server-rendered first paint — it reads through the very
// same listFeed, so the visibility rules cannot drift apart.
//
// Callable from the main site's Pattern Lab, which is a different origin, so
// every response carries CORS headers and OPTIONS answers the preflight.
export async function POST(request: Request) {
const blocked = originBlocked(request);
if (blocked) return blocked;
return withCors(request, await handlePost(request));
}
export async function GET(request: Request) {
const blocked = originBlocked(request);
if (blocked) return blocked;
return withCors(request, await handleGet(request));
}
export const OPTIONS = preflight;
function clampInt(raw: string | null, fallback: number, min: number, max: number) {
const value = Number(raw);
if (!Number.isFinite(value)) return fallback;
return Math.max(min, Math.min(max, Math.floor(value)));
}
async function handleGet(request: Request) {
if (!communityEnabled()) {
return Response.json({ error: "Community is not enabled on this deployment." }, { status: 503 });
}
const params = new URL(request.url).searchParams;
const sort = parseFeedSort(params.get("sort") ?? undefined);
const hardwareOnly = params.get("hw") === "1";
const offset = clampInt(params.get("offset"), 0, 0, 1_000_000);
const size = clampInt(params.get("size"), 12, 1, MAX_FEED_PAGE_SIZE);
// The infinite scroll refills through here, so every page needs the same
// viewer the first paint had: `liked` for the subset itself, and the card
// hearts on any sort — without it page two would arrive unlit.
const session = await getAuth().api.getSession({ headers: request.headers });
const viewerId = session?.user.id ?? null;
const [items, total] = await Promise.all([
listFeed({ sort, hardwareOnly, limit: size, offset, viewerId }),
countFeed(hardwareOnly),
]);
const likedIds = await likedPatternIds(viewerId, items.map((item) => item.id));
return Response.json({ items: items.map((item) => toCardItem(item, likedIds)), total });
}
async function handlePost(request: Request) {
if (!communityEnabled()) {
return Response.json({ error: "Community is not enabled on this deployment." }, { status: 503 });
}
const session = await getAuth().api.getSession({ headers: request.headers });
if (!session) {
return Response.json({ error: "Sign in to share a pattern." }, { status: 401 });
}
if (!rateLimit(`publish:${session.user.id}`, 5, 60_000)) {
return Response.json({ error: "Too many uploads — wait a minute and try again." }, { status: 429 });
}
let body: unknown;
try {
body = await request.json();
} catch {
return Response.json({ error: "Invalid JSON body." }, { status: 400 });
}
const raw = body as Record<string, unknown>;
const title = cleanTitle(raw.title);
if (!title) return Response.json({ error: "Title is required (max 80 chars)." }, { status: 400 });
const description = cleanDescription(raw.description);
if (description === undefined) {
return Response.json({ error: "Description is too long (max 2000 chars)." }, { status: 400 });
}
// Measured with the licence wrapping off: that is what gets stored, and a
// pattern accepted just under the cap came back from a download or a fork
// a few hundred characters over it, unchanged and refused.
const code = cleanCode(
typeof raw.code === "string" && raw.code.length <= CODE_MAX * 2 ? stripShareWrapping(raw.code) : raw.code,
);
if (!code) return Response.json({ error: "Pattern code is missing or over 100KB." }, { status: 400 });
// Any licence block the code arrived with belongs to someone else (a lab
// preset's header, a Gemini stamp, a previously exported file), so drop it
// before we write our own.
if (stripShareWrapping(code).length === 0) {
return Response.json({ error: "Pattern code is empty once the licence header is removed." }, { status: 400 });
}
// A pattern may arrive already ported. Pattern Lab's hardware flow converts
// to a header, then offers to publish — making people publish first and
// attach the header afterwards would lose the port half the time.
const codeCpp = cleanCpp(raw.codeCpp);
if (codeCpp === undefined) {
return Response.json(
{ error: "That does not look like a Patternflow header — it must start with `#pragma once` and be under 200KB." },
{ status: 400 },
);
}
const madeHow = cleanMadeHow(raw.madeHow);
if (madeHow === undefined) {
return Response.json({ error: "Unknown \"made how\" value." }, { status: 400 });
}
// Absent means public — the community works because things are shared. The
// quieter states are a choice made at the picker, not a silent default.
const visibility = raw.visibility === undefined ? "public" : cleanVisibility(raw.visibility);
if (!visibility) {
return Response.json({ error: "Unknown visibility value." }, { status: 400 });
}
// New work can only take a currently-offered licence (the retired ones stay
// readable but are not selectable — see LICENSE_OPTIONS).
const license =
LICENSE_OPTIONS.find((option) => option.spdx === raw.license)?.spdx ?? "CC-BY-SA-4.0";
// Fork lineage: only record parents that actually exist; a dangling or
// malformed parentId silently degrades to an original post.
let parentId: string | null = null;
let parent: Awaited<ReturnType<typeof getPatternStub>> | null = null;
if (typeof raw.parentId === "string" && raw.parentId.length > 0) {
parent = await getPatternStub(raw.parentId);
parentId = parent?.id ?? null;
}
// A fork of a private pattern would bake a credit link that 404s for every
// reader (#255). The author forking their own private work is fine.
if (parent && forkBlocked(parent, session.user.id)) {
return Response.json(
{ error: "That pattern is private — it cannot be forked." },
{ status: 400 },
);
}
// A fork is a derivative: it cannot be published under looser terms than the
// pattern it came from. Without this a CC BY-SA pattern could be forked and
// re-published as something permissive, with the platform doing the laundering.
if (parent && !forkLicenseAllowed(parent.license, license)) {
return Response.json(
{
error: `A fork of a ${parent.license} pattern cannot be published as ${license}. Publish it under ${parent.license}.`,
},
{ status: 400 },
);
}
const now = new Date();
const id = newId();
const handle =
(session.user as { username?: string | null; displayUsername?: string | null }).displayUsername ??
(session.user as { username?: string | null }).username ??
null;
await getDb().insert(patterns).values({
id,
userId: session.user.id,
title,
description,
// Licence header + attribution are baked into the stored source, so anyone
// who copies the code out of the page takes the terms and the credit with
// it — not just people who download the file. On a fork that includes the
// upstream credit, which both CC licences require a derivative to keep.
code: buildStoredPatternCode(code, {
title,
license,
handle,
date: now,
basedOn: lineageFrom(parent),
}),
codeCpp,
license,
madeHow,
parentId,
visibility,
createdAt: now,
updatedAt: now,
});
if (parent) {
await notifyForkPublished({
parentOwnerId: parent.userId,
parentTitle: parent.title,
forkId: id,
forkVisibility: visibility,
actorId: session.user.id,
});
}
return Response.json({ id }, { status: 201 });
}
e5b871ed5This file is pinned to the selected release. Download the original to open it in your design tool.