@plasius/images 1.0.0 → 1.0.1

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 (67) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/dist/index.cjs +225 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +27 -0
  5. package/dist/index.d.ts +27 -2
  6. package/dist/index.js +186 -1
  7. package/dist/index.js.map +1 -0
  8. package/docs/adrs/index.md +4 -0
  9. package/package.json +6 -7
  10. package/dist/detectFormat.d.ts +0 -2
  11. package/dist/detectFormat.d.ts.map +0 -1
  12. package/dist/detectFormat.js +0 -20
  13. package/dist/index.d.ts.map +0 -1
  14. package/dist/parse/index.d.ts +0 -5
  15. package/dist/parse/index.d.ts.map +0 -1
  16. package/dist/parse/index.js +0 -4
  17. package/dist/parse/parseJpeg.d.ts +0 -5
  18. package/dist/parse/parseJpeg.d.ts.map +0 -1
  19. package/dist/parse/parseJpeg.js +0 -18
  20. package/dist/parse/parsePng.d.ts +0 -5
  21. package/dist/parse/parsePng.d.ts.map +0 -1
  22. package/dist/parse/parsePng.js +0 -6
  23. package/dist/parse/parseSVG.d.ts +0 -5
  24. package/dist/parse/parseSVG.d.ts.map +0 -1
  25. package/dist/parse/parseSVG.js +0 -23
  26. package/dist/parse/parseWebp.d.ts +0 -5
  27. package/dist/parse/parseWebp.d.ts.map +0 -1
  28. package/dist/parse/parseWebp.js +0 -17
  29. package/dist/validators/avatarValidator.d.ts +0 -9
  30. package/dist/validators/avatarValidator.d.ts.map +0 -1
  31. package/dist/validators/avatarValidator.js +0 -51
  32. package/dist/validators/index.d.ts +0 -3
  33. package/dist/validators/index.d.ts.map +0 -1
  34. package/dist/validators/index.js +0 -2
  35. package/dist/validators/svgValidator.d.ts +0 -18
  36. package/dist/validators/svgValidator.d.ts.map +0 -1
  37. package/dist/validators/svgValidator.js +0 -139
  38. package/dist-cjs/detectFormat.d.ts +0 -2
  39. package/dist-cjs/detectFormat.d.ts.map +0 -1
  40. package/dist-cjs/detectFormat.js +0 -23
  41. package/dist-cjs/index.d.ts +0 -2
  42. package/dist-cjs/index.d.ts.map +0 -1
  43. package/dist-cjs/index.js +0 -17
  44. package/dist-cjs/parse/index.d.ts +0 -5
  45. package/dist-cjs/parse/index.d.ts.map +0 -1
  46. package/dist-cjs/parse/index.js +0 -20
  47. package/dist-cjs/parse/parseJpeg.d.ts +0 -5
  48. package/dist-cjs/parse/parseJpeg.d.ts.map +0 -1
  49. package/dist-cjs/parse/parseJpeg.js +0 -21
  50. package/dist-cjs/parse/parsePng.d.ts +0 -5
  51. package/dist-cjs/parse/parsePng.d.ts.map +0 -1
  52. package/dist-cjs/parse/parsePng.js +0 -9
  53. package/dist-cjs/parse/parseSVG.d.ts +0 -5
  54. package/dist-cjs/parse/parseSVG.d.ts.map +0 -1
  55. package/dist-cjs/parse/parseSVG.js +0 -26
  56. package/dist-cjs/parse/parseWebp.d.ts +0 -5
  57. package/dist-cjs/parse/parseWebp.d.ts.map +0 -1
  58. package/dist-cjs/parse/parseWebp.js +0 -20
  59. package/dist-cjs/validators/avatarValidator.d.ts +0 -9
  60. package/dist-cjs/validators/avatarValidator.d.ts.map +0 -1
  61. package/dist-cjs/validators/avatarValidator.js +0 -57
  62. package/dist-cjs/validators/index.d.ts +0 -3
  63. package/dist-cjs/validators/index.d.ts.map +0 -1
  64. package/dist-cjs/validators/index.js +0 -18
  65. package/dist-cjs/validators/svgValidator.d.ts +0 -18
  66. package/dist-cjs/validators/svgValidator.d.ts.map +0 -1
  67. package/dist-cjs/validators/svgValidator.js +0 -143
package/CHANGELOG.md CHANGED
@@ -20,6 +20,20 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
20
20
  - **Security**
21
21
  - (placeholder)
22
22
 
23
+ ## [1.0.1] - 2026-02-13
24
+
25
+ - **Added**
26
+ - (placeholder)
27
+
28
+ - **Changed**
29
+ - Replace dual-`tsc` build steps with `tsup` to emit ESM + CJS + types side-by-side in `dist/` (`index.js`, `index.cjs`, `index.d.ts`).
30
+
31
+ - **Fixed**
32
+ - (placeholder)
33
+
34
+ - **Security**
35
+ - (placeholder)
36
+
23
37
  ## [1.0.0] - 2026-02-12
24
38
 
25
39
  - **Added**
@@ -48,7 +62,7 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
48
62
 
49
63
  ---
50
64
 
51
- [Unreleased]: https://github.com/Plasius-LTD/images/compare/v1.0.0...HEAD
65
+ [Unreleased]: https://github.com/Plasius-LTD/images/compare/v1.0.1...HEAD
52
66
 
53
67
  ## [1.0.0] - 2026-02-11
54
68
 
@@ -64,3 +78,4 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
64
78
  - **Security**
65
79
  - (placeholder)
66
80
  [1.0.0]: https://github.com/Plasius-LTD/images/releases/tag/v1.0.0
81
+ [1.0.1]: https://github.com/Plasius-LTD/images/releases/tag/v1.0.1
package/dist/index.cjs ADDED
@@ -0,0 +1,225 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ sanitiseSVG: () => sanitiseSVG,
34
+ validateAvatarImage: () => validateAvatarImage,
35
+ validateSvgAvatar: () => validateSvgAvatar
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // src/validators/svgValidator.ts
40
+ var import_xmldom = require("@xmldom/xmldom");
41
+ function sanitiseSVG(svgText, options) {
42
+ const strippedTags = [];
43
+ const strippedAttributes = [];
44
+ const parser = new import_xmldom.DOMParser({
45
+ onError: (level, msg) => {
46
+ switch (level) {
47
+ case "warning":
48
+ break;
49
+ default:
50
+ throw new Error(msg);
51
+ }
52
+ }
53
+ });
54
+ const serializer = new import_xmldom.XMLSerializer();
55
+ const doc = parser.parseFromString(svgText, "image/svg+xml");
56
+ function cleanNode(node) {
57
+ if (node === null) return;
58
+ if (node.nodeType === 8 && !options.allowComments) {
59
+ node.parentNode?.removeChild(node);
60
+ return;
61
+ }
62
+ if (node.nodeType === 1) {
63
+ const el = node;
64
+ const tagName = el.tagName.toLowerCase();
65
+ if (tagName === "style") {
66
+ strippedTags.push(tagName);
67
+ node.parentNode?.removeChild(node);
68
+ return;
69
+ }
70
+ if (!options.allowedTags.includes(tagName) && !options.allowUnknownElements) {
71
+ strippedTags.push(tagName);
72
+ node.parentNode?.removeChild(node);
73
+ return;
74
+ }
75
+ const allowedAttrs = options.allowedAttributes["*"] || [];
76
+ const toRemove = [];
77
+ for (const attr of Array.from(el.attributes)) {
78
+ const attrName = attr.name.toLowerCase();
79
+ if (attrName.startsWith("on")) {
80
+ toRemove.push(attr.name);
81
+ strippedAttributes.push(`${tagName}.${attr.name}`);
82
+ continue;
83
+ }
84
+ if (attrName === "xlink:href" || attrName === "xmlns:xlink") {
85
+ toRemove.push(attr.name);
86
+ strippedAttributes.push(`${tagName}.${attr.name}`);
87
+ continue;
88
+ }
89
+ if (attrName === "style") {
90
+ toRemove.push(attr.name);
91
+ strippedAttributes.push(`${tagName}.${attr.name}`);
92
+ continue;
93
+ }
94
+ if (!allowedAttrs.includes(attr.name) && !options.allowUnknownAttributes) {
95
+ toRemove.push(attr.name);
96
+ strippedAttributes.push(`${tagName}.${attr.name}`);
97
+ }
98
+ }
99
+ toRemove.forEach((attrName) => el.removeAttribute(attrName));
100
+ }
101
+ const children = Array.from(node.childNodes);
102
+ children.forEach((child) => cleanNode(child));
103
+ }
104
+ cleanNode(doc.documentElement);
105
+ return {
106
+ svg: serializer.serializeToString(doc),
107
+ audit: { strippedTags, strippedAttributes }
108
+ };
109
+ }
110
+ function validateSvgAvatar(buffer) {
111
+ const svgText = buffer.toString("utf8");
112
+ const { svg: cleanSvg } = sanitiseSVG(svgText, {
113
+ allowedTags: [
114
+ "svg",
115
+ "g",
116
+ "rect",
117
+ "circle",
118
+ "ellipse",
119
+ "line",
120
+ "polygon",
121
+ "polyline",
122
+ "path",
123
+ "text",
124
+ "title",
125
+ "desc"
126
+ ],
127
+ allowedAttributes: {
128
+ "*": [
129
+ "width",
130
+ "height",
131
+ "viewBox",
132
+ "xmlns",
133
+ "transform",
134
+ "x",
135
+ "y",
136
+ "cx",
137
+ "cy",
138
+ "r",
139
+ "rx",
140
+ "ry",
141
+ "x1",
142
+ "y1",
143
+ "x2",
144
+ "y2",
145
+ "points",
146
+ "d",
147
+ "fill",
148
+ "stroke",
149
+ "stroke-width",
150
+ "font-family",
151
+ "font-size",
152
+ "text-anchor"
153
+ ]
154
+ },
155
+ allowComments: false,
156
+ allowUnknownElements: false,
157
+ allowUnknownAttributes: false
158
+ });
159
+ const cleanBuffer = Buffer.from(cleanSvg, "utf8");
160
+ if (cleanBuffer.length > 256 * 1024) {
161
+ throw new Error("SVG too large (max 256 KB)");
162
+ }
163
+ return Promise.resolve({
164
+ width: 0,
165
+ // SVG is scalable, can’t guarantee pixel dimensions
166
+ height: 0,
167
+ format: "svg",
168
+ size: cleanBuffer.length,
169
+ safeBuffer: cleanBuffer
170
+ });
171
+ }
172
+
173
+ // src/validators/avatarValidator.ts
174
+ var import_sharp = __toESM(require("sharp"), 1);
175
+ async function validateAvatarImage(buffer) {
176
+ const metadata = await (0, import_sharp.default)(buffer).metadata();
177
+ const format = metadata.format;
178
+ if (!format) {
179
+ throw new Error("Unsupported or unknown image format");
180
+ }
181
+ const width = metadata.width;
182
+ const height = metadata.height;
183
+ if (!width || !height) {
184
+ throw new Error("Could not determine image dimensions");
185
+ }
186
+ if (width < 64 || height < 64) {
187
+ throw new Error("Image too small (min 64x64)");
188
+ }
189
+ if (width > 2048 || height > 2048) {
190
+ throw new Error("Image too large (max 2048x2048)");
191
+ }
192
+ const targetFormat = "png";
193
+ let safeBuffer;
194
+ if (format === "svg") {
195
+ const svgText = buffer.toString("utf8");
196
+ const svgValidation = await validateSvgAvatar(
197
+ Buffer.from(svgText, "utf-8")
198
+ );
199
+ safeBuffer = await (0, import_sharp.default)(svgValidation.safeBuffer).resize({
200
+ width: Math.min(width, 2048),
201
+ height: Math.min(height, 2048),
202
+ fit: "inside"
203
+ }).toFormat(targetFormat).toBuffer();
204
+ } else {
205
+ safeBuffer = await (0, import_sharp.default)(buffer).resize({
206
+ width: Math.min(width, 2048),
207
+ height: Math.min(height, 2048),
208
+ fit: "inside"
209
+ }).toFormat(targetFormat, { quality: 90 }).toBuffer();
210
+ }
211
+ return {
212
+ width,
213
+ height,
214
+ format,
215
+ size: buffer.length,
216
+ safeBuffer
217
+ };
218
+ }
219
+ // Annotate the CommonJS export names for ESM import in node:
220
+ 0 && (module.exports = {
221
+ sanitiseSVG,
222
+ validateAvatarImage,
223
+ validateSvgAvatar
224
+ });
225
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/validators/svgValidator.ts","../src/validators/avatarValidator.ts"],"sourcesContent":["export * from \"./validators/index.js\";\n","import type { ImageValidationResult } from \"./avatarValidator.js\";\nimport { DOMParser, XMLSerializer } from \"@xmldom/xmldom\";\nimport type { Element as XmlElement } from \"@xmldom/xmldom\";\n\nexport function sanitiseSVG(\n svgText: string,\n options: {\n allowedTags: string[];\n allowedAttributes: { \"*\": string[] };\n allowComments: boolean;\n allowUnknownElements: boolean;\n allowUnknownAttributes: boolean;\n }\n): {\n svg: string;\n audit: { strippedTags: string[]; strippedAttributes: string[] };\n} {\n const strippedTags: string[] = [];\n const strippedAttributes: string[] = [];\n\n const parser = new DOMParser({\n onError: (\n level: \"warning\" | \"error\" | \"fatalError\",\n msg: string\n ): void => {\n switch (level) {\n case \"warning\":\n break;\n default:\n throw new Error(msg);\n }\n },\n });\n const serializer = new XMLSerializer();\n const doc = parser.parseFromString(svgText, \"image/svg+xml\");\n\n function cleanNode(node: Node | null) {\n // TODO: optionally sanitize <style> elements and inline style attributes\n\n if (node === null) return;\n\n if (node.nodeType === 8 /* Comment */ && !options.allowComments) {\n node.parentNode?.removeChild(node);\n return;\n }\n\n if (node.nodeType === 1 /* Element */) {\n const el = node as unknown as XmlElement;\n const tagName = el.tagName.toLowerCase();\n\n // Strip <style> elements entirely\n if (tagName === \"style\") {\n strippedTags.push(tagName);\n node.parentNode?.removeChild(node);\n return;\n }\n\n if (\n !options.allowedTags.includes(tagName) &&\n !options.allowUnknownElements\n ) {\n strippedTags.push(tagName);\n node.parentNode?.removeChild(node);\n return;\n }\n\n // Clean attributes\n const allowedAttrs = options.allowedAttributes[\"*\"] || [];\n\n const toRemove: string[] = [];\n for (const attr of Array.from(el.attributes)) {\n const attrName = attr.name.toLowerCase();\n\n if (attrName.startsWith(\"on\")) {\n toRemove.push(attr.name);\n strippedAttributes.push(`${tagName}.${attr.name}`);\n continue;\n }\n if (attrName === \"xlink:href\" || attrName === \"xmlns:xlink\") {\n toRemove.push(attr.name);\n strippedAttributes.push(`${tagName}.${attr.name}`);\n continue;\n }\n if (attrName === \"style\") {\n toRemove.push(attr.name);\n strippedAttributes.push(`${tagName}.${attr.name}`);\n continue;\n }\n\n if (\n !allowedAttrs.includes(attr.name) &&\n !options.allowUnknownAttributes\n ) {\n toRemove.push(attr.name);\n strippedAttributes.push(`${tagName}.${attr.name}`);\n }\n }\n toRemove.forEach((attrName) => el.removeAttribute(attrName));\n }\n\n // Recursively clean children\n const children = Array.from(node.childNodes);\n children.forEach((child) => cleanNode(child));\n }\n\n cleanNode(doc.documentElement as unknown as Node);\n\n return {\n svg: serializer.serializeToString(doc),\n audit: { strippedTags, strippedAttributes },\n };\n}\n\nexport function validateSvgAvatar(\n buffer: Buffer\n): Promise<ImageValidationResult> {\n const svgText = buffer.toString(\"utf8\");\n\n const { svg: cleanSvg } = sanitiseSVG(svgText, {\n allowedTags: [\n \"svg\",\n \"g\",\n \"rect\",\n \"circle\",\n \"ellipse\",\n \"line\",\n \"polygon\",\n \"polyline\",\n \"path\",\n \"text\",\n \"title\",\n \"desc\",\n ],\n allowedAttributes: {\n \"*\": [\n \"width\",\n \"height\",\n \"viewBox\",\n \"xmlns\",\n \"transform\",\n \"x\",\n \"y\",\n \"cx\",\n \"cy\",\n \"r\",\n \"rx\",\n \"ry\",\n \"x1\",\n \"y1\",\n \"x2\",\n \"y2\",\n \"points\",\n \"d\",\n \"fill\",\n \"stroke\",\n \"stroke-width\",\n \"font-family\",\n \"font-size\",\n \"text-anchor\",\n ],\n },\n\n allowComments: false,\n allowUnknownElements: false,\n allowUnknownAttributes: false,\n });\n\n const cleanBuffer = Buffer.from(cleanSvg, \"utf8\");\n\n // Basic sanity checks\n if (cleanBuffer.length > 256 * 1024) {\n throw new Error(\"SVG too large (max 256 KB)\");\n }\n\n return Promise.resolve({\n width: 0, // SVG is scalable, can’t guarantee pixel dimensions\n height: 0,\n format: \"svg\",\n size: cleanBuffer.length,\n safeBuffer: cleanBuffer,\n });\n}\n","import sharp from \"sharp\";\nimport { validateSvgAvatar } from \"./svgValidator.js\";\n\nexport interface ImageValidationResult {\n width: number;\n height: number;\n format: string;\n size: number;\n safeBuffer: Buffer;\n}\n\nexport async function validateAvatarImage(\n buffer: Buffer\n): Promise<ImageValidationResult> {\n const metadata = await sharp(buffer).metadata();\n const format = metadata.format;\n\n if (!format) {\n throw new Error(\"Unsupported or unknown image format\");\n }\n\n const width = metadata.width;\n const height = metadata.height;\n\n if (!width || !height) {\n throw new Error(\"Could not determine image dimensions\");\n }\n\n if (width < 64 || height < 64) {\n throw new Error(\"Image too small (min 64x64)\");\n }\n\n if (width > 2048 || height > 2048) {\n throw new Error(\"Image too large (max 2048x2048)\");\n }\n\n const targetFormat = \"png\"; // common safe web format\n\n let safeBuffer: Buffer;\n\n if (format === \"svg\") {\n const svgText = buffer.toString(\"utf8\");\n const svgValidation = await validateSvgAvatar(\n Buffer.from(svgText, \"utf-8\")\n );\n safeBuffer = await sharp(svgValidation.safeBuffer)\n .resize({\n width: Math.min(width, 2048),\n height: Math.min(height, 2048),\n fit: \"inside\",\n })\n .toFormat(targetFormat)\n .toBuffer();\n } else {\n safeBuffer = await sharp(buffer)\n .resize({\n width: Math.min(width, 2048),\n height: Math.min(height, 2048),\n fit: \"inside\",\n })\n .toFormat(targetFormat, { quality: 90 })\n .toBuffer();\n }\n\n return {\n width,\n height,\n format,\n size: buffer.length,\n safeBuffer,\n };\n}\n"],"mappings":"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","names":["sharp"]}
@@ -0,0 +1,27 @@
1
+ interface ImageValidationResult {
2
+ width: number;
3
+ height: number;
4
+ format: string;
5
+ size: number;
6
+ safeBuffer: Buffer;
7
+ }
8
+ declare function validateAvatarImage(buffer: Buffer): Promise<ImageValidationResult>;
9
+
10
+ declare function sanitiseSVG(svgText: string, options: {
11
+ allowedTags: string[];
12
+ allowedAttributes: {
13
+ "*": string[];
14
+ };
15
+ allowComments: boolean;
16
+ allowUnknownElements: boolean;
17
+ allowUnknownAttributes: boolean;
18
+ }): {
19
+ svg: string;
20
+ audit: {
21
+ strippedTags: string[];
22
+ strippedAttributes: string[];
23
+ };
24
+ };
25
+ declare function validateSvgAvatar(buffer: Buffer): Promise<ImageValidationResult>;
26
+
27
+ export { type ImageValidationResult, sanitiseSVG, validateAvatarImage, validateSvgAvatar };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,27 @@
1
- export * from "./validators/index.js";
2
- //# sourceMappingURL=index.d.ts.map
1
+ interface ImageValidationResult {
2
+ width: number;
3
+ height: number;
4
+ format: string;
5
+ size: number;
6
+ safeBuffer: Buffer;
7
+ }
8
+ declare function validateAvatarImage(buffer: Buffer): Promise<ImageValidationResult>;
9
+
10
+ declare function sanitiseSVG(svgText: string, options: {
11
+ allowedTags: string[];
12
+ allowedAttributes: {
13
+ "*": string[];
14
+ };
15
+ allowComments: boolean;
16
+ allowUnknownElements: boolean;
17
+ allowUnknownAttributes: boolean;
18
+ }): {
19
+ svg: string;
20
+ audit: {
21
+ strippedTags: string[];
22
+ strippedAttributes: string[];
23
+ };
24
+ };
25
+ declare function validateSvgAvatar(buffer: Buffer): Promise<ImageValidationResult>;
26
+
27
+ export { type ImageValidationResult, sanitiseSVG, validateAvatarImage, validateSvgAvatar };
package/dist/index.js CHANGED
@@ -1 +1,186 @@
1
- export * from "./validators/index.js";
1
+ // src/validators/svgValidator.ts
2
+ import { DOMParser, XMLSerializer } from "@xmldom/xmldom";
3
+ function sanitiseSVG(svgText, options) {
4
+ const strippedTags = [];
5
+ const strippedAttributes = [];
6
+ const parser = new DOMParser({
7
+ onError: (level, msg) => {
8
+ switch (level) {
9
+ case "warning":
10
+ break;
11
+ default:
12
+ throw new Error(msg);
13
+ }
14
+ }
15
+ });
16
+ const serializer = new XMLSerializer();
17
+ const doc = parser.parseFromString(svgText, "image/svg+xml");
18
+ function cleanNode(node) {
19
+ if (node === null) return;
20
+ if (node.nodeType === 8 && !options.allowComments) {
21
+ node.parentNode?.removeChild(node);
22
+ return;
23
+ }
24
+ if (node.nodeType === 1) {
25
+ const el = node;
26
+ const tagName = el.tagName.toLowerCase();
27
+ if (tagName === "style") {
28
+ strippedTags.push(tagName);
29
+ node.parentNode?.removeChild(node);
30
+ return;
31
+ }
32
+ if (!options.allowedTags.includes(tagName) && !options.allowUnknownElements) {
33
+ strippedTags.push(tagName);
34
+ node.parentNode?.removeChild(node);
35
+ return;
36
+ }
37
+ const allowedAttrs = options.allowedAttributes["*"] || [];
38
+ const toRemove = [];
39
+ for (const attr of Array.from(el.attributes)) {
40
+ const attrName = attr.name.toLowerCase();
41
+ if (attrName.startsWith("on")) {
42
+ toRemove.push(attr.name);
43
+ strippedAttributes.push(`${tagName}.${attr.name}`);
44
+ continue;
45
+ }
46
+ if (attrName === "xlink:href" || attrName === "xmlns:xlink") {
47
+ toRemove.push(attr.name);
48
+ strippedAttributes.push(`${tagName}.${attr.name}`);
49
+ continue;
50
+ }
51
+ if (attrName === "style") {
52
+ toRemove.push(attr.name);
53
+ strippedAttributes.push(`${tagName}.${attr.name}`);
54
+ continue;
55
+ }
56
+ if (!allowedAttrs.includes(attr.name) && !options.allowUnknownAttributes) {
57
+ toRemove.push(attr.name);
58
+ strippedAttributes.push(`${tagName}.${attr.name}`);
59
+ }
60
+ }
61
+ toRemove.forEach((attrName) => el.removeAttribute(attrName));
62
+ }
63
+ const children = Array.from(node.childNodes);
64
+ children.forEach((child) => cleanNode(child));
65
+ }
66
+ cleanNode(doc.documentElement);
67
+ return {
68
+ svg: serializer.serializeToString(doc),
69
+ audit: { strippedTags, strippedAttributes }
70
+ };
71
+ }
72
+ function validateSvgAvatar(buffer) {
73
+ const svgText = buffer.toString("utf8");
74
+ const { svg: cleanSvg } = sanitiseSVG(svgText, {
75
+ allowedTags: [
76
+ "svg",
77
+ "g",
78
+ "rect",
79
+ "circle",
80
+ "ellipse",
81
+ "line",
82
+ "polygon",
83
+ "polyline",
84
+ "path",
85
+ "text",
86
+ "title",
87
+ "desc"
88
+ ],
89
+ allowedAttributes: {
90
+ "*": [
91
+ "width",
92
+ "height",
93
+ "viewBox",
94
+ "xmlns",
95
+ "transform",
96
+ "x",
97
+ "y",
98
+ "cx",
99
+ "cy",
100
+ "r",
101
+ "rx",
102
+ "ry",
103
+ "x1",
104
+ "y1",
105
+ "x2",
106
+ "y2",
107
+ "points",
108
+ "d",
109
+ "fill",
110
+ "stroke",
111
+ "stroke-width",
112
+ "font-family",
113
+ "font-size",
114
+ "text-anchor"
115
+ ]
116
+ },
117
+ allowComments: false,
118
+ allowUnknownElements: false,
119
+ allowUnknownAttributes: false
120
+ });
121
+ const cleanBuffer = Buffer.from(cleanSvg, "utf8");
122
+ if (cleanBuffer.length > 256 * 1024) {
123
+ throw new Error("SVG too large (max 256 KB)");
124
+ }
125
+ return Promise.resolve({
126
+ width: 0,
127
+ // SVG is scalable, can’t guarantee pixel dimensions
128
+ height: 0,
129
+ format: "svg",
130
+ size: cleanBuffer.length,
131
+ safeBuffer: cleanBuffer
132
+ });
133
+ }
134
+
135
+ // src/validators/avatarValidator.ts
136
+ import sharp from "sharp";
137
+ async function validateAvatarImage(buffer) {
138
+ const metadata = await sharp(buffer).metadata();
139
+ const format = metadata.format;
140
+ if (!format) {
141
+ throw new Error("Unsupported or unknown image format");
142
+ }
143
+ const width = metadata.width;
144
+ const height = metadata.height;
145
+ if (!width || !height) {
146
+ throw new Error("Could not determine image dimensions");
147
+ }
148
+ if (width < 64 || height < 64) {
149
+ throw new Error("Image too small (min 64x64)");
150
+ }
151
+ if (width > 2048 || height > 2048) {
152
+ throw new Error("Image too large (max 2048x2048)");
153
+ }
154
+ const targetFormat = "png";
155
+ let safeBuffer;
156
+ if (format === "svg") {
157
+ const svgText = buffer.toString("utf8");
158
+ const svgValidation = await validateSvgAvatar(
159
+ Buffer.from(svgText, "utf-8")
160
+ );
161
+ safeBuffer = await sharp(svgValidation.safeBuffer).resize({
162
+ width: Math.min(width, 2048),
163
+ height: Math.min(height, 2048),
164
+ fit: "inside"
165
+ }).toFormat(targetFormat).toBuffer();
166
+ } else {
167
+ safeBuffer = await sharp(buffer).resize({
168
+ width: Math.min(width, 2048),
169
+ height: Math.min(height, 2048),
170
+ fit: "inside"
171
+ }).toFormat(targetFormat, { quality: 90 }).toBuffer();
172
+ }
173
+ return {
174
+ width,
175
+ height,
176
+ format,
177
+ size: buffer.length,
178
+ safeBuffer
179
+ };
180
+ }
181
+ export {
182
+ sanitiseSVG,
183
+ validateAvatarImage,
184
+ validateSvgAvatar
185
+ };
186
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/validators/svgValidator.ts","../src/validators/avatarValidator.ts"],"sourcesContent":["import type { ImageValidationResult } from \"./avatarValidator.js\";\nimport { DOMParser, XMLSerializer } from \"@xmldom/xmldom\";\nimport type { Element as XmlElement } from \"@xmldom/xmldom\";\n\nexport function sanitiseSVG(\n svgText: string,\n options: {\n allowedTags: string[];\n allowedAttributes: { \"*\": string[] };\n allowComments: boolean;\n allowUnknownElements: boolean;\n allowUnknownAttributes: boolean;\n }\n): {\n svg: string;\n audit: { strippedTags: string[]; strippedAttributes: string[] };\n} {\n const strippedTags: string[] = [];\n const strippedAttributes: string[] = [];\n\n const parser = new DOMParser({\n onError: (\n level: \"warning\" | \"error\" | \"fatalError\",\n msg: string\n ): void => {\n switch (level) {\n case \"warning\":\n break;\n default:\n throw new Error(msg);\n }\n },\n });\n const serializer = new XMLSerializer();\n const doc = parser.parseFromString(svgText, \"image/svg+xml\");\n\n function cleanNode(node: Node | null) {\n // TODO: optionally sanitize <style> elements and inline style attributes\n\n if (node === null) return;\n\n if (node.nodeType === 8 /* Comment */ && !options.allowComments) {\n node.parentNode?.removeChild(node);\n return;\n }\n\n if (node.nodeType === 1 /* Element */) {\n const el = node as unknown as XmlElement;\n const tagName = el.tagName.toLowerCase();\n\n // Strip <style> elements entirely\n if (tagName === \"style\") {\n strippedTags.push(tagName);\n node.parentNode?.removeChild(node);\n return;\n }\n\n if (\n !options.allowedTags.includes(tagName) &&\n !options.allowUnknownElements\n ) {\n strippedTags.push(tagName);\n node.parentNode?.removeChild(node);\n return;\n }\n\n // Clean attributes\n const allowedAttrs = options.allowedAttributes[\"*\"] || [];\n\n const toRemove: string[] = [];\n for (const attr of Array.from(el.attributes)) {\n const attrName = attr.name.toLowerCase();\n\n if (attrName.startsWith(\"on\")) {\n toRemove.push(attr.name);\n strippedAttributes.push(`${tagName}.${attr.name}`);\n continue;\n }\n if (attrName === \"xlink:href\" || attrName === \"xmlns:xlink\") {\n toRemove.push(attr.name);\n strippedAttributes.push(`${tagName}.${attr.name}`);\n continue;\n }\n if (attrName === \"style\") {\n toRemove.push(attr.name);\n strippedAttributes.push(`${tagName}.${attr.name}`);\n continue;\n }\n\n if (\n !allowedAttrs.includes(attr.name) &&\n !options.allowUnknownAttributes\n ) {\n toRemove.push(attr.name);\n strippedAttributes.push(`${tagName}.${attr.name}`);\n }\n }\n toRemove.forEach((attrName) => el.removeAttribute(attrName));\n }\n\n // Recursively clean children\n const children = Array.from(node.childNodes);\n children.forEach((child) => cleanNode(child));\n }\n\n cleanNode(doc.documentElement as unknown as Node);\n\n return {\n svg: serializer.serializeToString(doc),\n audit: { strippedTags, strippedAttributes },\n };\n}\n\nexport function validateSvgAvatar(\n buffer: Buffer\n): Promise<ImageValidationResult> {\n const svgText = buffer.toString(\"utf8\");\n\n const { svg: cleanSvg } = sanitiseSVG(svgText, {\n allowedTags: [\n \"svg\",\n \"g\",\n \"rect\",\n \"circle\",\n \"ellipse\",\n \"line\",\n \"polygon\",\n \"polyline\",\n \"path\",\n \"text\",\n \"title\",\n \"desc\",\n ],\n allowedAttributes: {\n \"*\": [\n \"width\",\n \"height\",\n \"viewBox\",\n \"xmlns\",\n \"transform\",\n \"x\",\n \"y\",\n \"cx\",\n \"cy\",\n \"r\",\n \"rx\",\n \"ry\",\n \"x1\",\n \"y1\",\n \"x2\",\n \"y2\",\n \"points\",\n \"d\",\n \"fill\",\n \"stroke\",\n \"stroke-width\",\n \"font-family\",\n \"font-size\",\n \"text-anchor\",\n ],\n },\n\n allowComments: false,\n allowUnknownElements: false,\n allowUnknownAttributes: false,\n });\n\n const cleanBuffer = Buffer.from(cleanSvg, \"utf8\");\n\n // Basic sanity checks\n if (cleanBuffer.length > 256 * 1024) {\n throw new Error(\"SVG too large (max 256 KB)\");\n }\n\n return Promise.resolve({\n width: 0, // SVG is scalable, can’t guarantee pixel dimensions\n height: 0,\n format: \"svg\",\n size: cleanBuffer.length,\n safeBuffer: cleanBuffer,\n });\n}\n","import sharp from \"sharp\";\nimport { validateSvgAvatar } from \"./svgValidator.js\";\n\nexport interface ImageValidationResult {\n width: number;\n height: number;\n format: string;\n size: number;\n safeBuffer: Buffer;\n}\n\nexport async function validateAvatarImage(\n buffer: Buffer\n): Promise<ImageValidationResult> {\n const metadata = await sharp(buffer).metadata();\n const format = metadata.format;\n\n if (!format) {\n throw new Error(\"Unsupported or unknown image format\");\n }\n\n const width = metadata.width;\n const height = metadata.height;\n\n if (!width || !height) {\n throw new Error(\"Could not determine image dimensions\");\n }\n\n if (width < 64 || height < 64) {\n throw new Error(\"Image too small (min 64x64)\");\n }\n\n if (width > 2048 || height > 2048) {\n throw new Error(\"Image too large (max 2048x2048)\");\n }\n\n const targetFormat = \"png\"; // common safe web format\n\n let safeBuffer: Buffer;\n\n if (format === \"svg\") {\n const svgText = buffer.toString(\"utf8\");\n const svgValidation = await validateSvgAvatar(\n Buffer.from(svgText, \"utf-8\")\n );\n safeBuffer = await sharp(svgValidation.safeBuffer)\n .resize({\n width: Math.min(width, 2048),\n height: Math.min(height, 2048),\n fit: \"inside\",\n })\n .toFormat(targetFormat)\n .toBuffer();\n } else {\n safeBuffer = await sharp(buffer)\n .resize({\n width: Math.min(width, 2048),\n height: Math.min(height, 2048),\n fit: \"inside\",\n })\n .toFormat(targetFormat, { quality: 90 })\n .toBuffer();\n }\n\n return {\n width,\n height,\n format,\n size: buffer.length,\n safeBuffer,\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,4 @@
1
+ # ADR Index
2
+
3
+ - [ADR-0001: Standalone @plasius/images Package Scope](./adr-0001-images-package-scope.md)
4
+ - [ADR-0002: Public Repository Governance Baseline](./adr-0002-public-repo-governance.md)
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@plasius/images",
3
- "version": "1.0.0",
4
- "main": "./dist-cjs/index.js",
3
+ "version": "1.0.1",
4
+ "main": "./dist/index.cjs",
5
5
  "types": "./dist/index.d.ts",
6
6
  "private": false,
7
7
  "type": "module",
8
8
  "description": "Image validator and uploader for Plasius projects",
9
9
  "scripts": {
10
- "build": "tsc --build && npm run build:cjs",
10
+ "build": "tsup",
11
11
  "test": "vitest run",
12
12
  "test:watch": "vitest",
13
13
  "test:coverage": "vitest run --coverage",
14
14
  "test:coverage:watch": "vitest --coverage",
15
- "clean": "rimraf dist-cjs dist tsconfig.tsbuildinfo",
15
+ "clean": "rimraf dist dist-cjs tsconfig.tsbuildinfo",
16
16
  "reset:clean": "rm -rf node_modules package-lock.json && npm run clean",
17
17
  "audit:ts": "tsc --noEmit --pretty",
18
18
  "audit:eslint": "eslint \"{src,apps,packages}/**/*.{ts,tsx}\" --max-warnings=0 --ext .ts,.tsx",
@@ -20,7 +20,6 @@
20
20
  "audit:npm": "npm audit --audit-level=moderate || true",
21
21
  "audit:test": "vitest run --coverage",
22
22
  "audit:all": "npm-run-all -l audit:ts audit:eslint audit:deps audit:npm audit:test",
23
- "build:cjs": "tsc -p tsconfig.json --module commonjs --moduleResolution node --outDir dist-cjs --tsBuildInfoFile dist-cjs/tsconfig.tsbuildinfo",
24
23
  "lint": "eslint .",
25
24
  "prepare": "npm run build"
26
25
  },
@@ -49,6 +48,7 @@
49
48
  "npm-run-all": "^4.1.5",
50
49
  "react": "^19.1.0",
51
50
  "tsx": "^4.20.3",
51
+ "tsup": "^8.5.0",
52
52
  "typescript": "^5.8.3",
53
53
  "vitest": "^3.2.4",
54
54
  "zod": "^4.0.17"
@@ -57,14 +57,13 @@
57
57
  ".": {
58
58
  "types": "./dist/index.d.ts",
59
59
  "import": "./dist/index.js",
60
- "require": "./dist-cjs/index.js"
60
+ "require": "./dist/index.cjs"
61
61
  },
62
62
  "./package.json": "./package.json"
63
63
  },
64
64
  "module": "./dist/index.js",
65
65
  "files": [
66
66
  "dist",
67
- "dist-cjs",
68
67
  "src",
69
68
  "README.md",
70
69
  "CHANGELOG.md",
@@ -1,2 +0,0 @@
1
- export declare function detectFormat(buffer: Buffer): "jpeg" | "png" | "webp" | "svg" | undefined;
2
- //# sourceMappingURL=detectFormat.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"detectFormat.d.ts","sourceRoot":"","sources":["../src/detectFormat.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,GACb,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,SAAS,CAyB7C"}
@@ -1,20 +0,0 @@
1
- export function detectFormat(buffer) {
2
- if (buffer[0] === 0xff && buffer[1] === 0xd8) {
3
- return "jpeg";
4
- }
5
- if (buffer[0] === 0x89 &&
6
- buffer[1] === 0x50 &&
7
- buffer[2] === 0x4e &&
8
- buffer[3] === 0x47) {
9
- return "png";
10
- }
11
- if (buffer.slice(0, 4).toString() === "RIFF" &&
12
- buffer.slice(8, 12).toString() === "WEBP") {
13
- return "webp";
14
- }
15
- const header = buffer.slice(0, 1000).toString("utf8").toLowerCase();
16
- if (header.includes("<svg")) {
17
- return "svg";
18
- }
19
- return undefined;
20
- }
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC"}
@@ -1,5 +0,0 @@
1
- export * from "./parseJpeg.js";
2
- export * from "./parsePng.js";
3
- export * from "./parseSVG.js";
4
- export * from "./parseWebp.js";
5
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/parse/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC"}
@@ -1,4 +0,0 @@
1
- export * from "./parseJpeg.js";
2
- export * from "./parsePng.js";
3
- export * from "./parseSVG.js";
4
- export * from "./parseWebp.js";
@@ -1,5 +0,0 @@
1
- export declare function parseJpeg(buffer: Buffer): {
2
- width: number;
3
- height: number;
4
- };
5
- //# sourceMappingURL=parseJpeg.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"parseJpeg.d.ts","sourceRoot":"","sources":["../../src/parse/parseJpeg.ts"],"names":[],"mappings":"AAAA,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAqB3E"}