qhttpx 1.9.1 → 1.9.3
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.
- package/CHANGELOG.md +22 -0
- package/README.md +79 -17
- package/dist/examples/api-server.d.ts +1 -0
- package/dist/examples/api-server.js +77 -0
- package/dist/examples/basic.d.ts +1 -0
- package/dist/examples/basic.js +10 -0
- package/dist/examples/compression.d.ts +1 -0
- package/dist/examples/compression.js +17 -0
- package/dist/examples/cors.d.ts +1 -0
- package/dist/examples/cors.js +19 -0
- package/dist/examples/errors.d.ts +1 -0
- package/dist/examples/errors.js +25 -0
- package/dist/examples/file-upload.d.ts +1 -0
- package/dist/examples/file-upload.js +24 -0
- package/dist/examples/fusion.d.ts +1 -0
- package/dist/examples/fusion.js +21 -0
- package/dist/examples/rate-limiting.d.ts +1 -0
- package/dist/examples/rate-limiting.js +17 -0
- package/dist/examples/validation.d.ts +1 -0
- package/dist/examples/validation.js +23 -0
- package/dist/examples/websockets.d.ts +1 -0
- package/dist/examples/websockets.js +20 -0
- package/dist/package.json +112 -0
- package/dist/src/benchmarks/compare-frameworks.js +119 -0
- package/dist/src/benchmarks/compare.d.ts +1 -0
- package/dist/src/benchmarks/compare.js +288 -0
- package/dist/src/benchmarks/quantam-users.d.ts +1 -0
- package/dist/src/benchmarks/quantam-users.js +56 -0
- package/dist/src/benchmarks/simple-json.d.ts +1 -0
- package/dist/src/benchmarks/simple-json.js +60 -0
- package/dist/src/benchmarks/ultra-mode.d.ts +1 -0
- package/dist/src/benchmarks/ultra-mode.js +94 -0
- package/dist/src/buffer-pool.js +70 -0
- package/dist/src/cli/index.d.ts +2 -0
- package/dist/src/cli/index.js +222 -0
- package/dist/src/client/index.d.ts +17 -0
- package/dist/src/client/index.js +72 -0
- package/dist/src/config.js +50 -0
- package/dist/src/cookies.js +59 -0
- package/dist/src/core/batch.d.ts +24 -0
- package/dist/src/core/batch.js +97 -0
- package/dist/src/core/body-parser.d.ts +15 -0
- package/dist/src/core/body-parser.js +121 -0
- package/dist/src/core/buffer-pool.d.ts +41 -0
- package/dist/src/core/buffer-pool.js +70 -0
- package/dist/src/core/config.d.ts +7 -0
- package/dist/src/core/config.js +50 -0
- package/dist/src/core/errors.d.ts +34 -0
- package/dist/src/core/errors.js +70 -0
- package/dist/src/core/fusion.d.ts +20 -0
- package/dist/src/core/fusion.js +193 -0
- package/dist/src/core/logger.d.ts +22 -0
- package/dist/src/core/logger.js +49 -0
- package/dist/src/core/metrics.d.ts +48 -0
- package/dist/src/core/metrics.js +117 -0
- package/dist/src/core/native-adapter.d.ts +11 -0
- package/dist/src/core/native-adapter.js +211 -0
- package/dist/src/core/resources.d.ts +9 -0
- package/dist/src/core/resources.js +25 -0
- package/dist/src/core/scheduler.d.ts +34 -0
- package/dist/src/core/scheduler.js +85 -0
- package/dist/src/core/scope.d.ts +26 -0
- package/dist/src/core/scope.js +68 -0
- package/dist/src/core/serializer.d.ts +10 -0
- package/dist/src/core/serializer.js +44 -0
- package/dist/src/core/server.d.ts +138 -0
- package/dist/src/core/server.js +1082 -0
- package/dist/src/core/stream.d.ts +15 -0
- package/dist/src/core/stream.js +71 -0
- package/dist/src/core/tasks.d.ts +29 -0
- package/dist/src/core/tasks.js +87 -0
- package/dist/src/core/types.d.ts +173 -0
- package/dist/src/core/types.js +19 -0
- package/dist/src/core/websocket.d.ts +25 -0
- package/dist/src/core/websocket.js +86 -0
- package/dist/src/core/worker-queue.d.ts +41 -0
- package/dist/src/core/worker-queue.js +73 -0
- package/dist/src/cors.js +66 -0
- package/dist/src/database/adapters/memory.d.ts +21 -0
- package/dist/src/database/adapters/memory.js +90 -0
- package/dist/src/database/adapters/mongo.d.ts +11 -0
- package/dist/src/database/adapters/mongo.js +141 -0
- package/dist/src/database/adapters/postgres.d.ts +10 -0
- package/dist/src/database/adapters/postgres.js +111 -0
- package/dist/src/database/adapters/sqlite.d.ts +10 -0
- package/dist/src/database/adapters/sqlite.js +42 -0
- package/dist/src/database/coalescer.d.ts +14 -0
- package/dist/src/database/coalescer.js +134 -0
- package/dist/src/database/manager.d.ts +35 -0
- package/dist/src/database/manager.js +87 -0
- package/dist/src/database/types.d.ts +20 -0
- package/dist/src/database/types.js +2 -0
- package/dist/src/index.d.ts +50 -0
- package/dist/src/index.js +91 -0
- package/dist/src/logger.js +45 -0
- package/dist/src/metrics.js +111 -0
- package/dist/src/middleware/compression.d.ts +2 -0
- package/dist/src/middleware/compression.js +133 -0
- package/dist/src/middleware/cors.d.ts +2 -0
- package/dist/src/middleware/cors.js +66 -0
- package/dist/src/middleware/presets.d.ts +16 -0
- package/dist/src/middleware/presets.js +52 -0
- package/dist/src/middleware/rate-limit.d.ts +14 -0
- package/dist/src/middleware/rate-limit.js +83 -0
- package/dist/src/middleware/security.d.ts +21 -0
- package/dist/src/middleware/security.js +69 -0
- package/dist/src/middleware/static.d.ts +11 -0
- package/dist/src/middleware/static.js +191 -0
- package/dist/src/native/index.d.ts +32 -0
- package/dist/src/native/index.js +141 -0
- package/dist/src/openapi/generator.d.ts +19 -0
- package/dist/src/openapi/generator.js +149 -0
- package/dist/src/presets.js +33 -0
- package/dist/src/radix-router.js +89 -0
- package/dist/src/radix-tree.js +81 -0
- package/dist/src/resources.js +25 -0
- package/dist/src/router/radix-router.d.ts +18 -0
- package/dist/src/router/radix-router.js +89 -0
- package/dist/src/router/radix-tree.d.ts +18 -0
- package/dist/src/router/radix-tree.js +131 -0
- package/dist/src/router/router.d.ts +34 -0
- package/dist/src/router/router.js +186 -0
- package/dist/src/router.js +138 -0
- package/dist/src/scheduler.js +85 -0
- package/dist/src/security.js +69 -0
- package/dist/src/server.js +685 -0
- package/dist/src/signals.js +31 -0
- package/dist/src/static.js +107 -0
- package/dist/src/stream.js +71 -0
- package/dist/src/tasks.js +87 -0
- package/dist/src/testing/index.d.ts +25 -0
- package/dist/src/testing/index.js +84 -0
- package/dist/src/testing.js +40 -0
- package/dist/src/types.js +19 -0
- package/dist/src/utils/cookies.d.ts +3 -0
- package/dist/src/utils/cookies.js +59 -0
- package/dist/src/utils/logger.d.ts +12 -0
- package/dist/src/utils/logger.js +45 -0
- package/dist/src/utils/signals.d.ts +6 -0
- package/dist/src/utils/signals.js +31 -0
- package/dist/src/utils/sse.d.ts +6 -0
- package/dist/src/utils/sse.js +32 -0
- package/dist/src/utils/testing.js +40 -0
- package/dist/src/validation/index.d.ts +3 -0
- package/dist/src/validation/index.js +19 -0
- package/dist/src/validation/simple.d.ts +5 -0
- package/dist/src/validation/simple.js +102 -0
- package/dist/src/validation/types.d.ts +32 -0
- package/dist/src/validation/types.js +12 -0
- package/dist/src/validation/zod.d.ts +4 -0
- package/dist/src/validation/zod.js +18 -0
- package/dist/src/views/index.d.ts +1 -0
- package/dist/src/views/index.js +17 -0
- package/dist/src/views/types.d.ts +3 -0
- package/dist/src/views/types.js +2 -0
- package/dist/src/worker-queue.js +73 -0
- package/dist/tests/adapters.test.d.ts +1 -0
- package/dist/tests/adapters.test.js +106 -0
- package/dist/tests/batch.test.d.ts +1 -0
- package/dist/tests/batch.test.js +117 -0
- package/dist/tests/body-parser.test.d.ts +1 -0
- package/dist/tests/body-parser.test.js +52 -0
- package/dist/tests/compression-sse.test.d.ts +1 -0
- package/dist/tests/compression-sse.test.js +87 -0
- package/dist/tests/cookies.test.d.ts +1 -0
- package/dist/tests/cookies.test.js +63 -0
- package/dist/tests/cors.test.d.ts +1 -0
- package/dist/tests/cors.test.js +55 -0
- package/dist/tests/database.test.d.ts +1 -0
- package/dist/tests/database.test.js +80 -0
- package/dist/tests/dx.test.d.ts +1 -0
- package/dist/tests/dx.test.js +114 -0
- package/dist/tests/ecosystem.test.d.ts +1 -0
- package/dist/tests/ecosystem.test.js +133 -0
- package/dist/tests/features.test.d.ts +1 -0
- package/dist/tests/features.test.js +47 -0
- package/dist/tests/fusion.test.d.ts +1 -0
- package/dist/tests/fusion.test.js +92 -0
- package/dist/tests/http-basic.test.d.ts +1 -0
- package/dist/tests/http-basic.test.js +124 -0
- package/dist/tests/logger.test.d.ts +1 -0
- package/dist/tests/logger.test.js +33 -0
- package/dist/tests/middleware.test.d.ts +1 -0
- package/dist/tests/middleware.test.js +109 -0
- package/dist/tests/native-adapter.test.d.ts +1 -0
- package/dist/tests/native-adapter.test.js +71 -0
- package/dist/tests/observability.test.d.ts +1 -0
- package/dist/tests/observability.test.js +59 -0
- package/dist/tests/openapi.test.d.ts +1 -0
- package/dist/tests/openapi.test.js +64 -0
- package/dist/tests/plugin.test.d.ts +1 -0
- package/dist/tests/plugin.test.js +65 -0
- package/dist/tests/plugins.test.d.ts +1 -0
- package/dist/tests/plugins.test.js +71 -0
- package/dist/tests/rate-limit.test.d.ts +1 -0
- package/dist/tests/rate-limit.test.js +77 -0
- package/dist/tests/resources.test.d.ts +1 -0
- package/dist/tests/resources.test.js +47 -0
- package/dist/tests/scheduler.test.d.ts +1 -0
- package/dist/tests/scheduler.test.js +46 -0
- package/dist/tests/schema-routes.test.d.ts +1 -0
- package/dist/tests/schema-routes.test.js +77 -0
- package/dist/tests/security.test.d.ts +1 -0
- package/dist/tests/security.test.js +83 -0
- package/dist/tests/server-db.test.d.ts +1 -0
- package/dist/tests/server-db.test.js +72 -0
- package/dist/tests/smoke.test.d.ts +1 -0
- package/dist/tests/smoke.test.js +10 -0
- package/dist/tests/sqlite-fusion.test.d.ts +1 -0
- package/dist/tests/sqlite-fusion.test.js +92 -0
- package/dist/tests/static.test.d.ts +1 -0
- package/dist/tests/static.test.js +102 -0
- package/dist/tests/stream.test.d.ts +1 -0
- package/dist/tests/stream.test.js +44 -0
- package/dist/tests/task-metrics.test.d.ts +1 -0
- package/dist/tests/task-metrics.test.js +53 -0
- package/dist/tests/tasks.test.d.ts +1 -0
- package/dist/tests/tasks.test.js +62 -0
- package/dist/tests/testing.test.d.ts +1 -0
- package/dist/tests/testing.test.js +47 -0
- package/dist/tests/validation.test.d.ts +1 -0
- package/dist/tests/validation.test.js +107 -0
- package/dist/tests/websocket.test.d.ts +1 -0
- package/dist/tests/websocket.test.js +146 -0
- package/dist/vitest.config.d.ts +2 -0
- package/dist/vitest.config.js +9 -0
- package/docs/FUSION.md +19 -0
- package/package.json +4 -15
- package/prebuilds/darwin-arm64/qhttpx.node +0 -0
- package/prebuilds/linux-x64/qhttpx.node +0 -0
- package/prebuilds/win32-x64/qhttpx.node +0 -0
- package/scripts/install-native.js +26 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createApiPreset = createApiPreset;
|
|
4
|
+
exports.createStaticAppPreset = createStaticAppPreset;
|
|
5
|
+
const security_1 = require("./security");
|
|
6
|
+
const cors_1 = require("./cors");
|
|
7
|
+
const compression_1 = require("./compression");
|
|
8
|
+
const logger_1 = require("../utils/logger");
|
|
9
|
+
const static_1 = require("./static");
|
|
10
|
+
const rate_limit_1 = require("./rate-limit");
|
|
11
|
+
function createApiPreset(options = {}) {
|
|
12
|
+
const middlewares = [];
|
|
13
|
+
// 1. CORS
|
|
14
|
+
// Default to true (enabled) if not specified, to match createSecureDefaults behavior
|
|
15
|
+
const corsOpts = options.cors ?? options.security?.cors ?? true;
|
|
16
|
+
if (corsOpts !== false) {
|
|
17
|
+
const opts = corsOpts === true ? {} : corsOpts;
|
|
18
|
+
middlewares.push((0, cors_1.createCorsMiddleware)(opts));
|
|
19
|
+
}
|
|
20
|
+
// 2. Security Headers
|
|
21
|
+
middlewares.push((0, security_1.createSecurityHeadersMiddleware)(options.security?.securityHeaders));
|
|
22
|
+
// 3. Compression
|
|
23
|
+
if (options.compression) {
|
|
24
|
+
const opts = options.compression === true ? {} : options.compression;
|
|
25
|
+
middlewares.push((0, compression_1.createCompressionMiddleware)(opts));
|
|
26
|
+
}
|
|
27
|
+
// 4. Logging
|
|
28
|
+
if (options.logging !== false) {
|
|
29
|
+
const loggerOptions = typeof options.logging === 'object' ? options.logging : {};
|
|
30
|
+
middlewares.push((0, logger_1.createLoggerMiddleware)(loggerOptions));
|
|
31
|
+
}
|
|
32
|
+
// 5. Rate Limit
|
|
33
|
+
if (options.rateLimit) {
|
|
34
|
+
middlewares.push((0, rate_limit_1.rateLimit)(options.rateLimit));
|
|
35
|
+
}
|
|
36
|
+
return middlewares;
|
|
37
|
+
}
|
|
38
|
+
function createStaticAppPreset(options) {
|
|
39
|
+
const middlewares = [];
|
|
40
|
+
// 1. Security
|
|
41
|
+
middlewares.push(...(0, security_1.createSecureDefaults)(options.security));
|
|
42
|
+
// 2. Logging
|
|
43
|
+
if (options.logging !== false) {
|
|
44
|
+
const loggerOptions = typeof options.logging === 'object' ? options.logging : {};
|
|
45
|
+
middlewares.push((0, logger_1.createLoggerMiddleware)(loggerOptions));
|
|
46
|
+
}
|
|
47
|
+
// 3. Static Files
|
|
48
|
+
// We force fallthrough to true so API routes can handle non-static requests
|
|
49
|
+
const staticOptions = { ...options.static, fallthrough: true };
|
|
50
|
+
middlewares.push((0, static_1.createStaticMiddleware)(staticOptions));
|
|
51
|
+
return middlewares;
|
|
52
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { QHTTPXMiddleware, RateLimitOptions, RateLimitStore } from '../core/types';
|
|
2
|
+
export declare class MemoryStore implements RateLimitStore {
|
|
3
|
+
private hits;
|
|
4
|
+
private interval?;
|
|
5
|
+
constructor(clearPeriodMs?: number);
|
|
6
|
+
increment(key: string, windowMs: number): Promise<{
|
|
7
|
+
total: number;
|
|
8
|
+
resetTime: number;
|
|
9
|
+
}>;
|
|
10
|
+
decrement(key: string): Promise<void>;
|
|
11
|
+
reset(key: string): Promise<void>;
|
|
12
|
+
private cleanup;
|
|
13
|
+
}
|
|
14
|
+
export declare const rateLimit: (options?: RateLimitOptions) => QHTTPXMiddleware;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.rateLimit = exports.MemoryStore = void 0;
|
|
4
|
+
class MemoryStore {
|
|
5
|
+
constructor(clearPeriodMs = 60000) {
|
|
6
|
+
this.hits = new Map();
|
|
7
|
+
// Cleanup expired entries periodically
|
|
8
|
+
if (clearPeriodMs > 0) {
|
|
9
|
+
this.interval = setInterval(() => this.cleanup(), clearPeriodMs);
|
|
10
|
+
this.interval.unref(); // Don't hold process open
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
async increment(key, windowMs) {
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
let record = this.hits.get(key);
|
|
16
|
+
if (!record || now > record.resetTime) {
|
|
17
|
+
record = { count: 1, resetTime: now + windowMs };
|
|
18
|
+
this.hits.set(key, record);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
record.count++;
|
|
22
|
+
}
|
|
23
|
+
return { total: record.count, resetTime: record.resetTime };
|
|
24
|
+
}
|
|
25
|
+
async decrement(key) {
|
|
26
|
+
const record = this.hits.get(key);
|
|
27
|
+
if (record && record.count > 0) {
|
|
28
|
+
record.count--;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async reset(key) {
|
|
32
|
+
this.hits.delete(key);
|
|
33
|
+
}
|
|
34
|
+
cleanup() {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
for (const [key, record] of this.hits.entries()) {
|
|
37
|
+
if (now > record.resetTime) {
|
|
38
|
+
this.hits.delete(key);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
exports.MemoryStore = MemoryStore;
|
|
44
|
+
const rateLimit = (options = {}) => {
|
|
45
|
+
const windowMs = options.windowMs ?? 60000; // 1 minute default
|
|
46
|
+
const max = options.max ?? 100; // 100 requests default
|
|
47
|
+
const message = options.message ?? 'Too many requests, please try again later.';
|
|
48
|
+
const statusCode = options.statusCode ?? 429;
|
|
49
|
+
const headers = options.headers ?? true;
|
|
50
|
+
const store = options.store ?? new MemoryStore();
|
|
51
|
+
const keyGenerator = options.keyGenerator ?? ((ctx) => {
|
|
52
|
+
if (options.trustProxy) {
|
|
53
|
+
const forwarded = ctx.req.headers['x-forwarded-for'];
|
|
54
|
+
if (forwarded) {
|
|
55
|
+
return Array.isArray(forwarded) ? forwarded[0] : forwarded.split(',')[0].trim();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return ctx.req.socket.remoteAddress || 'unknown';
|
|
59
|
+
});
|
|
60
|
+
return async (ctx, next) => {
|
|
61
|
+
if (options.skip?.(ctx)) {
|
|
62
|
+
return next();
|
|
63
|
+
}
|
|
64
|
+
const key = keyGenerator(ctx);
|
|
65
|
+
const { total, resetTime } = await store.increment(key, windowMs);
|
|
66
|
+
const remaining = Math.max(0, max - total);
|
|
67
|
+
const resetSeconds = Math.ceil((resetTime - Date.now()) / 1000);
|
|
68
|
+
if (headers) {
|
|
69
|
+
ctx.res.setHeader('X-RateLimit-Limit', max);
|
|
70
|
+
ctx.res.setHeader('X-RateLimit-Remaining', remaining);
|
|
71
|
+
ctx.res.setHeader('X-RateLimit-Reset', resetSeconds);
|
|
72
|
+
}
|
|
73
|
+
if (total > max) {
|
|
74
|
+
if (headers) {
|
|
75
|
+
ctx.res.setHeader('Retry-After', resetSeconds);
|
|
76
|
+
}
|
|
77
|
+
ctx.json(typeof message === 'string' ? { error: message } : message, statusCode);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
await next();
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
exports.rateLimit = rateLimit;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { QHTTPXContext, QHTTPXMiddleware, CorsOptions } from '../core/types';
|
|
2
|
+
export type SecurityHeadersOptions = {
|
|
3
|
+
contentSecurityPolicy?: string | null;
|
|
4
|
+
referrerPolicy?: string | null;
|
|
5
|
+
xFrameOptions?: 'DENY' | 'SAMEORIGIN' | null;
|
|
6
|
+
xContentTypeOptions?: 'nosniff' | null;
|
|
7
|
+
xXssProtection?: '0' | '1; mode=block' | null;
|
|
8
|
+
strictTransportSecurity?: string | null;
|
|
9
|
+
};
|
|
10
|
+
export declare function createSecurityHeadersMiddleware(options?: SecurityHeadersOptions): QHTTPXMiddleware;
|
|
11
|
+
export type SecureDefaultsOptions = {
|
|
12
|
+
cors?: CorsOptions;
|
|
13
|
+
securityHeaders?: SecurityHeadersOptions;
|
|
14
|
+
};
|
|
15
|
+
export declare function createSecureDefaults(options?: SecureDefaultsOptions): QHTTPXMiddleware[];
|
|
16
|
+
export type RateLimitOptions = {
|
|
17
|
+
maxRequests: number;
|
|
18
|
+
windowMs: number;
|
|
19
|
+
keyGenerator?: (ctx: QHTTPXContext) => string;
|
|
20
|
+
};
|
|
21
|
+
export declare function createRateLimitMiddleware(options: RateLimitOptions): QHTTPXMiddleware;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSecurityHeadersMiddleware = createSecurityHeadersMiddleware;
|
|
4
|
+
exports.createSecureDefaults = createSecureDefaults;
|
|
5
|
+
exports.createRateLimitMiddleware = createRateLimitMiddleware;
|
|
6
|
+
const cors_1 = require("./cors");
|
|
7
|
+
function createSecurityHeadersMiddleware(options = {}) {
|
|
8
|
+
const { contentSecurityPolicy = "default-src 'self'", referrerPolicy = 'no-referrer', xFrameOptions = 'SAMEORIGIN', xContentTypeOptions = 'nosniff', xXssProtection = '1; mode=block', strictTransportSecurity, } = options;
|
|
9
|
+
return async (ctx, next) => {
|
|
10
|
+
if (contentSecurityPolicy) {
|
|
11
|
+
ctx.res.setHeader('content-security-policy', contentSecurityPolicy);
|
|
12
|
+
}
|
|
13
|
+
if (referrerPolicy) {
|
|
14
|
+
ctx.res.setHeader('referrer-policy', referrerPolicy);
|
|
15
|
+
}
|
|
16
|
+
if (xFrameOptions) {
|
|
17
|
+
ctx.res.setHeader('x-frame-options', xFrameOptions);
|
|
18
|
+
}
|
|
19
|
+
if (xContentTypeOptions) {
|
|
20
|
+
ctx.res.setHeader('x-content-type-options', xContentTypeOptions);
|
|
21
|
+
}
|
|
22
|
+
if (xXssProtection) {
|
|
23
|
+
ctx.res.setHeader('x-xss-protection', xXssProtection);
|
|
24
|
+
}
|
|
25
|
+
if (strictTransportSecurity) {
|
|
26
|
+
ctx.res.setHeader('strict-transport-security', strictTransportSecurity);
|
|
27
|
+
}
|
|
28
|
+
await next();
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function createSecureDefaults(options = {}) {
|
|
32
|
+
const middlewares = [];
|
|
33
|
+
middlewares.push((0, cors_1.createCorsMiddleware)(options.cors));
|
|
34
|
+
middlewares.push(createSecurityHeadersMiddleware(options.securityHeaders));
|
|
35
|
+
return middlewares;
|
|
36
|
+
}
|
|
37
|
+
function createRateLimitMiddleware(options) {
|
|
38
|
+
const maxRequests = options.maxRequests;
|
|
39
|
+
const windowMs = options.windowMs;
|
|
40
|
+
const keyGenerator = options.keyGenerator ??
|
|
41
|
+
((ctx) => {
|
|
42
|
+
const addr = ctx.req.socket.remoteAddress;
|
|
43
|
+
return addr || 'anonymous';
|
|
44
|
+
});
|
|
45
|
+
const buckets = new Map();
|
|
46
|
+
return async (ctx, next) => {
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
const key = keyGenerator(ctx);
|
|
49
|
+
const existing = buckets.get(key);
|
|
50
|
+
let bucket = existing;
|
|
51
|
+
if (!bucket || bucket.resetAt <= now) {
|
|
52
|
+
bucket = {
|
|
53
|
+
count: 0,
|
|
54
|
+
resetAt: now + windowMs,
|
|
55
|
+
};
|
|
56
|
+
buckets.set(key, bucket);
|
|
57
|
+
}
|
|
58
|
+
bucket.count += 1;
|
|
59
|
+
if (bucket.count > maxRequests) {
|
|
60
|
+
if (!ctx.res.headersSent) {
|
|
61
|
+
ctx.res.statusCode = 429;
|
|
62
|
+
ctx.res.setHeader('content-type', 'text/plain; charset=utf-8');
|
|
63
|
+
}
|
|
64
|
+
ctx.res.end('Too Many Requests');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
await next();
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { QHTTPXMiddleware } from '../core/types';
|
|
2
|
+
export type StaticOptions = {
|
|
3
|
+
root: string;
|
|
4
|
+
index?: string;
|
|
5
|
+
fallthrough?: boolean;
|
|
6
|
+
etag?: boolean;
|
|
7
|
+
lastModified?: boolean;
|
|
8
|
+
maxAge?: number;
|
|
9
|
+
immutable?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare function createStaticMiddleware(options: StaticOptions): QHTTPXMiddleware;
|
|
@@ -0,0 +1,191 @@
|
|
|
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.createStaticMiddleware = createStaticMiddleware;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
function guessContentType(filePath) {
|
|
10
|
+
const ext = path_1.default.extname(filePath).toLowerCase();
|
|
11
|
+
const types = {
|
|
12
|
+
'.html': 'text/html; charset=utf-8',
|
|
13
|
+
'.htm': 'text/html; charset=utf-8',
|
|
14
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
15
|
+
'.mjs': 'application/javascript; charset=utf-8',
|
|
16
|
+
'.css': 'text/css; charset=utf-8',
|
|
17
|
+
'.json': 'application/json; charset=utf-8',
|
|
18
|
+
'.png': 'image/png',
|
|
19
|
+
'.jpg': 'image/jpeg',
|
|
20
|
+
'.jpeg': 'image/jpeg',
|
|
21
|
+
'.gif': 'image/gif',
|
|
22
|
+
'.svg': 'image/svg+xml',
|
|
23
|
+
'.ico': 'image/x-icon',
|
|
24
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
25
|
+
'.mp4': 'video/mp4',
|
|
26
|
+
'.webm': 'video/webm',
|
|
27
|
+
'.mp3': 'audio/mpeg',
|
|
28
|
+
'.wav': 'audio/wav',
|
|
29
|
+
'.pdf': 'application/pdf',
|
|
30
|
+
'.zip': 'application/zip',
|
|
31
|
+
};
|
|
32
|
+
return types[ext] || 'application/octet-stream';
|
|
33
|
+
}
|
|
34
|
+
function generateETag(stat) {
|
|
35
|
+
const mtime = stat.mtimeMs.toString(16);
|
|
36
|
+
const size = stat.size.toString(16);
|
|
37
|
+
return `W/"${size}-${mtime}"`;
|
|
38
|
+
}
|
|
39
|
+
function createStaticMiddleware(options) {
|
|
40
|
+
const root = path_1.default.resolve(options.root);
|
|
41
|
+
const indexFile = options.index ?? 'index.html';
|
|
42
|
+
const fallthrough = options.fallthrough ?? false;
|
|
43
|
+
const etag = options.etag ?? true;
|
|
44
|
+
const lastModified = options.lastModified ?? true;
|
|
45
|
+
const maxAge = options.maxAge ?? 0;
|
|
46
|
+
const immutable = options.immutable ?? false;
|
|
47
|
+
return async (ctx, next) => {
|
|
48
|
+
const method = ctx.req.method || 'GET';
|
|
49
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
50
|
+
if (fallthrough) {
|
|
51
|
+
await next();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
ctx.res.statusCode = 405;
|
|
55
|
+
ctx.res.setHeader('Allow', 'GET, HEAD');
|
|
56
|
+
ctx.res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
57
|
+
ctx.res.end('Method Not Allowed');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
let requestPath = ctx.url.pathname || '/';
|
|
61
|
+
// Prevent directory traversal
|
|
62
|
+
requestPath = requestPath.replace(/^(\.\.(\/|\\|$))+/, '');
|
|
63
|
+
// Normalize path
|
|
64
|
+
let safePath = path_1.default.normalize(requestPath);
|
|
65
|
+
if (safePath.startsWith('..'))
|
|
66
|
+
safePath = '/'; // Extra safety
|
|
67
|
+
// Handle root/directory requests
|
|
68
|
+
let filePath = path_1.default.join(root, safePath);
|
|
69
|
+
let stat;
|
|
70
|
+
try {
|
|
71
|
+
stat = await fs_1.default.promises.stat(filePath);
|
|
72
|
+
if (stat.isDirectory()) {
|
|
73
|
+
filePath = path_1.default.join(filePath, indexFile);
|
|
74
|
+
stat = await fs_1.default.promises.stat(filePath);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
if (fallthrough) {
|
|
79
|
+
await next();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
ctx.res.statusCode = 404;
|
|
83
|
+
ctx.res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
84
|
+
ctx.res.end('Not Found');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (!stat.isFile()) {
|
|
88
|
+
if (fallthrough) {
|
|
89
|
+
await next();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
ctx.res.statusCode = 404;
|
|
93
|
+
ctx.res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
94
|
+
ctx.res.end('Not Found');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// Caching Headers
|
|
98
|
+
if (maxAge > 0 || immutable) {
|
|
99
|
+
let cacheControl = `public, max-age=${maxAge}`;
|
|
100
|
+
if (immutable)
|
|
101
|
+
cacheControl += ', immutable';
|
|
102
|
+
ctx.res.setHeader('Cache-Control', cacheControl);
|
|
103
|
+
}
|
|
104
|
+
if (lastModified) {
|
|
105
|
+
ctx.res.setHeader('Last-Modified', stat.mtime.toUTCString());
|
|
106
|
+
}
|
|
107
|
+
if (etag) {
|
|
108
|
+
const tag = generateETag(stat);
|
|
109
|
+
ctx.res.setHeader('ETag', tag);
|
|
110
|
+
// ETag Freshness Check
|
|
111
|
+
const ifNoneMatch = ctx.req.headers['if-none-match'];
|
|
112
|
+
if (ifNoneMatch === tag) {
|
|
113
|
+
ctx.res.statusCode = 304;
|
|
114
|
+
ctx.res.end();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Last-Modified Freshness Check
|
|
119
|
+
const ifModifiedSince = ctx.req.headers['if-modified-since'];
|
|
120
|
+
if (ifModifiedSince) {
|
|
121
|
+
const since = new Date(ifModifiedSince).getTime();
|
|
122
|
+
// Round down mtime to seconds for comparison
|
|
123
|
+
const mtime = Math.floor(stat.mtimeMs / 1000) * 1000;
|
|
124
|
+
if (mtime <= since) {
|
|
125
|
+
ctx.res.statusCode = 304;
|
|
126
|
+
ctx.res.end();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const contentType = guessContentType(filePath);
|
|
131
|
+
ctx.res.setHeader('Content-Type', contentType);
|
|
132
|
+
ctx.res.setHeader('Accept-Ranges', 'bytes');
|
|
133
|
+
// Range Support
|
|
134
|
+
const range = ctx.req.headers['range'];
|
|
135
|
+
let start = 0;
|
|
136
|
+
let end = stat.size - 1;
|
|
137
|
+
// let isRange = false;
|
|
138
|
+
if (range) {
|
|
139
|
+
const parts = range.replace(/bytes=/, '').split('-');
|
|
140
|
+
const partialStart = parts[0];
|
|
141
|
+
const partialEnd = parts[1];
|
|
142
|
+
const parsedStart = parseInt(partialStart, 10);
|
|
143
|
+
const parsedEnd = partialEnd ? parseInt(partialEnd, 10) : end;
|
|
144
|
+
if (!isNaN(parsedStart) && !isNaN(parsedEnd) && parsedStart <= parsedEnd && parsedEnd < stat.size) {
|
|
145
|
+
start = parsedStart;
|
|
146
|
+
end = parsedEnd;
|
|
147
|
+
// isRange = true;
|
|
148
|
+
ctx.res.statusCode = 206;
|
|
149
|
+
ctx.res.setHeader('Content-Range', `bytes ${start}-${end}/${stat.size}`);
|
|
150
|
+
ctx.res.setHeader('Content-Length', end - start + 1);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
// Invalid range
|
|
154
|
+
ctx.res.statusCode = 416;
|
|
155
|
+
ctx.res.setHeader('Content-Range', `bytes */${stat.size}`);
|
|
156
|
+
ctx.res.end();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
ctx.res.statusCode = 200;
|
|
162
|
+
ctx.res.setHeader('Content-Length', stat.size);
|
|
163
|
+
}
|
|
164
|
+
if (method === 'HEAD') {
|
|
165
|
+
ctx.res.end();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const stream = fs_1.default.createReadStream(filePath, { start, end });
|
|
169
|
+
// Disable auto-end if we are streaming?
|
|
170
|
+
// Actually, we can just pipe. But we need to make sure the server doesn't call res.end()
|
|
171
|
+
// The middleware architecture awaits next(), then server calls end().
|
|
172
|
+
// If we handle the response here, we should probably set a flag or just return without calling next() (which we do).
|
|
173
|
+
// But the server might still try to end it if we don't tell it we are done.
|
|
174
|
+
// In QHTTPX, if a handler returns, server checks `res.writableEnded`.
|
|
175
|
+
// Pipe calls end() by default.
|
|
176
|
+
// We need to wait for the stream to finish before returning from middleware
|
|
177
|
+
// so that the server doesn't race.
|
|
178
|
+
await new Promise((resolve, reject) => {
|
|
179
|
+
stream.pipe(ctx.res);
|
|
180
|
+
stream.on('end', resolve);
|
|
181
|
+
stream.on('error', (err) => {
|
|
182
|
+
stream.destroy();
|
|
183
|
+
reject(err);
|
|
184
|
+
});
|
|
185
|
+
ctx.res.on('close', () => {
|
|
186
|
+
stream.destroy();
|
|
187
|
+
resolve();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface NativeServerBinding {
|
|
2
|
+
parse(buffer: Buffer): {
|
|
3
|
+
method: string;
|
|
4
|
+
path: string;
|
|
5
|
+
version: number;
|
|
6
|
+
headers: Record<string, string>;
|
|
7
|
+
bodyOffset: number;
|
|
8
|
+
} | null;
|
|
9
|
+
createResponse(statusCode: number, headers: Record<string, string>, body?: string | Buffer): Buffer;
|
|
10
|
+
createJSONResponse(obj: unknown): Buffer;
|
|
11
|
+
writeResponse(fd: number, chunks: (Buffer | string)[]): void;
|
|
12
|
+
setCPUAffinity(cpuId: number): boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare class NativeServer {
|
|
15
|
+
private binding;
|
|
16
|
+
constructor();
|
|
17
|
+
get isAvailable(): boolean;
|
|
18
|
+
parse(buffer: Buffer): {
|
|
19
|
+
method: string;
|
|
20
|
+
path: string;
|
|
21
|
+
version: number;
|
|
22
|
+
headers: Record<string, string>;
|
|
23
|
+
bodyOffset: number;
|
|
24
|
+
} | null;
|
|
25
|
+
createResponse(statusCode: number, headers: Record<string, string>, body?: string | Buffer): Buffer<ArrayBufferLike>;
|
|
26
|
+
createJSONResponse(obj: unknown): Buffer<ArrayBufferLike>;
|
|
27
|
+
writeResponse(fd: number, chunks: (Buffer | string)[]): void;
|
|
28
|
+
setCPUAffinity(cpuId: number): boolean;
|
|
29
|
+
private disableNative;
|
|
30
|
+
private jsCreateResponse;
|
|
31
|
+
private jsCreateJSONResponse;
|
|
32
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
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.NativeServer = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
9
|
+
const loadBinding = require('node-gyp-build');
|
|
10
|
+
// EXPECTED ABI VERSION
|
|
11
|
+
const EXPECTED_ABI = 1;
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
let nativeBinding = null;
|
|
14
|
+
try {
|
|
15
|
+
// Look for prebuilds or built binary in root
|
|
16
|
+
const rootDir = __dirname.includes('dist')
|
|
17
|
+
? path_1.default.join(__dirname, '..', '..', '..')
|
|
18
|
+
: path_1.default.join(__dirname, '..', '..');
|
|
19
|
+
const binding = loadBinding(rootDir);
|
|
20
|
+
// 5. Version lock the ABI
|
|
21
|
+
if (binding && binding.abi === EXPECTED_ABI) {
|
|
22
|
+
nativeBinding = binding;
|
|
23
|
+
}
|
|
24
|
+
else if (binding) {
|
|
25
|
+
// Version mismatch - disable native
|
|
26
|
+
// console.warn(`QHTTPX: Native ABI mismatch (Expected: ${EXPECTED_ABI}, Got: ${binding.abi}). Falling back to JS.`);
|
|
27
|
+
nativeBinding = null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Graceful fallback to JS implementation
|
|
32
|
+
}
|
|
33
|
+
class NativeServer {
|
|
34
|
+
constructor() {
|
|
35
|
+
if (nativeBinding && nativeBinding.NativeServer) {
|
|
36
|
+
try {
|
|
37
|
+
this.binding = new nativeBinding.NativeServer();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
this.binding = null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
this.binding = null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
get isAvailable() {
|
|
48
|
+
return this.binding !== null;
|
|
49
|
+
}
|
|
50
|
+
// 4. Guard every native call
|
|
51
|
+
parse(buffer) {
|
|
52
|
+
if (this.binding) {
|
|
53
|
+
try {
|
|
54
|
+
return this.binding.parse(buffer);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
this.disableNative();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Fallback: If native parse fails, we return null.
|
|
61
|
+
// The NativeAdapter will see null and might close the connection or handle error.
|
|
62
|
+
// We cannot easily implement a full HTTP parser in JS here without adding dependencies.
|
|
63
|
+
// But since NativeAdapter falls back to http.Server if !isAvailable,
|
|
64
|
+
// disabling native here ensures subsequent requests use the safe path.
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
createResponse(statusCode, headers, body) {
|
|
68
|
+
if (this.binding) {
|
|
69
|
+
try {
|
|
70
|
+
return this.binding.createResponse(statusCode, headers, body);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
this.disableNative();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return this.jsCreateResponse(statusCode, headers, body);
|
|
77
|
+
}
|
|
78
|
+
createJSONResponse(obj) {
|
|
79
|
+
if (this.binding) {
|
|
80
|
+
try {
|
|
81
|
+
return this.binding.createJSONResponse(obj);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
this.disableNative();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Fallback: Use JSON.stringify + Buffer
|
|
88
|
+
return this.jsCreateJSONResponse(obj);
|
|
89
|
+
}
|
|
90
|
+
writeResponse(fd, chunks) {
|
|
91
|
+
if (this.binding) {
|
|
92
|
+
try {
|
|
93
|
+
return this.binding.writeResponse(fd, chunks);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
this.disableNative();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// No JS fallback for raw FD writes (requires net.Socket usually)
|
|
100
|
+
// The caller (NativeAdapter) should handle this by using socket.write()
|
|
101
|
+
// We throw here so caller knows to use fallback
|
|
102
|
+
throw new Error('Native writeResponse not available');
|
|
103
|
+
}
|
|
104
|
+
setCPUAffinity(cpuId) {
|
|
105
|
+
if (this.binding) {
|
|
106
|
+
try {
|
|
107
|
+
return this.binding.setCPUAffinity(cpuId);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
this.disableNative();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
disableNative() {
|
|
116
|
+
this.binding = null;
|
|
117
|
+
// console.warn('QHTTPX: Native module disabled due to error. Falling back to JS.');
|
|
118
|
+
}
|
|
119
|
+
// --- JS Fallbacks ---
|
|
120
|
+
jsCreateResponse(statusCode, headers, body) {
|
|
121
|
+
const statusMessage = statusCode === 200 ? 'OK' : 'Unknown'; // Simplified
|
|
122
|
+
let head = `HTTP/1.1 ${statusCode} ${statusMessage}\r\n`;
|
|
123
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
124
|
+
head += `${key}: ${value}\r\n`;
|
|
125
|
+
}
|
|
126
|
+
head += '\r\n';
|
|
127
|
+
const headBuf = Buffer.from(head);
|
|
128
|
+
if (body) {
|
|
129
|
+
const bodyBuf = Buffer.isBuffer(body) ? body : Buffer.from(body);
|
|
130
|
+
return Buffer.concat([headBuf, bodyBuf]);
|
|
131
|
+
}
|
|
132
|
+
return headBuf;
|
|
133
|
+
}
|
|
134
|
+
jsCreateJSONResponse(obj) {
|
|
135
|
+
const json = JSON.stringify(obj);
|
|
136
|
+
const contentLen = Buffer.byteLength(json);
|
|
137
|
+
const head = `HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: ${contentLen}\r\n\r\n`;
|
|
138
|
+
return Buffer.concat([Buffer.from(head), Buffer.from(json)]);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
exports.NativeServer = NativeServer;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Router } from '../router/router';
|
|
2
|
+
export type OpenAPIOptions = {
|
|
3
|
+
info: {
|
|
4
|
+
title: string;
|
|
5
|
+
version: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
};
|
|
8
|
+
servers?: {
|
|
9
|
+
url: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
}[];
|
|
12
|
+
};
|
|
13
|
+
export declare class OpenAPIGenerator {
|
|
14
|
+
private router;
|
|
15
|
+
private options;
|
|
16
|
+
constructor(router: Router, options: OpenAPIOptions);
|
|
17
|
+
generate(): object;
|
|
18
|
+
private convertSchema;
|
|
19
|
+
}
|