Product registry: catalogs, revisions, product info and libraries
CI / test (push) Successful in 43s

The hub registers catalogs (hidden until a moderator approves them), hands out upload tokens, and records the
revisions catalogs register after validating uploads: numbered, never changed, signed by the hub (its own
Ed25519 key, published at /.well-known/t5de-keys.json). Creators edit product info (descriptions cleaned by an
allowlist) and can retract revisions. People get products into their library, optionally staying on a
revision; the proxy asks which revision to load and where. Contract in docs/products.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-10-29 18:53:52 -07:00
1 parent 7fea63fc4f
commit b4f6fce9df
15 files changed
+2883 -14

No files matched your search

+51
View File
@@ -0,0 +1,51 @@
// Product descriptions: HTML, as on IMVU, made safe by an allowlist (docs/design.md, "Product info"). Cleaned when
// saved; whatever shows them cleans again.
import sanitize from "sanitize-html";
export const maxDescription = 20_000;
const color = [/^#[0-9a-f]{3,8}$/i, /^[a-z]{3,20}$/i, /^rgba?\(\s*[\d.\s,%]+\)$/i];
const size = [/^\d{1,3}(\.\d+)?(px|pt|em|rem|%)$/, /^(xx-small|x-small|small|medium|large|x-large|xx-large)$/];
const options: sanitize.IOptions = {
allowedTags: [
"b", "i", "u", "s", "em", "strong", "small", "big", "sub", "sup", "font", "span", "div", "p", "br", "hr",
"center", "ul", "ol", "li", "table", "thead", "tbody", "tr", "td", "th", "h1", "h2", "h3", "h4", "h5", "h6",
"a", "img",
],
allowedAttributes: {
a: ["href", "rel", "target"], // rel and target are always set by the transform below
img: ["src", "alt", "width", "height"],
font: ["color", "size"],
td: ["colspan", "rowspan"],
th: ["colspan", "rowspan"],
"*": ["style"],
},
allowedSchemes: ["http", "https"],
allowedSchemesByTag: { img: ["https"] },
allowProtocolRelative: false,
allowedStyles: {
"*": {
color,
"background-color": color,
"font-size": size,
"font-weight": [/^(normal|bold|bolder|lighter|[1-9]00)$/],
"font-style": [/^(normal|italic|oblique)$/],
"text-align": [/^(left|right|center|justify)$/],
"text-decoration": [/^(none|underline|line-through|overline)$/],
},
},
transformTags: {
a: (tagName, attribs) => ({
tagName,
attribs: { ...attribs, rel: "nofollow ugc noopener noreferrer", target: "_blank" },
}),
},
// Images whose address was refused are dropped.
exclusiveFilter: (frame) => frame.tag === "img" && !frame.attribs.src,
disallowedTagsMode: "discard",
};
export function cleanHTML(html: string): string {
return sanitize(html, options).trim();
}