Product registry: catalogs, revisions, product info and libraries
CI / test (push) Successful in 43s
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:
1 parent
7fea63fc4f
commit
b4f6fce9df
15 files changed
+2883
-14
No files matched your search
@@ -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();
|
||||
}
|
||||
Reference in new issue
Block a user