@designofadecade/server 4.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 +62 -0
  2. package/LICENSE +21 -0
  3. package/README.md +297 -0
  4. package/dist/client/ApiClient.d.ts +121 -0
  5. package/dist/client/ApiClient.d.ts.map +1 -0
  6. package/dist/client/ApiClient.js +289 -0
  7. package/dist/client/ApiClient.js.map +1 -0
  8. package/dist/context/Context.d.ts +71 -0
  9. package/dist/context/Context.d.ts.map +1 -0
  10. package/dist/context/Context.js +81 -0
  11. package/dist/context/Context.js.map +1 -0
  12. package/dist/docs/OpenApiGenerator.d.ts +135 -0
  13. package/dist/docs/OpenApiGenerator.d.ts.map +1 -0
  14. package/dist/docs/OpenApiGenerator.js +165 -0
  15. package/dist/docs/OpenApiGenerator.js.map +1 -0
  16. package/dist/events/Events.d.ts +52 -0
  17. package/dist/events/Events.d.ts.map +1 -0
  18. package/dist/events/Events.js +70 -0
  19. package/dist/events/Events.js.map +1 -0
  20. package/dist/events/EventsManager.d.ts +46 -0
  21. package/dist/events/EventsManager.d.ts.map +1 -0
  22. package/dist/events/EventsManager.js +137 -0
  23. package/dist/events/EventsManager.js.map +1 -0
  24. package/dist/index.d.ts +32 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +38 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/local/Local.d.ts +83 -0
  29. package/dist/local/Local.d.ts.map +1 -0
  30. package/dist/local/Local.js +114 -0
  31. package/dist/local/Local.js.map +1 -0
  32. package/dist/logger/Logger.d.ts +365 -0
  33. package/dist/logger/Logger.d.ts.map +1 -0
  34. package/dist/logger/Logger.js +582 -0
  35. package/dist/logger/Logger.js.map +1 -0
  36. package/dist/middleware/RequestLogger.d.ts +62 -0
  37. package/dist/middleware/RequestLogger.d.ts.map +1 -0
  38. package/dist/middleware/RequestLogger.js +71 -0
  39. package/dist/middleware/RequestLogger.js.map +1 -0
  40. package/dist/notifications/Slack.d.ts +19 -0
  41. package/dist/notifications/Slack.d.ts.map +1 -0
  42. package/dist/notifications/Slack.js +55 -0
  43. package/dist/notifications/Slack.js.map +1 -0
  44. package/dist/router/RouteError.d.ts +21 -0
  45. package/dist/router/RouteError.d.ts.map +1 -0
  46. package/dist/router/RouteError.js +31 -0
  47. package/dist/router/RouteError.js.map +1 -0
  48. package/dist/router/Router.d.ts +66 -0
  49. package/dist/router/Router.d.ts.map +1 -0
  50. package/dist/router/Router.js +327 -0
  51. package/dist/router/Router.js.map +1 -0
  52. package/dist/router/Routes.d.ts +30 -0
  53. package/dist/router/Routes.d.ts.map +1 -0
  54. package/dist/router/Routes.js +52 -0
  55. package/dist/router/Routes.js.map +1 -0
  56. package/dist/router/StaticFileHandler.d.ts +44 -0
  57. package/dist/router/StaticFileHandler.d.ts.map +1 -0
  58. package/dist/router/StaticFileHandler.js +148 -0
  59. package/dist/router/StaticFileHandler.js.map +1 -0
  60. package/dist/sanitizer/HtmlSanitizer.d.ts +306 -0
  61. package/dist/sanitizer/HtmlSanitizer.d.ts.map +1 -0
  62. package/dist/sanitizer/HtmlSanitizer.js +808 -0
  63. package/dist/sanitizer/HtmlSanitizer.js.map +1 -0
  64. package/dist/server/Server.d.ts +28 -0
  65. package/dist/server/Server.d.ts.map +1 -0
  66. package/dist/server/Server.js +95 -0
  67. package/dist/server/Server.js.map +1 -0
  68. package/dist/state/AppState.d.ts +64 -0
  69. package/dist/state/AppState.d.ts.map +1 -0
  70. package/dist/state/AppState.js +89 -0
  71. package/dist/state/AppState.js.map +1 -0
  72. package/dist/utils/HtmlRenderer.d.ts +6 -0
  73. package/dist/utils/HtmlRenderer.d.ts.map +1 -0
  74. package/dist/utils/HtmlRenderer.js +128 -0
  75. package/dist/utils/HtmlRenderer.js.map +1 -0
  76. package/dist/websocket/WebSocketMessageFormatter.d.ts +40 -0
  77. package/dist/websocket/WebSocketMessageFormatter.d.ts.map +1 -0
  78. package/dist/websocket/WebSocketMessageFormatter.js +99 -0
  79. package/dist/websocket/WebSocketMessageFormatter.js.map +1 -0
  80. package/dist/websocket/WebSocketServer.d.ts +14 -0
  81. package/dist/websocket/WebSocketServer.d.ts.map +1 -0
  82. package/dist/websocket/WebSocketServer.js +138 -0
  83. package/dist/websocket/WebSocketServer.js.map +1 -0
  84. package/package.json +97 -0
@@ -0,0 +1,30 @@
1
+ import type Router from './Router.js';
2
+ import type { RouterRequest, RouterResponse, RouterMiddleware } from './Router.js';
3
+ import type Context from '../context/Context.js';
4
+ interface RouteRegistration {
5
+ path: string;
6
+ methods: string[];
7
+ pattern: URLPattern;
8
+ handler: (request: RouterRequest) => Promise<RouterResponse>;
9
+ middleware?: RouterMiddleware[];
10
+ }
11
+ export default class Routes {
12
+ #private;
13
+ /**
14
+ * Base path prefix for all routes in this class
15
+ * @type {string}
16
+ */
17
+ static basePath: string;
18
+ /**
19
+ * Array of nested route classes to register
20
+ * @type {Array}
21
+ */
22
+ static register: (new (router: Router, context?: Context) => Routes)[];
23
+ protected router: Router;
24
+ protected context?: Context;
25
+ constructor(router: Router, context?: Context);
26
+ get routerRoutes(): RouteRegistration[];
27
+ addRoute(path: string, methods: string | string[], handler: (request: RouterRequest) => Promise<RouterResponse>, middleware?: RouterMiddleware[]): void;
28
+ }
29
+ export {};
30
+ //# sourceMappingURL=Routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Routes.d.ts","sourceRoot":"","sources":["../../src/router/Routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AACtC,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACnF,OAAO,KAAK,OAAO,MAAM,uBAAuB,CAAC;AAEjD,UAAU,iBAAiB;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,UAAU,CAAC;IACpB,OAAO,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IAC7D,UAAU,CAAC,EAAE,gBAAgB,EAAE,CAAC;CACjC;AAED,MAAM,CAAC,OAAO,OAAO,MAAM;;IACzB;;;OAGG;IACH,MAAM,CAAC,QAAQ,SAAM;IAErB;;;OAGG;IACH,MAAM,CAAC,QAAQ,EAAE,CAAC,KAAK,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK,MAAM,CAAC,EAAE,CAAM;IAG5E,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;gBAEhB,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO;IAY7C,IAAI,YAAY,IAAI,iBAAiB,EAAE,CAEtC;IAED,QAAQ,CACN,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,EAC1B,OAAO,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,cAAc,CAAC,EAC5D,UAAU,CAAC,EAAE,gBAAgB,EAAE,GAC9B,IAAI;CAiCR"}
@@ -0,0 +1,52 @@
1
+ export default class Routes {
2
+ /**
3
+ * Base path prefix for all routes in this class
4
+ * @type {string}
5
+ */
6
+ static basePath = '';
7
+ /**
8
+ * Array of nested route classes to register
9
+ * @type {Array}
10
+ */
11
+ static register = [];
12
+ #routerRoutes = [];
13
+ router;
14
+ context;
15
+ constructor(router, context) {
16
+ this.router = router;
17
+ this.context = context;
18
+ this.constructor.register.forEach((RouteClass) => {
19
+ const route = new RouteClass(router, context);
20
+ this.#routerRoutes.push(...route.routerRoutes);
21
+ });
22
+ }
23
+ get routerRoutes() {
24
+ return this.#routerRoutes;
25
+ }
26
+ addRoute(path, methods, handler, middleware) {
27
+ // Validate inputs
28
+ if (typeof path !== 'string') {
29
+ throw new Error('Path must be a string');
30
+ }
31
+ if (typeof handler !== 'function') {
32
+ throw new Error('Handler must be a function');
33
+ }
34
+ const normalizedPath = `${this.constructor.basePath}${path}`.replace(/\/+/g, '/');
35
+ // Normalize methods to array
36
+ const methodsArray = Array.isArray(methods) ? methods : [methods];
37
+ // Validate HTTP methods
38
+ const validMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
39
+ const invalidMethods = methodsArray.filter((m) => !validMethods.includes(m));
40
+ if (invalidMethods.length > 0) {
41
+ throw new Error(`Invalid HTTP methods: ${invalidMethods.join(', ')}`);
42
+ }
43
+ this.#routerRoutes.push({
44
+ path: normalizedPath,
45
+ methods: methodsArray,
46
+ pattern: new URLPattern({ pathname: normalizedPath }),
47
+ handler,
48
+ ...(middleware && { middleware }),
49
+ });
50
+ }
51
+ }
52
+ //# sourceMappingURL=Routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Routes.js","sourceRoot":"","sources":["../../src/router/Routes.ts"],"names":[],"mappings":"AAYA,MAAM,CAAC,OAAO,OAAO,MAAM;IACzB;;;OAGG;IACH,MAAM,CAAC,QAAQ,GAAG,EAAE,CAAC;IAErB;;;OAGG;IACH,MAAM,CAAC,QAAQ,GAA0D,EAAE,CAAC;IAE5E,aAAa,GAAwB,EAAE,CAAC;IAC9B,MAAM,CAAS;IACf,OAAO,CAAW;IAE5B,YAAY,MAAc,EAAE,OAAiB;QAC3C,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QAEtB,IAAI,CAAC,WAA6B,CAAC,QAAQ,CAAC,OAAO,CAClD,CAAC,UAA6D,EAAE,EAAE;YAChE,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;YAC9C,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,YAAY,CAAC,CAAC;QACjD,CAAC,CACF,CAAC;IACJ,CAAC;IAED,IAAI,YAAY;QACd,OAAO,IAAI,CAAC,aAAa,CAAC;IAC5B,CAAC;IAED,QAAQ,CACN,IAAY,EACZ,OAA0B,EAC1B,OAA4D,EAC5D,UAA+B;QAE/B,kBAAkB;QAClB,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;QAC3C,CAAC;QAED,IAAI,OAAO,OAAO,KAAK,UAAU,EAAE,CAAC;YAClC,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;QAChD,CAAC;QAED,MAAM,cAAc,GAAG,GAAI,IAAI,CAAC,WAA6B,CAAC,QAAQ,GAAG,IAAI,EAAE,CAAC,OAAO,CACrF,MAAM,EACN,GAAG,CACJ,CAAC;QAEF,6BAA6B;QAC7B,MAAM,YAAY,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QAElE,wBAAwB;QACxB,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;QAClF,MAAM,cAAc,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7E,IAAI,cAAc,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CAAC,yBAAyB,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACxE,CAAC;QAED,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC;YACtB,IAAI,EAAE,cAAc;YACpB,OAAO,EAAE,YAAY;YACrB,OAAO,EAAE,IAAI,UAAU,CAAC,EAAE,QAAQ,EAAE,cAAc,EAAE,CAAC;YACrD,OAAO;YACP,GAAG,CAAC,UAAU,IAAI,EAAE,UAAU,EAAE,CAAC;SAClC,CAAC,CAAC;IACL,CAAC"}
@@ -0,0 +1,44 @@
1
+ interface ServeResponse {
2
+ status: number;
3
+ headers: Record<string, string>;
4
+ body: string | Buffer;
5
+ }
6
+ interface StaticFileHandlerOptions {
7
+ cacheControl?: string;
8
+ }
9
+ /**
10
+ * StaticFileHandler - Serves static files with security and proper MIME types
11
+ *
12
+ * Handles serving static files from a base directory with directory traversal
13
+ * protection, proper content types, and caching headers.
14
+ *
15
+ * @example
16
+ * const handler = new StaticFileHandler('./public');
17
+ * const response = await handler.serve('/assets/style.css');
18
+ */
19
+ export default class StaticFileHandler {
20
+ #private;
21
+ static MIME_TYPES: Record<string, string>;
22
+ /**
23
+ * Create a new StaticFileHandler
24
+ *
25
+ * @param {string} baseDir - Base directory to serve files from (absolute path)
26
+ * @param {Object} options - Configuration options
27
+ * @param {string} [options.cacheControl='public, max-age=3600'] - Cache-Control header value
28
+ * @throws {Error} If baseDir is not a non-empty string
29
+ */
30
+ constructor(baseDir: string, options?: StaticFileHandlerOptions);
31
+ /**
32
+ * Serve a static file
33
+ *
34
+ * @param {string} requestPath - The requested file path
35
+ * @returns {Promise<Object>} Response object with status, headers, and body
36
+ *
37
+ * @example
38
+ * const response = await handler.serve('/assets/style.css');
39
+ * // Returns: { status: 200, headers: {...}, body: Buffer }
40
+ */
41
+ serve(requestPath: string): Promise<ServeResponse>;
42
+ }
43
+ export {};
44
+ //# sourceMappingURL=StaticFileHandler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StaticFileHandler.d.ts","sourceRoot":"","sources":["../../src/router/StaticFileHandler.ts"],"names":[],"mappings":"AAIA,UAAU,aAAa;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;CACvB;AAED,UAAU,wBAAwB;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,OAAO,OAAO,iBAAiB;;IACpC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA0BvC;IAKF;;;;;;;OAOG;gBACS,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,wBAA6B;IAQnE;;;;;;;;;OASG;IACG,KAAK,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;CAsFzD"}
@@ -0,0 +1,148 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { logger } from '../logger/Logger.js';
4
+ /**
5
+ * StaticFileHandler - Serves static files with security and proper MIME types
6
+ *
7
+ * Handles serving static files from a base directory with directory traversal
8
+ * protection, proper content types, and caching headers.
9
+ *
10
+ * @example
11
+ * const handler = new StaticFileHandler('./public');
12
+ * const response = await handler.serve('/assets/style.css');
13
+ */
14
+ export default class StaticFileHandler {
15
+ static MIME_TYPES = {
16
+ '.html': 'text/html',
17
+ '.css': 'text/css',
18
+ '.js': 'text/javascript',
19
+ '.json': 'application/json',
20
+ '.png': 'image/png',
21
+ '.jpg': 'image/jpeg',
22
+ '.jpeg': 'image/jpeg',
23
+ '.gif': 'image/gif',
24
+ '.svg': 'image/svg+xml',
25
+ '.ico': 'image/x-icon',
26
+ '.woff': 'font/woff',
27
+ '.woff2': 'font/woff2',
28
+ '.ttf': 'font/ttf',
29
+ '.otf': 'font/otf',
30
+ '.eot': 'application/vnd.ms-fontobject',
31
+ '.webp': 'image/webp',
32
+ '.pdf': 'application/pdf',
33
+ '.xml': 'application/xml',
34
+ '.txt': 'text/plain',
35
+ '.wasm': 'application/wasm',
36
+ '.mp4': 'video/mp4',
37
+ '.webm': 'video/webm',
38
+ '.mp3': 'audio/mpeg',
39
+ '.wav': 'audio/wav',
40
+ '.zip': 'application/zip',
41
+ };
42
+ #baseDir;
43
+ #cacheControl;
44
+ /**
45
+ * Create a new StaticFileHandler
46
+ *
47
+ * @param {string} baseDir - Base directory to serve files from (absolute path)
48
+ * @param {Object} options - Configuration options
49
+ * @param {string} [options.cacheControl='public, max-age=3600'] - Cache-Control header value
50
+ * @throws {Error} If baseDir is not a non-empty string
51
+ */
52
+ constructor(baseDir, options = {}) {
53
+ if (!baseDir || typeof baseDir !== 'string') {
54
+ throw new Error('baseDir must be a non-empty string');
55
+ }
56
+ this.#baseDir = path.resolve(baseDir);
57
+ this.#cacheControl = options.cacheControl || 'public, max-age=3600';
58
+ }
59
+ /**
60
+ * Serve a static file
61
+ *
62
+ * @param {string} requestPath - The requested file path
63
+ * @returns {Promise<Object>} Response object with status, headers, and body
64
+ *
65
+ * @example
66
+ * const response = await handler.serve('/assets/style.css');
67
+ * // Returns: { status: 200, headers: {...}, body: Buffer }
68
+ */
69
+ async serve(requestPath) {
70
+ try {
71
+ // Security: prevent directory traversal
72
+ // Resolve the full path and ensure it's within the base directory
73
+ const filePath = path.resolve(this.#baseDir, '.' + requestPath);
74
+ // Ensure file is within base directory (using resolve to handle .. properly)
75
+ const resolvedBase = path.resolve(this.#baseDir);
76
+ if (!filePath.startsWith(resolvedBase + path.sep) && filePath !== resolvedBase)
77
+ return {
78
+ status: 403,
79
+ headers: { 'Content-Type': 'text/plain' },
80
+ body: 'Forbidden',
81
+ };
82
+ let stats = await fs.stat(filePath);
83
+ // Support index.html for directories
84
+ let finalPath = filePath;
85
+ if (stats.isDirectory()) {
86
+ finalPath = path.join(filePath, 'index.html');
87
+ try {
88
+ stats = await fs.stat(finalPath);
89
+ }
90
+ catch {
91
+ return {
92
+ status: 404,
93
+ headers: { 'Content-Type': 'text/plain' },
94
+ body: 'Not Found',
95
+ };
96
+ }
97
+ }
98
+ if (!stats.isFile())
99
+ return {
100
+ status: 404,
101
+ headers: { 'Content-Type': 'text/plain' },
102
+ body: 'Not Found',
103
+ };
104
+ const content = await fs.readFile(finalPath);
105
+ const ext = path.extname(finalPath).toLowerCase();
106
+ const contentType = StaticFileHandler.MIME_TYPES[ext] || 'application/octet-stream';
107
+ // Add charset for text-based content
108
+ const fullContentType = contentType.startsWith('text/') ||
109
+ contentType.includes('javascript') ||
110
+ contentType.includes('json')
111
+ ? `${contentType}; charset=utf-8`
112
+ : contentType;
113
+ return {
114
+ status: 200,
115
+ headers: {
116
+ 'Content-Type': fullContentType,
117
+ 'Cache-Control': this.#cacheControl,
118
+ 'Content-Length': stats.size.toString(),
119
+ 'Last-Modified': stats.mtime.toUTCString(),
120
+ 'X-Content-Type-Options': 'nosniff',
121
+ },
122
+ body: content,
123
+ };
124
+ }
125
+ catch (error) {
126
+ if (error.code === 'ENOENT') {
127
+ return {
128
+ status: 404,
129
+ headers: { 'Content-Type': 'text/plain' },
130
+ body: 'Not Found',
131
+ };
132
+ }
133
+ logger.error('Static file error', {
134
+ code: 'STATIC_FILE_ERROR',
135
+ source: 'StaticFileHandler.serve',
136
+ path: requestPath,
137
+ error,
138
+ errorCode: error.code,
139
+ });
140
+ return {
141
+ status: 500,
142
+ headers: { 'Content-Type': 'text/plain' },
143
+ body: 'Internal Server Error',
144
+ };
145
+ }
146
+ }
147
+ }
148
+ //# sourceMappingURL=StaticFileHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StaticFileHandler.js","sourceRoot":"","sources":["../../src/router/StaticFileHandler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,306 @@
1
+ /**
2
+ * HTML Sanitization Utility
3
+ *
4
+ * Production-grade HTML sanitization to prevent XSS attacks and ensure safe HTML rendering.
5
+ *
6
+ * Security Features:
7
+ * - XSS Prevention: Blocks dangerous protocols (javascript:, data:, vbscript:, etc.)
8
+ * - DoS Protection: Enforces maximum input size limits
9
+ * - Script/Style Removal: Completely removes script and style tags with content
10
+ * - HTML Comment Stripping: Removes comments that could hide malicious content
11
+ * - URL Validation: Validates and sanitizes URLs in anchor tags
12
+ * - Entity Decoding: Safely decodes HTML entities with range validation
13
+ * - Attribute Escaping: Prevents attribute injection attacks
14
+ *
15
+ * @module HtmlSanitizer
16
+ * @version 2.0.0
17
+ */
18
+ /**
19
+ * Result type for sanitization methods
20
+ */
21
+ export interface SanitizationResult {
22
+ readonly sanitized: string;
23
+ readonly warnings?: string[];
24
+ }
25
+ /**
26
+ * HTML Sanitization utility class with static methods for secure HTML processing
27
+ *
28
+ * @class HtmlSanitizer
29
+ */
30
+ export default class HtmlSanitizer {
31
+ /**
32
+ * Cleans HTML by allowing only specified tags and removing all others.
33
+ *
34
+ * Features:
35
+ * - Strips disallowed tags while preserving text content
36
+ * - Validates and sanitizes URLs in anchor tags
37
+ * - Removes HTML comments, script, and style tags
38
+ * - Enforces DoS protection with size limits
39
+ * - Validates allowed tags is a valid array
40
+ *
41
+ * @param html - The HTML string to clean
42
+ * @param allowedTags - Array of allowed tag names (case-insensitive)
43
+ * @returns Sanitized HTML string with only allowed tags
44
+ *
45
+ * @example
46
+ * // Allow only paragraph and bold tags
47
+ * HtmlSanitizer.clean('<p>Hello <b>World</b></p><script>alert("xss")</script>', ['p', 'b']);
48
+ * // Returns: '<p>Hello <b>World</b></p>'
49
+ *
50
+ * @example
51
+ * // Sanitize anchor tags with URL validation
52
+ * HtmlSanitizer.clean('<a href="https://example.com">Safe</a><a href="javascript:alert(1)">Unsafe</a>', ['a']);
53
+ * // Returns: '<a href="https://example.com">Safe</a><a>Unsafe</a>'
54
+ *
55
+ * @example
56
+ * // Allow common formatting tags
57
+ * HtmlSanitizer.clean(userInput, ['p', 'br', 'strong', 'em', 'a', 'ul', 'ol', 'li']);
58
+ */
59
+ static clean(html: string, allowedTags: string[]): string;
60
+ /**
61
+ * Sanitizes basic HTML content by allowing only b, i, u, and a tags.
62
+ * Common use case for simple user-generated content.
63
+ *
64
+ * @param html - The HTML string to sanitize
65
+ * @returns Sanitized HTML string with basic formatting
66
+ *
67
+ * @example
68
+ * HtmlSanitizer.sanitizeBasicHtml('<b>Bold</b> and <i>italic</i> text');
69
+ * // Returns: '<b>Bold</b> and <i>italic</i> text'
70
+ */
71
+ static sanitizeBasicHtml(html: string): string;
72
+ /**
73
+ * Replaces or adds attributes to specific HTML tags.
74
+ * Useful for adding email-specific attributes like target="_blank" to links.
75
+ *
76
+ * @param html - The HTML string to process
77
+ * @param tagName - The tag name to target (e.g., 'a')
78
+ * @param attributes - Object with attribute key-value pairs to add/replace
79
+ * @returns HTML with updated tag attributes
80
+ *
81
+ * @example
82
+ * HtmlSanitizer.replaceTagAttributes('<a href="/link">Click</a>', 'a', {
83
+ * target: '_blank',
84
+ * style: 'color: blue;'
85
+ * });
86
+ * // Returns: '<a href="/link" target="_blank" style="color: blue;">Click</a>'
87
+ */
88
+ static replaceTagAttributes(html: string, tagName: string, attributes: Record<string, string>): string;
89
+ /**
90
+ * Strips all HTML tags from content, leaving only plain text.
91
+ * Removes comments, scripts, styles, and decodes HTML entities.
92
+ *
93
+ * @param html - The HTML string to strip
94
+ * @returns Plain text without any HTML tags
95
+ *
96
+ * @example
97
+ * HtmlSanitizer.stripAllTags('<p>Hello <b>World</b></p>');
98
+ * // Returns: 'Hello World'
99
+ *
100
+ * @example
101
+ * HtmlSanitizer.stripAllTags('<script>alert(1)</script>Safe text');
102
+ * // Returns: 'Safe text'
103
+ */
104
+ static stripAllTags(html: string): string;
105
+ /**
106
+ * Alias for stripAllTags() - strips all HTML tags from content.
107
+ * Provided for convenience and backwards compatibility.
108
+ *
109
+ * @param html - The HTML string to strip
110
+ * @returns Plain text without HTML tags
111
+ * @see stripAllTags
112
+ *
113
+ * @example
114
+ * HtmlSanitizer.stripAll('<div>Content</div>');
115
+ * // Returns: 'Content'
116
+ */
117
+ static stripAll(html: string): string;
118
+ /**
119
+ * Sanitizes text for safe use in HTML attributes.
120
+ * Escapes characters that could break out of attribute context or inject code.
121
+ *
122
+ * Escapes: &, ", ', <, >, newlines, carriage returns
123
+ *
124
+ * @param text - The text to sanitize
125
+ * @returns HTML-safe text for use in attributes
126
+ *
127
+ * @example
128
+ * HtmlSanitizer.sanitizeForAttribute('value with "quotes"');
129
+ * // Returns: 'value with &quot;quotes&quot;'
130
+ *
131
+ * @example
132
+ * const value = HtmlSanitizer.sanitizeForAttribute(userInput);
133
+ * // Safe in: <div data-value="${value}">...</div>
134
+ */
135
+ static sanitizeForAttribute(text: string): string;
136
+ /**
137
+ * Sanitizes text for safe display in HTML content.
138
+ * Escapes HTML special characters to prevent XSS attacks.
139
+ *
140
+ * Escapes: &, <, >, ", '
141
+ *
142
+ * @param text - The text to sanitize
143
+ * @returns HTML-safe text that can be inserted into HTML content
144
+ *
145
+ * @example
146
+ * HtmlSanitizer.sanitizeForHtml('<script>alert("xss")</script>');
147
+ * // Returns: '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;'
148
+ *
149
+ * @example
150
+ * const safeText = HtmlSanitizer.sanitizeForHtml(userInput);
151
+ * // Safe in: <p>${safeText}</p>
152
+ */
153
+ static sanitizeForHtml(text: string): string;
154
+ /**
155
+ * Validates if a URL is safe for use in links.
156
+ * Blocks dangerous protocols and validates against common XSS vectors.
157
+ *
158
+ * Blocked protocols: javascript:, vbscript:, data:, file:, about:, blob:
159
+ * Allowed protocols: https://, http://, mailto:, tel:, sms:, relative paths
160
+ *
161
+ * @param url - The URL to validate
162
+ * @returns True if the URL is considered safe, false otherwise
163
+ *
164
+ * @example
165
+ * HtmlSanitizer.isValidUrl('https://example.com');
166
+ * // Returns: true
167
+ *
168
+ * @example
169
+ * HtmlSanitizer.isValidUrl('javascript:alert(1)');
170
+ * // Returns: false
171
+ *
172
+ * @example
173
+ * HtmlSanitizer.isValidUrl('/relative/path');
174
+ * // Returns: true
175
+ */
176
+ static isValidUrl(url: string): boolean;
177
+ /**
178
+ * Validates if an email address is properly formatted.
179
+ * Performs comprehensive email validation following RFC 5321/5322 standards.
180
+ *
181
+ * Validation Rules:
182
+ * - Maximum length: 254 characters (RFC 5321)
183
+ * - Local part (before @): 1-64 characters, allows a-z, 0-9, . _ + % -
184
+ * - Local part cannot start/end with dot or contain consecutive dots
185
+ * - Domain part (after @): 1-255 characters, requires valid TLD
186
+ * - Domain labels: 1-63 characters each, allows a-z, 0-9, hyphen (not at start/end)
187
+ * - Must contain exactly one @ symbol
188
+ * - Case-insensitive validation
189
+ *
190
+ * Security Features:
191
+ * - DoS protection with length limits
192
+ * - Rejects malformed patterns that could be used for injection
193
+ * - Validates against common email spoofing patterns
194
+ *
195
+ * Note: This performs format validation only. For production use, consider:
196
+ * - DNS MX record verification
197
+ * - Email verification (send confirmation email)
198
+ * - Third-party email validation services
199
+ * - Disposable email detection
200
+ *
201
+ * @param email - The email address to validate
202
+ * @returns True if email format is valid, false otherwise
203
+ *
204
+ * @example
205
+ * HtmlSanitizer.isValidEmail('user@example.com');
206
+ * // Returns: true
207
+ *
208
+ * @example
209
+ * HtmlSanitizer.isValidEmail('user+tag@subdomain.example.co.uk');
210
+ * // Returns: true
211
+ *
212
+ * @example
213
+ * HtmlSanitizer.isValidEmail('invalid.email');
214
+ * // Returns: false
215
+ *
216
+ * @example
217
+ * HtmlSanitizer.isValidEmail('user..name@example.com');
218
+ * // Returns: false (consecutive dots)
219
+ */
220
+ static isValidEmail(email: string): boolean;
221
+ /**
222
+ * Decodes common HTML entities to their character equivalents.
223
+ * Safely handles both named entities and numeric character references.
224
+ *
225
+ * Features:
226
+ * - Decodes common named entities (&amp;, &lt;, &nbsp;, etc.)
227
+ * - Decodes numeric entities (&#65; and &#x41; both -> 'A')
228
+ * - Validates character codes are in valid Unicode range
229
+ * - Excludes control characters (except tab, newline, CR)
230
+ *
231
+ * @param text - Text containing HTML entities
232
+ * @returns Text with entities decoded to characters
233
+ *
234
+ * @example
235
+ * HtmlSanitizer.decodeHtmlEntities('&lt;p&gt;Hello &amp; goodbye&lt;/p&gt;');
236
+ * // Returns: '<p>Hello & goodbye</p>'
237
+ *
238
+ * @example
239
+ * HtmlSanitizer.decodeHtmlEntities('&#65;&#66;&#67;'); // ABC
240
+ * HtmlSanitizer.decodeHtmlEntities('&#x41;&#x42;&#x43;'); // ABC
241
+ */
242
+ static decodeHtmlEntities(text: string): string;
243
+ }
244
+ /**
245
+ * USAGE GUIDELINES
246
+ *
247
+ * 1. USER-GENERATED CONTENT
248
+ * Always sanitize user input before storing or displaying:
249
+ * ```typescript
250
+ * // For rich text with limited formatting
251
+ * const sanitized = HtmlSanitizer.clean(userInput, ['p', 'br', 'strong', 'em', 'a']);
252
+ *
253
+ * // For plain text display
254
+ * const plainText = HtmlSanitizer.stripAll(userInput);
255
+ * ```
256
+ *
257
+ * 2. ATTRIBUTE VALUES
258
+ * Always escape text used in HTML attributes:
259
+ * ```typescript
260
+ * const safe = HtmlSanitizer.sanitizeForAttribute(userInput);
261
+ * html = `<div data-value="${safe}">...</div>`;
262
+ * ```
263
+ *
264
+ * 3. HTML CONTENT
265
+ * Escape text to be displayed as-is in HTML:
266
+ * ```typescript
267
+ * const safe = HtmlSanitizer.sanitizeForHtml(userInput);
268
+ * html = `<p>${safe}</p>`;
269
+ * ```
270
+ *
271
+ * 4. ALLOWED TAGS
272
+ * Choose the minimum set of tags needed:
273
+ * - Basic formatting: ['b', 'i', 'u', 'br']
274
+ * - Rich text: ['p', 'br', 'strong', 'em', 'a', 'ul', 'ol', 'li']
275
+ * - Extended: ['p', 'br', 'strong', 'em', 'u', 'a', 'ul', 'ol', 'li', 'h1', 'h2', 'h3']
276
+ *
277
+ * 5. URL VALIDATION
278
+ * URLs are automatically validated in anchor tags:
279
+ * - Allowed: https://, http://, mailto:, tel:, sms:, relative paths
280
+ * - Blocked: javascript:, data:, vbscript:, file:, blob:, about:
281
+ *
282
+ * 6. PERFORMANCE
283
+ * - Input size limited to 1MB (DoS protection)
284
+ * - Use stripAll() for better performance when HTML not needed
285
+ * - Cache sanitized content when possible
286
+ *
287
+ * SECURITY NOTES
288
+ *
289
+ * - XSS Prevention: All dangerous protocols and scripts are blocked
290
+ * - DoS Protection: Input size limits prevent resource exhaustion
291
+ * - Context-Aware: Different methods for different contexts (attributes vs content)
292
+ * - Defense in Depth: Multiple layers of validation and sanitization
293
+ * - Logging: Security events are logged to console for monitoring
294
+ *
295
+ * LIMITATIONS
296
+ *
297
+ * - Does not validate HTML structure (unclosed tags, nesting rules)
298
+ * - Does not support CSS sanitization
299
+ * - Does not support SVG sanitization
300
+ * - Maximum input size: 1MB
301
+ * - Maximum URL length: 2048 characters
302
+ *
303
+ * @see https://owasp.org/www-community/attacks/xss/
304
+ * @see https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
305
+ */
306
+ //# sourceMappingURL=HtmlSanitizer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HtmlSanitizer.d.ts","sourceRoot":"","sources":["../../src/sanitizer/HtmlSanitizer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAeH;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CAC9B;AAmCD;;;;GAIG;AACH,MAAM,CAAC,OAAO,OAAO,aAAa;IAKhC;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,MAAM;IA2IzD;;;;;;;;;;OAUG;IACH,MAAM,CAAC,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAI9C;;;;;;;;;;;;;;;OAeG;IACH,MAAM,CAAC,oBAAoB,CACzB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACjC,MAAM;IA4CT;;;;;;;;;;;;;;OAcG;IACH,MAAM,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAkCzC;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAQrC;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IA2BjD;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IA6B5C;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IA+EvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA0CG;IACH,MAAM,CAAC,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAuI3C;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,MAAM,CAAC,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;CAqFhD;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG"}