@plasius/images 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -0
- package/CODE_OF_CONDUCT.md +79 -0
- package/CONTRIBUTORS.md +27 -0
- package/LICENSE +21 -0
- package/README.md +43 -0
- package/SECURITY.md +17 -0
- package/dist/detectFormat.d.ts +2 -0
- package/dist/detectFormat.d.ts.map +1 -0
- package/dist/detectFormat.js +20 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/parse/index.d.ts +5 -0
- package/dist/parse/index.d.ts.map +1 -0
- package/dist/parse/index.js +4 -0
- package/dist/parse/parseJpeg.d.ts +5 -0
- package/dist/parse/parseJpeg.d.ts.map +1 -0
- package/dist/parse/parseJpeg.js +18 -0
- package/dist/parse/parsePng.d.ts +5 -0
- package/dist/parse/parsePng.d.ts.map +1 -0
- package/dist/parse/parsePng.js +6 -0
- package/dist/parse/parseSVG.d.ts +5 -0
- package/dist/parse/parseSVG.d.ts.map +1 -0
- package/dist/parse/parseSVG.js +23 -0
- package/dist/parse/parseWebp.d.ts +5 -0
- package/dist/parse/parseWebp.d.ts.map +1 -0
- package/dist/parse/parseWebp.js +17 -0
- package/dist/validators/avatarValidator.d.ts +9 -0
- package/dist/validators/avatarValidator.d.ts.map +1 -0
- package/dist/validators/avatarValidator.js +51 -0
- package/dist/validators/index.d.ts +3 -0
- package/dist/validators/index.d.ts.map +1 -0
- package/dist/validators/index.js +2 -0
- package/dist/validators/svgValidator.d.ts +18 -0
- package/dist/validators/svgValidator.d.ts.map +1 -0
- package/dist/validators/svgValidator.js +139 -0
- package/dist-cjs/detectFormat.d.ts +2 -0
- package/dist-cjs/detectFormat.d.ts.map +1 -0
- package/dist-cjs/detectFormat.js +23 -0
- package/dist-cjs/index.d.ts +2 -0
- package/dist-cjs/index.d.ts.map +1 -0
- package/dist-cjs/index.js +17 -0
- package/dist-cjs/parse/index.d.ts +5 -0
- package/dist-cjs/parse/index.d.ts.map +1 -0
- package/dist-cjs/parse/index.js +20 -0
- package/dist-cjs/parse/parseJpeg.d.ts +5 -0
- package/dist-cjs/parse/parseJpeg.d.ts.map +1 -0
- package/dist-cjs/parse/parseJpeg.js +21 -0
- package/dist-cjs/parse/parsePng.d.ts +5 -0
- package/dist-cjs/parse/parsePng.d.ts.map +1 -0
- package/dist-cjs/parse/parsePng.js +9 -0
- package/dist-cjs/parse/parseSVG.d.ts +5 -0
- package/dist-cjs/parse/parseSVG.d.ts.map +1 -0
- package/dist-cjs/parse/parseSVG.js +26 -0
- package/dist-cjs/parse/parseWebp.d.ts +5 -0
- package/dist-cjs/parse/parseWebp.d.ts.map +1 -0
- package/dist-cjs/parse/parseWebp.js +20 -0
- package/dist-cjs/validators/avatarValidator.d.ts +9 -0
- package/dist-cjs/validators/avatarValidator.d.ts.map +1 -0
- package/dist-cjs/validators/avatarValidator.js +57 -0
- package/dist-cjs/validators/index.d.ts +3 -0
- package/dist-cjs/validators/index.d.ts.map +1 -0
- package/dist-cjs/validators/index.js +18 -0
- package/dist-cjs/validators/svgValidator.d.ts +18 -0
- package/dist-cjs/validators/svgValidator.d.ts.map +1 -0
- package/dist-cjs/validators/svgValidator.js +143 -0
- package/docs/adrs/adr-0001-images-package-scope.md +21 -0
- package/docs/adrs/adr-0002-public-repo-governance.md +24 -0
- package/docs/adrs/adr-template.md +35 -0
- package/legal/CLA-REGISTRY.csv +1 -0
- package/legal/CLA.md +22 -0
- package/legal/CORPORATE_CLA.md +57 -0
- package/legal/INDIVIDUAL_CLA.md +91 -0
- package/package.json +102 -0
- package/src/detectFormat.ts +28 -0
- package/src/index.ts +1 -0
- package/src/parse/index.ts +4 -0
- package/src/parse/parseJpeg.ts +22 -0
- package/src/parse/parsePng.ts +9 -0
- package/src/parse/parseSVG.ts +30 -0
- package/src/parse/parseWebp.ts +19 -0
- package/src/validators/avatarValidator.ts +72 -0
- package/src/validators/index.ts +2 -0
- package/src/validators/svgValidator.ts +182 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { ImageValidationResult } from "./avatarValidator.js";
|
|
2
|
+
import { DOMParser, XMLSerializer } from "@xmldom/xmldom";
|
|
3
|
+
import type { Element as XmlElement } from "@xmldom/xmldom";
|
|
4
|
+
|
|
5
|
+
export function sanitiseSVG(
|
|
6
|
+
svgText: string,
|
|
7
|
+
options: {
|
|
8
|
+
allowedTags: string[];
|
|
9
|
+
allowedAttributes: { "*": string[] };
|
|
10
|
+
allowComments: boolean;
|
|
11
|
+
allowUnknownElements: boolean;
|
|
12
|
+
allowUnknownAttributes: boolean;
|
|
13
|
+
}
|
|
14
|
+
): {
|
|
15
|
+
svg: string;
|
|
16
|
+
audit: { strippedTags: string[]; strippedAttributes: string[] };
|
|
17
|
+
} {
|
|
18
|
+
const strippedTags: string[] = [];
|
|
19
|
+
const strippedAttributes: string[] = [];
|
|
20
|
+
|
|
21
|
+
const parser = new DOMParser({
|
|
22
|
+
onError: (
|
|
23
|
+
level: "warning" | "error" | "fatalError",
|
|
24
|
+
msg: string
|
|
25
|
+
): void => {
|
|
26
|
+
switch (level) {
|
|
27
|
+
case "warning":
|
|
28
|
+
break;
|
|
29
|
+
default:
|
|
30
|
+
throw new Error(msg);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
const serializer = new XMLSerializer();
|
|
35
|
+
const doc = parser.parseFromString(svgText, "image/svg+xml");
|
|
36
|
+
|
|
37
|
+
function cleanNode(node: Node | null) {
|
|
38
|
+
// TODO: optionally sanitize <style> elements and inline style attributes
|
|
39
|
+
|
|
40
|
+
if (node === null) return;
|
|
41
|
+
|
|
42
|
+
if (node.nodeType === 8 /* Comment */ && !options.allowComments) {
|
|
43
|
+
node.parentNode?.removeChild(node);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (node.nodeType === 1 /* Element */) {
|
|
48
|
+
const el = node as unknown as XmlElement;
|
|
49
|
+
const tagName = el.tagName.toLowerCase();
|
|
50
|
+
|
|
51
|
+
// Strip <style> elements entirely
|
|
52
|
+
if (tagName === "style") {
|
|
53
|
+
strippedTags.push(tagName);
|
|
54
|
+
node.parentNode?.removeChild(node);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (
|
|
59
|
+
!options.allowedTags.includes(tagName) &&
|
|
60
|
+
!options.allowUnknownElements
|
|
61
|
+
) {
|
|
62
|
+
strippedTags.push(tagName);
|
|
63
|
+
node.parentNode?.removeChild(node);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Clean attributes
|
|
68
|
+
const allowedAttrs = options.allowedAttributes["*"] || [];
|
|
69
|
+
|
|
70
|
+
const toRemove: string[] = [];
|
|
71
|
+
for (const attr of Array.from(el.attributes)) {
|
|
72
|
+
const attrName = attr.name.toLowerCase();
|
|
73
|
+
|
|
74
|
+
if (attrName.startsWith("on")) {
|
|
75
|
+
toRemove.push(attr.name);
|
|
76
|
+
strippedAttributes.push(`${tagName}.${attr.name}`);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (attrName === "xlink:href" || attrName === "xmlns:xlink") {
|
|
80
|
+
toRemove.push(attr.name);
|
|
81
|
+
strippedAttributes.push(`${tagName}.${attr.name}`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (attrName === "style") {
|
|
85
|
+
toRemove.push(attr.name);
|
|
86
|
+
strippedAttributes.push(`${tagName}.${attr.name}`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (
|
|
91
|
+
!allowedAttrs.includes(attr.name) &&
|
|
92
|
+
!options.allowUnknownAttributes
|
|
93
|
+
) {
|
|
94
|
+
toRemove.push(attr.name);
|
|
95
|
+
strippedAttributes.push(`${tagName}.${attr.name}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
toRemove.forEach((attrName) => el.removeAttribute(attrName));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Recursively clean children
|
|
102
|
+
const children = Array.from(node.childNodes);
|
|
103
|
+
children.forEach((child) => cleanNode(child));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
cleanNode(doc.documentElement as unknown as Node);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
svg: serializer.serializeToString(doc),
|
|
110
|
+
audit: { strippedTags, strippedAttributes },
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function validateSvgAvatar(
|
|
115
|
+
buffer: Buffer
|
|
116
|
+
): Promise<ImageValidationResult> {
|
|
117
|
+
const svgText = buffer.toString("utf8");
|
|
118
|
+
|
|
119
|
+
const { svg: cleanSvg } = sanitiseSVG(svgText, {
|
|
120
|
+
allowedTags: [
|
|
121
|
+
"svg",
|
|
122
|
+
"g",
|
|
123
|
+
"rect",
|
|
124
|
+
"circle",
|
|
125
|
+
"ellipse",
|
|
126
|
+
"line",
|
|
127
|
+
"polygon",
|
|
128
|
+
"polyline",
|
|
129
|
+
"path",
|
|
130
|
+
"text",
|
|
131
|
+
"title",
|
|
132
|
+
"desc",
|
|
133
|
+
],
|
|
134
|
+
allowedAttributes: {
|
|
135
|
+
"*": [
|
|
136
|
+
"width",
|
|
137
|
+
"height",
|
|
138
|
+
"viewBox",
|
|
139
|
+
"xmlns",
|
|
140
|
+
"transform",
|
|
141
|
+
"x",
|
|
142
|
+
"y",
|
|
143
|
+
"cx",
|
|
144
|
+
"cy",
|
|
145
|
+
"r",
|
|
146
|
+
"rx",
|
|
147
|
+
"ry",
|
|
148
|
+
"x1",
|
|
149
|
+
"y1",
|
|
150
|
+
"x2",
|
|
151
|
+
"y2",
|
|
152
|
+
"points",
|
|
153
|
+
"d",
|
|
154
|
+
"fill",
|
|
155
|
+
"stroke",
|
|
156
|
+
"stroke-width",
|
|
157
|
+
"font-family",
|
|
158
|
+
"font-size",
|
|
159
|
+
"text-anchor",
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
allowComments: false,
|
|
164
|
+
allowUnknownElements: false,
|
|
165
|
+
allowUnknownAttributes: false,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const cleanBuffer = Buffer.from(cleanSvg, "utf8");
|
|
169
|
+
|
|
170
|
+
// Basic sanity checks
|
|
171
|
+
if (cleanBuffer.length > 256 * 1024) {
|
|
172
|
+
throw new Error("SVG too large (max 256 KB)");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return Promise.resolve({
|
|
176
|
+
width: 0, // SVG is scalable, can’t guarantee pixel dimensions
|
|
177
|
+
height: 0,
|
|
178
|
+
format: "svg",
|
|
179
|
+
size: cleanBuffer.length,
|
|
180
|
+
safeBuffer: cleanBuffer,
|
|
181
|
+
});
|
|
182
|
+
}
|