@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.
Files changed (84) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/CODE_OF_CONDUCT.md +79 -0
  3. package/CONTRIBUTORS.md +27 -0
  4. package/LICENSE +21 -0
  5. package/README.md +43 -0
  6. package/SECURITY.md +17 -0
  7. package/dist/detectFormat.d.ts +2 -0
  8. package/dist/detectFormat.d.ts.map +1 -0
  9. package/dist/detectFormat.js +20 -0
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +1 -0
  13. package/dist/parse/index.d.ts +5 -0
  14. package/dist/parse/index.d.ts.map +1 -0
  15. package/dist/parse/index.js +4 -0
  16. package/dist/parse/parseJpeg.d.ts +5 -0
  17. package/dist/parse/parseJpeg.d.ts.map +1 -0
  18. package/dist/parse/parseJpeg.js +18 -0
  19. package/dist/parse/parsePng.d.ts +5 -0
  20. package/dist/parse/parsePng.d.ts.map +1 -0
  21. package/dist/parse/parsePng.js +6 -0
  22. package/dist/parse/parseSVG.d.ts +5 -0
  23. package/dist/parse/parseSVG.d.ts.map +1 -0
  24. package/dist/parse/parseSVG.js +23 -0
  25. package/dist/parse/parseWebp.d.ts +5 -0
  26. package/dist/parse/parseWebp.d.ts.map +1 -0
  27. package/dist/parse/parseWebp.js +17 -0
  28. package/dist/validators/avatarValidator.d.ts +9 -0
  29. package/dist/validators/avatarValidator.d.ts.map +1 -0
  30. package/dist/validators/avatarValidator.js +51 -0
  31. package/dist/validators/index.d.ts +3 -0
  32. package/dist/validators/index.d.ts.map +1 -0
  33. package/dist/validators/index.js +2 -0
  34. package/dist/validators/svgValidator.d.ts +18 -0
  35. package/dist/validators/svgValidator.d.ts.map +1 -0
  36. package/dist/validators/svgValidator.js +139 -0
  37. package/dist-cjs/detectFormat.d.ts +2 -0
  38. package/dist-cjs/detectFormat.d.ts.map +1 -0
  39. package/dist-cjs/detectFormat.js +23 -0
  40. package/dist-cjs/index.d.ts +2 -0
  41. package/dist-cjs/index.d.ts.map +1 -0
  42. package/dist-cjs/index.js +17 -0
  43. package/dist-cjs/parse/index.d.ts +5 -0
  44. package/dist-cjs/parse/index.d.ts.map +1 -0
  45. package/dist-cjs/parse/index.js +20 -0
  46. package/dist-cjs/parse/parseJpeg.d.ts +5 -0
  47. package/dist-cjs/parse/parseJpeg.d.ts.map +1 -0
  48. package/dist-cjs/parse/parseJpeg.js +21 -0
  49. package/dist-cjs/parse/parsePng.d.ts +5 -0
  50. package/dist-cjs/parse/parsePng.d.ts.map +1 -0
  51. package/dist-cjs/parse/parsePng.js +9 -0
  52. package/dist-cjs/parse/parseSVG.d.ts +5 -0
  53. package/dist-cjs/parse/parseSVG.d.ts.map +1 -0
  54. package/dist-cjs/parse/parseSVG.js +26 -0
  55. package/dist-cjs/parse/parseWebp.d.ts +5 -0
  56. package/dist-cjs/parse/parseWebp.d.ts.map +1 -0
  57. package/dist-cjs/parse/parseWebp.js +20 -0
  58. package/dist-cjs/validators/avatarValidator.d.ts +9 -0
  59. package/dist-cjs/validators/avatarValidator.d.ts.map +1 -0
  60. package/dist-cjs/validators/avatarValidator.js +57 -0
  61. package/dist-cjs/validators/index.d.ts +3 -0
  62. package/dist-cjs/validators/index.d.ts.map +1 -0
  63. package/dist-cjs/validators/index.js +18 -0
  64. package/dist-cjs/validators/svgValidator.d.ts +18 -0
  65. package/dist-cjs/validators/svgValidator.d.ts.map +1 -0
  66. package/dist-cjs/validators/svgValidator.js +143 -0
  67. package/docs/adrs/adr-0001-images-package-scope.md +21 -0
  68. package/docs/adrs/adr-0002-public-repo-governance.md +24 -0
  69. package/docs/adrs/adr-template.md +35 -0
  70. package/legal/CLA-REGISTRY.csv +1 -0
  71. package/legal/CLA.md +22 -0
  72. package/legal/CORPORATE_CLA.md +57 -0
  73. package/legal/INDIVIDUAL_CLA.md +91 -0
  74. package/package.json +102 -0
  75. package/src/detectFormat.ts +28 -0
  76. package/src/index.ts +1 -0
  77. package/src/parse/index.ts +4 -0
  78. package/src/parse/parseJpeg.ts +22 -0
  79. package/src/parse/parsePng.ts +9 -0
  80. package/src/parse/parseSVG.ts +30 -0
  81. package/src/parse/parseWebp.ts +19 -0
  82. package/src/validators/avatarValidator.ts +72 -0
  83. package/src/validators/index.ts +2 -0
  84. 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
+ }