navis.js 4.0.0 → 5.2.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.
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Response Compression Middleware
3
+ * v5: Gzip and Brotli compression support
4
+ */
5
+
6
+ const zlib = require('zlib');
7
+
8
+ /**
9
+ * Compression middleware
10
+ * @param {Object} options - Compression options
11
+ * @returns {Function} - Middleware function
12
+ */
13
+ function compress(options = {}) {
14
+ const {
15
+ level = 6, // Compression level (1-9)
16
+ threshold = 1024, // Minimum size to compress (bytes)
17
+ algorithm = 'gzip', // 'gzip' or 'brotli'
18
+ filter = (req, res) => {
19
+ // Default: compress JSON and text responses
20
+ const contentType = res.headers?.['content-type'] || '';
21
+ return contentType.includes('application/json') ||
22
+ contentType.includes('text/') ||
23
+ contentType.includes('application/javascript');
24
+ },
25
+ } = options;
26
+
27
+ return async (req, res, next) => {
28
+ // Store original body setter
29
+ const originalBody = res.body;
30
+ const originalEnd = res.end || (() => {});
31
+
32
+ // Wrap response to compress before sending
33
+ res.end = function(...args) {
34
+ // Check if should compress
35
+ if (!filter(req, res)) {
36
+ return originalEnd.apply(this, args);
37
+ }
38
+
39
+ // Get response body
40
+ let body = res.body;
41
+ if (typeof body === 'object') {
42
+ body = JSON.stringify(body);
43
+ } else if (typeof body !== 'string') {
44
+ body = String(body);
45
+ }
46
+
47
+ // Check threshold
48
+ if (Buffer.byteLength(body, 'utf8') < threshold) {
49
+ return originalEnd.apply(this, args);
50
+ }
51
+
52
+ // Check if client supports compression
53
+ const acceptEncoding = req.headers['accept-encoding'] || req.headers['Accept-Encoding'] || '';
54
+ const supportsGzip = acceptEncoding.includes('gzip');
55
+ const supportsBrotli = acceptEncoding.includes('br');
56
+
57
+ // Choose compression algorithm
58
+ let compressed;
59
+ let encoding;
60
+
61
+ if (algorithm === 'brotli' && supportsBrotli) {
62
+ try {
63
+ compressed = zlib.brotliCompressSync(body, { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: level } });
64
+ encoding = 'br';
65
+ } catch (error) {
66
+ // Fallback to gzip if brotli fails
67
+ compressed = zlib.gzipSync(body, { level });
68
+ encoding = 'gzip';
69
+ }
70
+ } else if (supportsGzip) {
71
+ compressed = zlib.gzipSync(body, { level });
72
+ encoding = 'gzip';
73
+ } else {
74
+ // No compression support
75
+ return originalEnd.apply(this, args);
76
+ }
77
+
78
+ // Set compression headers
79
+ res.headers = res.headers || {};
80
+ res.headers['Content-Encoding'] = encoding;
81
+ res.headers['Vary'] = 'Accept-Encoding';
82
+
83
+ // Update content length
84
+ res.headers['Content-Length'] = compressed.length.toString();
85
+
86
+ // Update body with compressed data
87
+ res.body = compressed;
88
+
89
+ return originalEnd.apply(this, args);
90
+ };
91
+
92
+ next();
93
+ };
94
+ }
95
+
96
+ module.exports = compress;
97
+
@@ -0,0 +1,86 @@
1
+ /**
2
+ * CORS Middleware
3
+ * v5: Cross-Origin Resource Sharing support
4
+ */
5
+
6
+ /**
7
+ * CORS middleware
8
+ * @param {Object} options - CORS options
9
+ * @returns {Function} - Middleware function
10
+ */
11
+ function cors(options = {}) {
12
+ const {
13
+ origin = '*',
14
+ methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
15
+ allowedHeaders = ['Content-Type', 'Authorization', 'X-Requested-With'],
16
+ exposedHeaders = [],
17
+ credentials = false,
18
+ maxAge = 86400, // 24 hours
19
+ preflightContinue = false,
20
+ } = options;
21
+
22
+ // Normalize origin
23
+ const allowedOrigins = Array.isArray(origin) ? origin : [origin];
24
+ const isWildcard = allowedOrigins.includes('*');
25
+
26
+ return async (req, res, next) => {
27
+ const requestOrigin = req.headers.origin || req.headers.Origin;
28
+
29
+ // Determine allowed origin
30
+ let allowedOrigin = null;
31
+ if (isWildcard) {
32
+ allowedOrigin = '*';
33
+ } else if (requestOrigin && allowedOrigins.includes(requestOrigin)) {
34
+ allowedOrigin = requestOrigin;
35
+ } else if (allowedOrigins.length === 1 && allowedOrigins[0] !== '*') {
36
+ allowedOrigin = allowedOrigins[0];
37
+ }
38
+
39
+ // Handle preflight requests
40
+ if (req.method === 'OPTIONS') {
41
+ res.headers = res.headers || {};
42
+
43
+ if (allowedOrigin) {
44
+ res.headers['Access-Control-Allow-Origin'] = allowedOrigin;
45
+ }
46
+
47
+ res.headers['Access-Control-Allow-Methods'] = methods.join(', ');
48
+ res.headers['Access-Control-Allow-Headers'] = allowedHeaders.join(', ');
49
+ res.headers['Access-Control-Max-Age'] = maxAge.toString();
50
+
51
+ if (credentials) {
52
+ res.headers['Access-Control-Allow-Credentials'] = 'true';
53
+ }
54
+
55
+ if (exposedHeaders.length > 0) {
56
+ res.headers['Access-Control-Expose-Headers'] = exposedHeaders.join(', ');
57
+ }
58
+
59
+ if (!preflightContinue) {
60
+ res.statusCode = 204;
61
+ res.body = null;
62
+ return;
63
+ }
64
+ }
65
+
66
+ // Set CORS headers for all responses
67
+ res.headers = res.headers || {};
68
+
69
+ if (allowedOrigin) {
70
+ res.headers['Access-Control-Allow-Origin'] = allowedOrigin;
71
+ }
72
+
73
+ if (credentials && allowedOrigin && allowedOrigin !== '*') {
74
+ res.headers['Access-Control-Allow-Credentials'] = 'true';
75
+ }
76
+
77
+ if (exposedHeaders.length > 0) {
78
+ res.headers['Access-Control-Expose-Headers'] = exposedHeaders.join(', ');
79
+ }
80
+
81
+ next();
82
+ };
83
+ }
84
+
85
+ module.exports = cors;
86
+
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Security Headers Middleware
3
+ * v5: Security headers for protection against common attacks
4
+ */
5
+
6
+ /**
7
+ * Security headers middleware
8
+ * @param {Object} options - Security options
9
+ * @returns {Function} - Middleware function
10
+ */
11
+ function security(options = {}) {
12
+ const {
13
+ helmet = true,
14
+ hsts = true,
15
+ hstsMaxAge = 31536000, // 1 year
16
+ hstsIncludeSubDomains = true,
17
+ hstsPreload = false,
18
+ noSniff = true,
19
+ xssFilter = true,
20
+ frameOptions = 'DENY', // DENY, SAMEORIGIN, or false
21
+ contentSecurityPolicy = false,
22
+ cspDirectives = {},
23
+ referrerPolicy = 'no-referrer',
24
+ permissionsPolicy = {},
25
+ } = options;
26
+
27
+ return async (req, res, next) => {
28
+ res.headers = res.headers || {};
29
+
30
+ // X-Content-Type-Options
31
+ if (noSniff) {
32
+ res.headers['X-Content-Type-Options'] = 'nosniff';
33
+ }
34
+
35
+ // X-XSS-Protection
36
+ if (xssFilter) {
37
+ res.headers['X-XSS-Protection'] = '1; mode=block';
38
+ }
39
+
40
+ // X-Frame-Options
41
+ if (frameOptions) {
42
+ res.headers['X-Frame-Options'] = frameOptions;
43
+ }
44
+
45
+ // Strict-Transport-Security (HSTS)
46
+ if (hsts && req.headers['x-forwarded-proto'] === 'https') {
47
+ let hstsValue = `max-age=${hstsMaxAge}`;
48
+ if (hstsIncludeSubDomains) {
49
+ hstsValue += '; includeSubDomains';
50
+ }
51
+ if (hstsPreload) {
52
+ hstsValue += '; preload';
53
+ }
54
+ res.headers['Strict-Transport-Security'] = hstsValue;
55
+ }
56
+
57
+ // Content-Security-Policy
58
+ if (contentSecurityPolicy) {
59
+ const directives = {
60
+ 'default-src': ["'self'"],
61
+ 'script-src': ["'self'"],
62
+ 'style-src': ["'self'", "'unsafe-inline'"],
63
+ 'img-src': ["'self'", 'data:', 'https:'],
64
+ 'font-src': ["'self'"],
65
+ 'connect-src': ["'self'"],
66
+ ...cspDirectives,
67
+ };
68
+
69
+ const cspValue = Object.entries(directives)
70
+ .map(([key, values]) => {
71
+ const valuesStr = Array.isArray(values) ? values.join(' ') : values;
72
+ return `${key} ${valuesStr}`;
73
+ })
74
+ .join('; ');
75
+
76
+ res.headers['Content-Security-Policy'] = cspValue;
77
+ }
78
+
79
+ // Referrer-Policy
80
+ if (referrerPolicy) {
81
+ res.headers['Referrer-Policy'] = referrerPolicy;
82
+ }
83
+
84
+ // Permissions-Policy (formerly Feature-Policy)
85
+ if (Object.keys(permissionsPolicy).length > 0) {
86
+ const policyValue = Object.entries(permissionsPolicy)
87
+ .map(([feature, allowlist]) => {
88
+ const allowlistStr = Array.isArray(allowlist) ? allowlist.join(', ') : allowlist;
89
+ return `${feature}=${allowlistStr}`;
90
+ })
91
+ .join(', ');
92
+
93
+ res.headers['Permissions-Policy'] = policyValue;
94
+ }
95
+
96
+ // X-Powered-By removal (security through obscurity)
97
+ if (helmet) {
98
+ // Remove X-Powered-By if present
99
+ delete res.headers['X-Powered-By'];
100
+ }
101
+
102
+ next();
103
+ };
104
+ }
105
+
106
+ module.exports = security;
107
+
@@ -0,0 +1,151 @@
1
+ /**
2
+ * File Upload Middleware
3
+ * v5.1: Multipart form data and file upload handling
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const crypto = require('crypto');
9
+
10
+ /**
11
+ * File upload middleware
12
+ * @param {Object} options - Upload options
13
+ * @returns {Function} - Middleware function
14
+ */
15
+ function upload(options = {}) {
16
+ const {
17
+ dest = '/tmp/uploads',
18
+ limits = {
19
+ fileSize: 5 * 1024 * 1024, // 5MB default
20
+ files: 10, // Max 10 files
21
+ },
22
+ fileFilter = null, // Function to filter files
23
+ preserveExtension = true,
24
+ generateFilename = (file) => {
25
+ // Generate unique filename
26
+ const ext = preserveExtension ? path.extname(file.originalName || '') : '';
27
+ return `${crypto.randomBytes(16).toString('hex')}${ext}`;
28
+ },
29
+ } = options;
30
+
31
+ // Ensure destination directory exists
32
+ if (!fs.existsSync(dest)) {
33
+ fs.mkdirSync(dest, { recursive: true });
34
+ }
35
+
36
+ return async (req, res, next) => {
37
+ // Check content type
38
+ const contentType = req.headers['content-type'] || req.headers['Content-Type'] || '';
39
+
40
+ if (!contentType.includes('multipart/form-data')) {
41
+ return next();
42
+ }
43
+
44
+ // Parse multipart form data (simplified implementation)
45
+ // In production, use a library like busboy or multer
46
+ try {
47
+ req.files = [];
48
+ req.body = req.body || {};
49
+
50
+ // For Lambda, body is already parsed
51
+ if (req.event && req.event.isBase64Encoded) {
52
+ // Handle base64 encoded body
53
+ const body = Buffer.from(req.event.body, 'base64').toString();
54
+ // Parse multipart data (simplified)
55
+ // In production, use proper multipart parser
56
+ }
57
+
58
+ // For Node.js HTTP, parse from request stream
59
+ if (req.on && typeof req.on === 'function') {
60
+ await parseMultipart(req, dest, limits, fileFilter, generateFilename, (files, fields) => {
61
+ req.files = files;
62
+ req.body = { ...req.body, ...fields };
63
+ });
64
+ }
65
+
66
+ next();
67
+ } catch (error) {
68
+ res.statusCode = 400;
69
+ res.body = { error: error.message || 'File upload failed' };
70
+ }
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Parse multipart form data (simplified)
76
+ * @private
77
+ */
78
+ function parseMultipart(req, dest, limits, fileFilter, generateFilename, callback) {
79
+ return new Promise((resolve, reject) => {
80
+ const files = [];
81
+ const fields = {};
82
+ let totalSize = 0;
83
+ let fileCount = 0;
84
+
85
+ // Simplified multipart parser
86
+ // In production, use busboy or multer
87
+ let buffer = '';
88
+
89
+ req.on('data', (chunk) => {
90
+ buffer += chunk.toString();
91
+ totalSize += chunk.length;
92
+
93
+ if (totalSize > limits.fileSize) {
94
+ reject(new Error('File size exceeds limit'));
95
+ return;
96
+ }
97
+ });
98
+
99
+ req.on('end', () => {
100
+ // Basic multipart parsing (simplified)
101
+ // This is a placeholder - in production use proper parser
102
+ try {
103
+ callback(files, fields);
104
+ resolve();
105
+ } catch (error) {
106
+ reject(error);
107
+ }
108
+ });
109
+
110
+ req.on('error', reject);
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Save uploaded file
116
+ * @param {Object} file - File object
117
+ * @param {string} dest - Destination directory
118
+ * @param {Function} generateFilename - Filename generator
119
+ * @returns {Promise<string>} - File path
120
+ */
121
+ async function saveFile(file, dest, generateFilename) {
122
+ const filename = generateFilename(file);
123
+ const filepath = path.join(dest, filename);
124
+
125
+ // Ensure directory exists
126
+ const dir = path.dirname(filepath);
127
+ if (!fs.existsSync(dir)) {
128
+ fs.mkdirSync(dir, { recursive: true });
129
+ }
130
+
131
+ // Write file
132
+ if (file.buffer) {
133
+ fs.writeFileSync(filepath, file.buffer);
134
+ } else if (file.stream) {
135
+ // Handle stream
136
+ const writeStream = fs.createWriteStream(filepath);
137
+ file.stream.pipe(writeStream);
138
+ await new Promise((resolve, reject) => {
139
+ writeStream.on('finish', resolve);
140
+ writeStream.on('error', reject);
141
+ });
142
+ }
143
+
144
+ return filepath;
145
+ }
146
+
147
+ module.exports = {
148
+ upload,
149
+ saveFile,
150
+ };
151
+
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Server-Sent Events (SSE) Support
3
+ * v5.2: Real-time event streaming to clients
4
+ */
5
+
6
+ class SSEServer {
7
+ constructor() {
8
+ this.clients = new Map();
9
+ }
10
+
11
+ /**
12
+ * SSE middleware
13
+ * @returns {Function} - Middleware function
14
+ */
15
+ middleware() {
16
+ return async (req, res, next) => {
17
+ // Check if this is an SSE request
18
+ const accept = req.headers.accept || req.headers.Accept || '';
19
+
20
+ if (accept.includes('text/event-stream')) {
21
+ // Set SSE headers
22
+ res.headers = res.headers || {};
23
+ res.headers['Content-Type'] = 'text/event-stream';
24
+ res.headers['Cache-Control'] = 'no-cache';
25
+ res.headers['Connection'] = 'keep-alive';
26
+ res.headers['X-Accel-Buffering'] = 'no'; // Disable nginx buffering
27
+
28
+ // Create SSE client
29
+ const clientId = this._generateId();
30
+ const client = {
31
+ id: clientId,
32
+ req,
33
+ res,
34
+ send: (data, event = null, id = null) => this._send(client, data, event, id),
35
+ close: () => this._close(client),
36
+ };
37
+
38
+ this.clients.set(clientId, client);
39
+
40
+ // Send initial connection message
41
+ this._send(client, { type: 'connected', clientId }, 'connection', clientId);
42
+
43
+ // Handle client disconnect
44
+ req.on('close', () => {
45
+ this._close(client);
46
+ });
47
+
48
+ // Attach SSE methods to response
49
+ res.sse = {
50
+ send: (data, event, id) => client.send(data, event, id),
51
+ close: () => client.close(),
52
+ };
53
+
54
+ // Don't call next() - SSE keeps connection open
55
+ return;
56
+ }
57
+
58
+ next();
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Send SSE message
64
+ * @private
65
+ */
66
+ _send(client, data, event = null, id = null) {
67
+ if (!client.res || client.res.destroyed) {
68
+ return false;
69
+ }
70
+
71
+ let message = '';
72
+
73
+ // Event ID
74
+ if (id !== null) {
75
+ message += `id: ${id}\n`;
76
+ }
77
+
78
+ // Event type
79
+ if (event) {
80
+ message += `event: ${event}\n`;
81
+ }
82
+
83
+ // Data
84
+ const dataStr = typeof data === 'string' ? data : JSON.stringify(data);
85
+ message += `data: ${dataStr}\n\n`;
86
+
87
+ try {
88
+ // For Node.js HTTP response
89
+ if (client.res.write) {
90
+ client.res.write(message);
91
+ } else {
92
+ // For Lambda, accumulate messages
93
+ if (!client.res.sseMessages) {
94
+ client.res.sseMessages = [];
95
+ }
96
+ client.res.sseMessages.push(message);
97
+ }
98
+ return true;
99
+ } catch (error) {
100
+ console.error('SSE send error:', error);
101
+ return false;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Close SSE connection
107
+ * @private
108
+ */
109
+ _close(client) {
110
+ this.clients.delete(client.id);
111
+
112
+ if (client.res && client.res.end) {
113
+ try {
114
+ client.res.end();
115
+ } catch (error) {
116
+ // Ignore
117
+ }
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Broadcast to all clients
123
+ * @param {*} data - Message data
124
+ * @param {string} event - Event type
125
+ */
126
+ broadcast(data, event = null) {
127
+ for (const client of this.clients.values()) {
128
+ this._send(client, data, event);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Get all connected clients
134
+ * @returns {Array} - Array of client objects
135
+ */
136
+ getClients() {
137
+ return Array.from(this.clients.values());
138
+ }
139
+
140
+ /**
141
+ * Generate unique client ID
142
+ * @private
143
+ */
144
+ _generateId() {
145
+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Create SSE server
151
+ * @returns {SSEServer} - SSE server instance
152
+ */
153
+ function createSSEServer() {
154
+ return new SSEServer();
155
+ }
156
+
157
+ /**
158
+ * SSE middleware helper
159
+ * @returns {Function} - Middleware function
160
+ */
161
+ function sse() {
162
+ const sseServer = createSSEServer();
163
+ return sseServer.middleware();
164
+ }
165
+
166
+ module.exports = {
167
+ SSEServer,
168
+ createSSEServer,
169
+ sse,
170
+ };
171
+