@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,139 @@
1
+ import { DOMParser, XMLSerializer } from "@xmldom/xmldom";
2
+ export function sanitiseSVG(svgText, options) {
3
+ const strippedTags = [];
4
+ const strippedAttributes = [];
5
+ const parser = new DOMParser({
6
+ onError: (level, msg) => {
7
+ switch (level) {
8
+ case "warning":
9
+ break;
10
+ default:
11
+ throw new Error(msg);
12
+ }
13
+ },
14
+ });
15
+ const serializer = new XMLSerializer();
16
+ const doc = parser.parseFromString(svgText, "image/svg+xml");
17
+ function cleanNode(node) {
18
+ // TODO: optionally sanitize <style> elements and inline style attributes
19
+ if (node === null)
20
+ return;
21
+ if (node.nodeType === 8 /* Comment */ && !options.allowComments) {
22
+ node.parentNode?.removeChild(node);
23
+ return;
24
+ }
25
+ if (node.nodeType === 1 /* Element */) {
26
+ const el = node;
27
+ const tagName = el.tagName.toLowerCase();
28
+ // Strip <style> elements entirely
29
+ if (tagName === "style") {
30
+ strippedTags.push(tagName);
31
+ node.parentNode?.removeChild(node);
32
+ return;
33
+ }
34
+ if (!options.allowedTags.includes(tagName) &&
35
+ !options.allowUnknownElements) {
36
+ strippedTags.push(tagName);
37
+ node.parentNode?.removeChild(node);
38
+ return;
39
+ }
40
+ // Clean attributes
41
+ const allowedAttrs = options.allowedAttributes["*"] || [];
42
+ const toRemove = [];
43
+ for (const attr of Array.from(el.attributes)) {
44
+ const attrName = attr.name.toLowerCase();
45
+ if (attrName.startsWith("on")) {
46
+ toRemove.push(attr.name);
47
+ strippedAttributes.push(`${tagName}.${attr.name}`);
48
+ continue;
49
+ }
50
+ if (attrName === "xlink:href" || attrName === "xmlns:xlink") {
51
+ toRemove.push(attr.name);
52
+ strippedAttributes.push(`${tagName}.${attr.name}`);
53
+ continue;
54
+ }
55
+ if (attrName === "style") {
56
+ toRemove.push(attr.name);
57
+ strippedAttributes.push(`${tagName}.${attr.name}`);
58
+ continue;
59
+ }
60
+ if (!allowedAttrs.includes(attr.name) &&
61
+ !options.allowUnknownAttributes) {
62
+ toRemove.push(attr.name);
63
+ strippedAttributes.push(`${tagName}.${attr.name}`);
64
+ }
65
+ }
66
+ toRemove.forEach((attrName) => el.removeAttribute(attrName));
67
+ }
68
+ // Recursively clean children
69
+ const children = Array.from(node.childNodes);
70
+ children.forEach((child) => cleanNode(child));
71
+ }
72
+ cleanNode(doc.documentElement);
73
+ return {
74
+ svg: serializer.serializeToString(doc),
75
+ audit: { strippedTags, strippedAttributes },
76
+ };
77
+ }
78
+ export function validateSvgAvatar(buffer) {
79
+ const svgText = buffer.toString("utf8");
80
+ const { svg: cleanSvg } = sanitiseSVG(svgText, {
81
+ allowedTags: [
82
+ "svg",
83
+ "g",
84
+ "rect",
85
+ "circle",
86
+ "ellipse",
87
+ "line",
88
+ "polygon",
89
+ "polyline",
90
+ "path",
91
+ "text",
92
+ "title",
93
+ "desc",
94
+ ],
95
+ allowedAttributes: {
96
+ "*": [
97
+ "width",
98
+ "height",
99
+ "viewBox",
100
+ "xmlns",
101
+ "transform",
102
+ "x",
103
+ "y",
104
+ "cx",
105
+ "cy",
106
+ "r",
107
+ "rx",
108
+ "ry",
109
+ "x1",
110
+ "y1",
111
+ "x2",
112
+ "y2",
113
+ "points",
114
+ "d",
115
+ "fill",
116
+ "stroke",
117
+ "stroke-width",
118
+ "font-family",
119
+ "font-size",
120
+ "text-anchor",
121
+ ],
122
+ },
123
+ allowComments: false,
124
+ allowUnknownElements: false,
125
+ allowUnknownAttributes: false,
126
+ });
127
+ const cleanBuffer = Buffer.from(cleanSvg, "utf8");
128
+ // Basic sanity checks
129
+ if (cleanBuffer.length > 256 * 1024) {
130
+ throw new Error("SVG too large (max 256 KB)");
131
+ }
132
+ return Promise.resolve({
133
+ width: 0, // SVG is scalable, can’t guarantee pixel dimensions
134
+ height: 0,
135
+ format: "svg",
136
+ size: cleanBuffer.length,
137
+ safeBuffer: cleanBuffer,
138
+ });
139
+ }
@@ -0,0 +1,2 @@
1
+ export declare function detectFormat(buffer: Buffer): "jpeg" | "png" | "webp" | "svg" | undefined;
2
+ //# sourceMappingURL=detectFormat.d.ts.map
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectFormat = detectFormat;
4
+ function detectFormat(buffer) {
5
+ if (buffer[0] === 0xff && buffer[1] === 0xd8) {
6
+ return "jpeg";
7
+ }
8
+ if (buffer[0] === 0x89 &&
9
+ buffer[1] === 0x50 &&
10
+ buffer[2] === 0x4e &&
11
+ buffer[3] === 0x47) {
12
+ return "png";
13
+ }
14
+ if (buffer.slice(0, 4).toString() === "RIFF" &&
15
+ buffer.slice(8, 12).toString() === "WEBP") {
16
+ return "webp";
17
+ }
18
+ const header = buffer.slice(0, 1000).toString("utf8").toLowerCase();
19
+ if (header.includes("<svg")) {
20
+ return "svg";
21
+ }
22
+ return undefined;
23
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./validators/index.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC"}
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./validators/index.js"), exports);
@@ -0,0 +1,5 @@
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
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./parseJpeg.js"), exports);
18
+ __exportStar(require("./parsePng.js"), exports);
19
+ __exportStar(require("./parseSVG.js"), exports);
20
+ __exportStar(require("./parseWebp.js"), exports);
@@ -0,0 +1,5 @@
1
+ export declare function parseJpeg(buffer: Buffer): {
2
+ width: number;
3
+ height: number;
4
+ };
5
+ //# sourceMappingURL=parseJpeg.d.ts.map
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseJpeg = parseJpeg;
4
+ function parseJpeg(buffer) {
5
+ let offset = 2;
6
+ while (offset < buffer.length) {
7
+ if (buffer[offset] !== 0xff) {
8
+ throw new Error("Invalid JPEG marker");
9
+ }
10
+ const marker = buffer[offset + 1];
11
+ const length = buffer.readUInt16BE(offset + 2);
12
+ // SOF0 (baseline), SOF2 (progressive)
13
+ if (marker === 0xc0 || marker === 0xc2) {
14
+ const height = buffer.readUInt16BE(offset + 5);
15
+ const width = buffer.readUInt16BE(offset + 7);
16
+ return { width, height };
17
+ }
18
+ offset += 2 + length;
19
+ }
20
+ throw new Error("JPEG SOF marker not found");
21
+ }
@@ -0,0 +1,5 @@
1
+ export declare function parsePng(buffer: Buffer): {
2
+ width: number;
3
+ height: number;
4
+ };
5
+ //# sourceMappingURL=parsePng.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parsePng.d.ts","sourceRoot":"","sources":["../../src/parse/parsePng.ts"],"names":[],"mappings":"AAAA,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,CAKA"}
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parsePng = parsePng;
4
+ function parsePng(buffer) {
5
+ // IHDR chunk is at byte 8+8 = 16
6
+ const width = buffer.readUInt32BE(16);
7
+ const height = buffer.readUInt32BE(20);
8
+ return { width, height };
9
+ }
@@ -0,0 +1,5 @@
1
+ export declare function parseSvg(buffer: Buffer): {
2
+ width: number;
3
+ height: number;
4
+ };
5
+ //# sourceMappingURL=parseSVG.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parseSVG.d.ts","sourceRoot":"","sources":["../../src/parse/parseSVG.ts"],"names":[],"mappings":"AAAA,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,CA0BA"}
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseSvg = parseSvg;
4
+ function parseSvg(buffer) {
5
+ const text = buffer.toString("utf8");
6
+ const matchViewBox = /viewBox\s*=\s*"([^"]+)"/i.exec(text);
7
+ const matchWidth = /width\s*=\s*"(\d+)(px)?"/i.exec(text);
8
+ const matchHeight = /height\s*=\s*"(\d+)(px)?"/i.exec(text);
9
+ let width;
10
+ let height;
11
+ if (matchWidth && matchHeight) {
12
+ width = parseInt(matchWidth[1], 10);
13
+ height = parseInt(matchHeight[1], 10);
14
+ }
15
+ else if (matchViewBox) {
16
+ const parts = matchViewBox[1].split(/\s+/);
17
+ if (parts.length === 4) {
18
+ width = parseFloat(parts[2]);
19
+ height = parseFloat(parts[3]);
20
+ }
21
+ }
22
+ if (!width || !height) {
23
+ throw new Error("Could not determine SVG dimensions");
24
+ }
25
+ return { width, height };
26
+ }
@@ -0,0 +1,5 @@
1
+ export declare function parseWebp(buffer: Buffer): {
2
+ width: number;
3
+ height: number;
4
+ };
5
+ //# sourceMappingURL=parseWebp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parseWebp.d.ts","sourceRoot":"","sources":["../../src/parse/parseWebp.ts"],"names":[],"mappings":"AAAA,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,CAeA"}
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseWebp = parseWebp;
4
+ function parseWebp(buffer) {
5
+ const chunkHeader = buffer.slice(12, 16).toString();
6
+ if (chunkHeader === "VP8X") {
7
+ const width = 1 + buffer.readUIntLE(24, 3);
8
+ const height = 1 + buffer.readUIntLE(27, 3);
9
+ return { width, height };
10
+ }
11
+ else if (chunkHeader === "VP8 ") {
12
+ // Simple lossless: parse frame header
13
+ const width = buffer.readUInt16LE(26) & 0x3fff;
14
+ const height = buffer.readUInt16LE(28) & 0x3fff;
15
+ return { width, height };
16
+ }
17
+ else {
18
+ throw new Error("Unsupported WEBP chunk type: " + chunkHeader);
19
+ }
20
+ }
@@ -0,0 +1,9 @@
1
+ export interface ImageValidationResult {
2
+ width: number;
3
+ height: number;
4
+ format: string;
5
+ size: number;
6
+ safeBuffer: Buffer;
7
+ }
8
+ export declare function validateAvatarImage(buffer: Buffer): Promise<ImageValidationResult>;
9
+ //# sourceMappingURL=avatarValidator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"avatarValidator.d.ts","sourceRoot":"","sources":["../../src/validators/avatarValidator.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,qBAAqB,CAAC,CA0DhC"}
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.validateAvatarImage = validateAvatarImage;
7
+ const sharp_1 = __importDefault(require("sharp"));
8
+ const svgValidator_js_1 = require("./svgValidator.js");
9
+ async function validateAvatarImage(buffer) {
10
+ const metadata = await (0, sharp_1.default)(buffer).metadata();
11
+ const format = metadata.format;
12
+ if (!format) {
13
+ throw new Error("Unsupported or unknown image format");
14
+ }
15
+ const width = metadata.width;
16
+ const height = metadata.height;
17
+ if (!width || !height) {
18
+ throw new Error("Could not determine image dimensions");
19
+ }
20
+ if (width < 64 || height < 64) {
21
+ throw new Error("Image too small (min 64x64)");
22
+ }
23
+ if (width > 2048 || height > 2048) {
24
+ throw new Error("Image too large (max 2048x2048)");
25
+ }
26
+ const targetFormat = "png"; // common safe web format
27
+ let safeBuffer;
28
+ if (format === "svg") {
29
+ const svgText = buffer.toString("utf8");
30
+ const svgValidation = await (0, svgValidator_js_1.validateSvgAvatar)(Buffer.from(svgText, "utf-8"));
31
+ safeBuffer = await (0, sharp_1.default)(svgValidation.safeBuffer)
32
+ .resize({
33
+ width: Math.min(width, 2048),
34
+ height: Math.min(height, 2048),
35
+ fit: "inside",
36
+ })
37
+ .toFormat(targetFormat)
38
+ .toBuffer();
39
+ }
40
+ else {
41
+ safeBuffer = await (0, sharp_1.default)(buffer)
42
+ .resize({
43
+ width: Math.min(width, 2048),
44
+ height: Math.min(height, 2048),
45
+ fit: "inside",
46
+ })
47
+ .toFormat(targetFormat, { quality: 90 })
48
+ .toBuffer();
49
+ }
50
+ return {
51
+ width,
52
+ height,
53
+ format,
54
+ size: buffer.length,
55
+ safeBuffer,
56
+ };
57
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./svgValidator.js";
2
+ export * from "./avatarValidator.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/validators/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC"}
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./svgValidator.js"), exports);
18
+ __exportStar(require("./avatarValidator.js"), exports);
@@ -0,0 +1,18 @@
1
+ import type { ImageValidationResult } from "./avatarValidator.js";
2
+ export declare function sanitiseSVG(svgText: string, options: {
3
+ allowedTags: string[];
4
+ allowedAttributes: {
5
+ "*": string[];
6
+ };
7
+ allowComments: boolean;
8
+ allowUnknownElements: boolean;
9
+ allowUnknownAttributes: boolean;
10
+ }): {
11
+ svg: string;
12
+ audit: {
13
+ strippedTags: string[];
14
+ strippedAttributes: string[];
15
+ };
16
+ };
17
+ export declare function validateSvgAvatar(buffer: Buffer): Promise<ImageValidationResult>;
18
+ //# sourceMappingURL=svgValidator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"svgValidator.d.ts","sourceRoot":"","sources":["../../src/validators/svgValidator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAIlE,wBAAgB,WAAW,CACzB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE;IACP,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,iBAAiB,EAAE;QAAE,GAAG,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACrC,aAAa,EAAE,OAAO,CAAC;IACvB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,sBAAsB,EAAE,OAAO,CAAC;CACjC,GACA;IACD,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE;QAAE,YAAY,EAAE,MAAM,EAAE,CAAC;QAAC,kBAAkB,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;CACjE,CA+FA;AAED,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,qBAAqB,CAAC,CAkEhC"}
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sanitiseSVG = sanitiseSVG;
4
+ exports.validateSvgAvatar = validateSvgAvatar;
5
+ const xmldom_1 = require("@xmldom/xmldom");
6
+ function sanitiseSVG(svgText, options) {
7
+ const strippedTags = [];
8
+ const strippedAttributes = [];
9
+ const parser = new xmldom_1.DOMParser({
10
+ onError: (level, msg) => {
11
+ switch (level) {
12
+ case "warning":
13
+ break;
14
+ default:
15
+ throw new Error(msg);
16
+ }
17
+ },
18
+ });
19
+ const serializer = new xmldom_1.XMLSerializer();
20
+ const doc = parser.parseFromString(svgText, "image/svg+xml");
21
+ function cleanNode(node) {
22
+ // TODO: optionally sanitize <style> elements and inline style attributes
23
+ if (node === null)
24
+ return;
25
+ if (node.nodeType === 8 /* Comment */ && !options.allowComments) {
26
+ node.parentNode?.removeChild(node);
27
+ return;
28
+ }
29
+ if (node.nodeType === 1 /* Element */) {
30
+ const el = node;
31
+ const tagName = el.tagName.toLowerCase();
32
+ // Strip <style> elements entirely
33
+ if (tagName === "style") {
34
+ strippedTags.push(tagName);
35
+ node.parentNode?.removeChild(node);
36
+ return;
37
+ }
38
+ if (!options.allowedTags.includes(tagName) &&
39
+ !options.allowUnknownElements) {
40
+ strippedTags.push(tagName);
41
+ node.parentNode?.removeChild(node);
42
+ return;
43
+ }
44
+ // Clean attributes
45
+ const allowedAttrs = options.allowedAttributes["*"] || [];
46
+ const toRemove = [];
47
+ for (const attr of Array.from(el.attributes)) {
48
+ const attrName = attr.name.toLowerCase();
49
+ if (attrName.startsWith("on")) {
50
+ toRemove.push(attr.name);
51
+ strippedAttributes.push(`${tagName}.${attr.name}`);
52
+ continue;
53
+ }
54
+ if (attrName === "xlink:href" || attrName === "xmlns:xlink") {
55
+ toRemove.push(attr.name);
56
+ strippedAttributes.push(`${tagName}.${attr.name}`);
57
+ continue;
58
+ }
59
+ if (attrName === "style") {
60
+ toRemove.push(attr.name);
61
+ strippedAttributes.push(`${tagName}.${attr.name}`);
62
+ continue;
63
+ }
64
+ if (!allowedAttrs.includes(attr.name) &&
65
+ !options.allowUnknownAttributes) {
66
+ toRemove.push(attr.name);
67
+ strippedAttributes.push(`${tagName}.${attr.name}`);
68
+ }
69
+ }
70
+ toRemove.forEach((attrName) => el.removeAttribute(attrName));
71
+ }
72
+ // Recursively clean children
73
+ const children = Array.from(node.childNodes);
74
+ children.forEach((child) => cleanNode(child));
75
+ }
76
+ cleanNode(doc.documentElement);
77
+ return {
78
+ svg: serializer.serializeToString(doc),
79
+ audit: { strippedTags, strippedAttributes },
80
+ };
81
+ }
82
+ function validateSvgAvatar(buffer) {
83
+ const svgText = buffer.toString("utf8");
84
+ const { svg: cleanSvg } = sanitiseSVG(svgText, {
85
+ allowedTags: [
86
+ "svg",
87
+ "g",
88
+ "rect",
89
+ "circle",
90
+ "ellipse",
91
+ "line",
92
+ "polygon",
93
+ "polyline",
94
+ "path",
95
+ "text",
96
+ "title",
97
+ "desc",
98
+ ],
99
+ allowedAttributes: {
100
+ "*": [
101
+ "width",
102
+ "height",
103
+ "viewBox",
104
+ "xmlns",
105
+ "transform",
106
+ "x",
107
+ "y",
108
+ "cx",
109
+ "cy",
110
+ "r",
111
+ "rx",
112
+ "ry",
113
+ "x1",
114
+ "y1",
115
+ "x2",
116
+ "y2",
117
+ "points",
118
+ "d",
119
+ "fill",
120
+ "stroke",
121
+ "stroke-width",
122
+ "font-family",
123
+ "font-size",
124
+ "text-anchor",
125
+ ],
126
+ },
127
+ allowComments: false,
128
+ allowUnknownElements: false,
129
+ allowUnknownAttributes: false,
130
+ });
131
+ const cleanBuffer = Buffer.from(cleanSvg, "utf8");
132
+ // Basic sanity checks
133
+ if (cleanBuffer.length > 256 * 1024) {
134
+ throw new Error("SVG too large (max 256 KB)");
135
+ }
136
+ return Promise.resolve({
137
+ width: 0, // SVG is scalable, can’t guarantee pixel dimensions
138
+ height: 0,
139
+ format: "svg",
140
+ size: cleanBuffer.length,
141
+ safeBuffer: cleanBuffer,
142
+ });
143
+ }
@@ -0,0 +1,21 @@
1
+ # ADR-0001: Standalone @plasius/images Package Scope
2
+
3
+ - Date: 2026-02-11
4
+ - Status: Accepted
5
+
6
+ ## Context
7
+
8
+ This package was previously maintained as a workspace-only module inside
9
+ `plasius-ltd-site`. External consumers and remote builds require it to be
10
+ installable from npm without monorepo-local links.
11
+
12
+ ## Decision
13
+
14
+ Move `@plasius/images` to a standalone root package with independent build,
15
+ test, governance, CI, and publish workflows.
16
+
17
+ ## Consequences
18
+
19
+ - The package can be versioned and released independently.
20
+ - `plasius-ltd-site` and other repositories can depend on npm-published versions.
21
+ - Build and lint rules must no longer rely on monorepo-relative tsconfig paths.