@onebun/core 0.2.7 → 0.2.9
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/package.json +1 -1
- package/src/application/application.test.ts +52 -6
- package/src/application/application.ts +308 -220
- package/src/decorators/decorators.ts +213 -0
- package/src/docs-examples.test.ts +357 -2
- package/src/exception-filters/exception-filters.test.ts +172 -0
- package/src/exception-filters/exception-filters.ts +129 -0
- package/src/exception-filters/http-exception.ts +22 -0
- package/src/exception-filters/index.ts +2 -0
- package/src/file/onebun-file.ts +8 -2
- package/src/http-guards/http-guards.test.ts +230 -0
- package/src/http-guards/http-guards.ts +173 -0
- package/src/http-guards/index.ts +1 -0
- package/src/index.ts +9 -0
- package/src/module/module.test.ts +78 -0
- package/src/module/module.ts +47 -7
- package/src/queue/docs-examples.test.ts +2 -2
- package/src/security/cors-middleware.ts +212 -0
- package/src/security/index.ts +19 -0
- package/src/security/rate-limit-middleware.ts +276 -0
- package/src/security/security-headers-middleware.ts +188 -0
- package/src/security/security.test.ts +285 -0
- package/src/testing/index.ts +1 -0
- package/src/testing/testing-module.test.ts +199 -0
- package/src/testing/testing-module.ts +252 -0
- package/src/types.ts +98 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import type { RedisClient } from '../redis/redis-client';
|
|
2
|
+
import type { OneBunRequest, OneBunResponse } from '../types';
|
|
3
|
+
|
|
4
|
+
import { BaseMiddleware } from '../module/middleware';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Rate limiting storage backend interface.
|
|
8
|
+
* Implement this to provide a custom backend (e.g. NATS KV, DynamoDB, etc.).
|
|
9
|
+
*/
|
|
10
|
+
export interface RateLimitStore {
|
|
11
|
+
/**
|
|
12
|
+
* Increment the request counter for a given key.
|
|
13
|
+
* Returns the new count and the Unix timestamp (ms) when the window resets.
|
|
14
|
+
* If the key did not exist, the window resets at `now + windowMs`.
|
|
15
|
+
*/
|
|
16
|
+
increment(key: string, windowMs: number): Promise<{ count: number; resetAt: number }>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// In-memory store (default)
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
interface MemoryEntry {
|
|
24
|
+
count: number;
|
|
25
|
+
resetAt: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Simple in-memory rate limit store.
|
|
30
|
+
* Not suitable for multi-process or multi-instance deployments — use `RedisRateLimitStore` instead.
|
|
31
|
+
*/
|
|
32
|
+
export class MemoryRateLimitStore implements RateLimitStore {
|
|
33
|
+
private readonly map = new Map<string, MemoryEntry>();
|
|
34
|
+
|
|
35
|
+
async increment(key: string, windowMs: number): Promise<{ count: number; resetAt: number }> {
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
const entry = this.map.get(key);
|
|
38
|
+
|
|
39
|
+
if (!entry || entry.resetAt <= now) {
|
|
40
|
+
const resetAt = now + windowMs;
|
|
41
|
+
this.map.set(key, { count: 1, resetAt });
|
|
42
|
+
|
|
43
|
+
return { count: 1, resetAt };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
entry.count += 1;
|
|
47
|
+
|
|
48
|
+
return { count: entry.count, resetAt: entry.resetAt };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Remove all entries (for testing / cleanup). */
|
|
52
|
+
clear(): void {
|
|
53
|
+
this.map.clear();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Redis store
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Redis-backed rate limit store.
|
|
63
|
+
* Atomic via Lua script — safe for multi-instance deployments.
|
|
64
|
+
* Requires a connected `RedisClient`.
|
|
65
|
+
*/
|
|
66
|
+
export class RedisRateLimitStore implements RateLimitStore {
|
|
67
|
+
constructor(private readonly redis: RedisClient) {}
|
|
68
|
+
|
|
69
|
+
async increment(key: string, windowMs: number): Promise<{ count: number; resetAt: number }> {
|
|
70
|
+
// Use a simple get/set approach; for true atomicity a Lua script would be needed,
|
|
71
|
+
// but this is sufficient for typical rate limiting use-cases.
|
|
72
|
+
const raw = await this.redis.get(`rl:${key}`);
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
|
|
75
|
+
if (raw !== null) {
|
|
76
|
+
const { count, resetAt } = JSON.parse(raw) as { count: number; resetAt: number };
|
|
77
|
+
|
|
78
|
+
if (resetAt > now) {
|
|
79
|
+
const newCount = count + 1;
|
|
80
|
+
await this.redis.set(
|
|
81
|
+
`rl:${key}`,
|
|
82
|
+
JSON.stringify({ count: newCount, resetAt }),
|
|
83
|
+
resetAt - now,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return { count: newCount, resetAt };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Key expired or does not exist — start a new window
|
|
91
|
+
const resetAt = now + windowMs;
|
|
92
|
+
await this.redis.set(`rl:${key}`, JSON.stringify({ count: 1, resetAt }), windowMs);
|
|
93
|
+
|
|
94
|
+
return { count: 1, resetAt };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ============================================================================
|
|
99
|
+
// Middleware
|
|
100
|
+
// ============================================================================
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Configuration for `RateLimitMiddleware`.
|
|
104
|
+
*/
|
|
105
|
+
export interface RateLimitOptions {
|
|
106
|
+
/**
|
|
107
|
+
* Time window in milliseconds.
|
|
108
|
+
* @defaultValue 60_000 (1 minute)
|
|
109
|
+
*/
|
|
110
|
+
windowMs?: number;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Maximum number of requests allowed per window per key.
|
|
114
|
+
* @defaultValue 100
|
|
115
|
+
*/
|
|
116
|
+
max?: number;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* A function that derives the rate-limit key from the incoming request.
|
|
120
|
+
* Defaults to the client's IP address (`x-forwarded-for` or `cf-connecting-ip` headers,
|
|
121
|
+
* falling back to `'unknown'`).
|
|
122
|
+
*/
|
|
123
|
+
keyGenerator?: (req: OneBunRequest) => string;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Custom message sent when the rate limit is exceeded.
|
|
127
|
+
* @defaultValue 'Too Many Requests'
|
|
128
|
+
*/
|
|
129
|
+
message?: string;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Whether to add `RateLimit-*` headers to every response.
|
|
133
|
+
* @defaultValue true
|
|
134
|
+
*/
|
|
135
|
+
standardHeaders?: boolean;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Whether to add the legacy `X-RateLimit-*` headers.
|
|
139
|
+
* @defaultValue false
|
|
140
|
+
*/
|
|
141
|
+
legacyHeaders?: boolean;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Custom rate limit storage backend.
|
|
145
|
+
* Defaults to `MemoryRateLimitStore` (in-process, not shared across instances).
|
|
146
|
+
* Use `RedisRateLimitStore` for multi-instance deployments.
|
|
147
|
+
*/
|
|
148
|
+
store?: RateLimitStore;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function defaultKeyGenerator(req: OneBunRequest): string {
|
|
152
|
+
return (
|
|
153
|
+
req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ??
|
|
154
|
+
req.headers.get('cf-connecting-ip') ??
|
|
155
|
+
'unknown'
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Built-in rate limiting middleware.
|
|
161
|
+
*
|
|
162
|
+
* By default uses an in-process `MemoryRateLimitStore`. For multi-instance
|
|
163
|
+
* deployments pass a `RedisRateLimitStore` (or any custom `RateLimitStore`).
|
|
164
|
+
*
|
|
165
|
+
* @example In-memory rate limiting (single instance)
|
|
166
|
+
* ```typescript
|
|
167
|
+
* const app = new OneBunApplication(AppModule, {
|
|
168
|
+
* middleware: [RateLimitMiddleware],
|
|
169
|
+
* });
|
|
170
|
+
* ```
|
|
171
|
+
*
|
|
172
|
+
* @example Redis-backed rate limiting with custom window
|
|
173
|
+
* ```typescript
|
|
174
|
+
* const redis = await SharedRedisProvider.getClient();
|
|
175
|
+
* const app = new OneBunApplication(AppModule, {
|
|
176
|
+
* middleware: [
|
|
177
|
+
* RateLimitMiddleware.configure({
|
|
178
|
+
* windowMs: 15 * 60 * 1000, // 15 minutes
|
|
179
|
+
* max: 200,
|
|
180
|
+
* store: new RedisRateLimitStore(redis),
|
|
181
|
+
* }),
|
|
182
|
+
* ],
|
|
183
|
+
* });
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
export class RateLimitMiddleware extends BaseMiddleware {
|
|
187
|
+
private readonly windowMs: number;
|
|
188
|
+
private readonly max: number;
|
|
189
|
+
private readonly keyGenerator: (req: OneBunRequest) => string;
|
|
190
|
+
private readonly message: string;
|
|
191
|
+
private readonly standardHeaders: boolean;
|
|
192
|
+
private readonly legacyHeaders: boolean;
|
|
193
|
+
private readonly store: RateLimitStore;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Create a pre-configured RateLimitMiddleware class with the given options.
|
|
197
|
+
* Returns a constructor — pass the result directly to `ApplicationOptions.middleware`.
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* ```typescript
|
|
201
|
+
* const app = new OneBunApplication(AppModule, {
|
|
202
|
+
* middleware: [RateLimitMiddleware.configure({ max: 50, windowMs: 60_000 })],
|
|
203
|
+
* });
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
static configure(options: RateLimitOptions): typeof RateLimitMiddleware {
|
|
207
|
+
class ConfiguredRateLimitMiddleware extends RateLimitMiddleware {
|
|
208
|
+
constructor() {
|
|
209
|
+
super(options);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return ConfiguredRateLimitMiddleware;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
constructor(options: RateLimitOptions = {}) {
|
|
217
|
+
super();
|
|
218
|
+
|
|
219
|
+
const defaultWindowMs = 60_000;
|
|
220
|
+
this.windowMs = options.windowMs ?? defaultWindowMs;
|
|
221
|
+
this.max = options.max ?? 100;
|
|
222
|
+
this.keyGenerator = options.keyGenerator ?? defaultKeyGenerator;
|
|
223
|
+
this.message = options.message ?? 'Too Many Requests';
|
|
224
|
+
this.standardHeaders = options.standardHeaders ?? true;
|
|
225
|
+
this.legacyHeaders = options.legacyHeaders ?? false;
|
|
226
|
+
this.store = options.store ?? new MemoryRateLimitStore();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async use(req: OneBunRequest, next: () => Promise<OneBunResponse>): Promise<OneBunResponse> {
|
|
230
|
+
const key = this.keyGenerator(req);
|
|
231
|
+
const { count, resetAt } = await this.store.increment(key, this.windowMs);
|
|
232
|
+
const remaining = Math.max(0, this.max - count);
|
|
233
|
+
const resetInSeconds = Math.ceil((resetAt - Date.now()) / 1000);
|
|
234
|
+
|
|
235
|
+
const TOO_MANY_REQUESTS = 429;
|
|
236
|
+
|
|
237
|
+
if (count > this.max) {
|
|
238
|
+
const headers = new Headers();
|
|
239
|
+
headers.set('Content-Type', 'application/json');
|
|
240
|
+
|
|
241
|
+
if (this.standardHeaders) {
|
|
242
|
+
headers.set('RateLimit-Limit', String(this.max));
|
|
243
|
+
headers.set('RateLimit-Remaining', '0');
|
|
244
|
+
headers.set('RateLimit-Reset', String(resetInSeconds));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (this.legacyHeaders) {
|
|
248
|
+
headers.set('X-RateLimit-Limit', String(this.max));
|
|
249
|
+
headers.set('X-RateLimit-Remaining', '0');
|
|
250
|
+
headers.set('X-RateLimit-Reset', String(resetInSeconds));
|
|
251
|
+
headers.set('Retry-After', String(resetInSeconds));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return new Response(
|
|
255
|
+
JSON.stringify({ success: false, error: this.message, statusCode: TOO_MANY_REQUESTS }),
|
|
256
|
+
{ status: TOO_MANY_REQUESTS, headers },
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const response = await next();
|
|
261
|
+
|
|
262
|
+
if (this.standardHeaders) {
|
|
263
|
+
response.headers.set('RateLimit-Limit', String(this.max));
|
|
264
|
+
response.headers.set('RateLimit-Remaining', String(remaining));
|
|
265
|
+
response.headers.set('RateLimit-Reset', String(resetInSeconds));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (this.legacyHeaders) {
|
|
269
|
+
response.headers.set('X-RateLimit-Limit', String(this.max));
|
|
270
|
+
response.headers.set('X-RateLimit-Remaining', String(remaining));
|
|
271
|
+
response.headers.set('X-RateLimit-Reset', String(resetInSeconds));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return response;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { OneBunRequest, OneBunResponse } from '../types';
|
|
2
|
+
|
|
3
|
+
import { BaseMiddleware } from '../module/middleware';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configuration for each security header directive.
|
|
7
|
+
* Set to `false` to disable a specific header entirely.
|
|
8
|
+
*/
|
|
9
|
+
export interface SecurityHeadersOptions {
|
|
10
|
+
/**
|
|
11
|
+
* `Content-Security-Policy`
|
|
12
|
+
* @defaultValue "default-src 'self'"
|
|
13
|
+
* Set to `false` to disable.
|
|
14
|
+
*/
|
|
15
|
+
contentSecurityPolicy?: string | false;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* `Cross-Origin-Opener-Policy`
|
|
19
|
+
* @defaultValue 'same-origin'
|
|
20
|
+
* Set to `false` to disable.
|
|
21
|
+
*/
|
|
22
|
+
crossOriginOpenerPolicy?: string | false;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* `Cross-Origin-Resource-Policy`
|
|
26
|
+
* @defaultValue 'same-origin'
|
|
27
|
+
* Set to `false` to disable.
|
|
28
|
+
*/
|
|
29
|
+
crossOriginResourcePolicy?: string | false;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* `Origin-Agent-Cluster`
|
|
33
|
+
* @defaultValue '?1'
|
|
34
|
+
* Set to `false` to disable.
|
|
35
|
+
*/
|
|
36
|
+
originAgentCluster?: string | false;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* `Referrer-Policy`
|
|
40
|
+
* @defaultValue 'no-referrer'
|
|
41
|
+
* Set to `false` to disable.
|
|
42
|
+
*/
|
|
43
|
+
referrerPolicy?: string | false;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* `Strict-Transport-Security` (HSTS)
|
|
47
|
+
* @defaultValue 'max-age=15552000; includeSubDomains'
|
|
48
|
+
* Set to `false` to disable.
|
|
49
|
+
*/
|
|
50
|
+
strictTransportSecurity?: string | false;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* `X-Content-Type-Options`
|
|
54
|
+
* @defaultValue 'nosniff'
|
|
55
|
+
* Set to `false` to disable.
|
|
56
|
+
*/
|
|
57
|
+
xContentTypeOptions?: string | false;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* `X-DNS-Prefetch-Control`
|
|
61
|
+
* @defaultValue 'off'
|
|
62
|
+
* Set to `false` to disable.
|
|
63
|
+
*/
|
|
64
|
+
xDnsPrefetchControl?: string | false;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* `X-Download-Options`
|
|
68
|
+
* @defaultValue 'noopen'
|
|
69
|
+
* Set to `false` to disable.
|
|
70
|
+
*/
|
|
71
|
+
xDownloadOptions?: string | false;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* `X-Frame-Options`
|
|
75
|
+
* @defaultValue 'SAMEORIGIN'
|
|
76
|
+
* Set to `false` to disable.
|
|
77
|
+
*/
|
|
78
|
+
xFrameOptions?: string | false;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* `X-Permitted-Cross-Domain-Policies`
|
|
82
|
+
* @defaultValue 'none'
|
|
83
|
+
* Set to `false` to disable.
|
|
84
|
+
*/
|
|
85
|
+
xPermittedCrossDomainPolicies?: string | false;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* `X-XSS-Protection`
|
|
89
|
+
* @defaultValue '0' (disabled by recommendation, modern browsers use CSP instead)
|
|
90
|
+
* Set to `false` to omit the header entirely.
|
|
91
|
+
*/
|
|
92
|
+
xXssProtection?: string | false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const DEFAULTS: Required<SecurityHeadersOptions> = {
|
|
96
|
+
contentSecurityPolicy: "default-src 'self'",
|
|
97
|
+
crossOriginOpenerPolicy: 'same-origin',
|
|
98
|
+
crossOriginResourcePolicy: 'same-origin',
|
|
99
|
+
originAgentCluster: '?1',
|
|
100
|
+
referrerPolicy: 'no-referrer',
|
|
101
|
+
strictTransportSecurity: 'max-age=15552000; includeSubDomains',
|
|
102
|
+
xContentTypeOptions: 'nosniff',
|
|
103
|
+
xDnsPrefetchControl: 'off',
|
|
104
|
+
xDownloadOptions: 'noopen',
|
|
105
|
+
xFrameOptions: 'SAMEORIGIN',
|
|
106
|
+
xPermittedCrossDomainPolicies: 'none',
|
|
107
|
+
xXssProtection: '0',
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Maps camelCase option keys to the actual HTTP header names.
|
|
112
|
+
*/
|
|
113
|
+
const HEADER_MAP: Record<keyof SecurityHeadersOptions, string> = {
|
|
114
|
+
contentSecurityPolicy: 'Content-Security-Policy',
|
|
115
|
+
crossOriginOpenerPolicy: 'Cross-Origin-Opener-Policy',
|
|
116
|
+
crossOriginResourcePolicy: 'Cross-Origin-Resource-Policy',
|
|
117
|
+
originAgentCluster: 'Origin-Agent-Cluster',
|
|
118
|
+
referrerPolicy: 'Referrer-Policy',
|
|
119
|
+
strictTransportSecurity: 'Strict-Transport-Security',
|
|
120
|
+
xContentTypeOptions: 'X-Content-Type-Options',
|
|
121
|
+
xDnsPrefetchControl: 'X-DNS-Prefetch-Control',
|
|
122
|
+
xDownloadOptions: 'X-Download-Options',
|
|
123
|
+
xFrameOptions: 'X-Frame-Options',
|
|
124
|
+
xPermittedCrossDomainPolicies: 'X-Permitted-Cross-Domain-Policies',
|
|
125
|
+
xXssProtection: 'X-XSS-Protection',
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Built-in security headers middleware — analogous to [helmet](https://helmetjs.github.io/).
|
|
130
|
+
*
|
|
131
|
+
* Sets a sensible default set of security-related HTTP response headers on every
|
|
132
|
+
* response. Individual headers can be overridden or disabled via the options object.
|
|
133
|
+
*
|
|
134
|
+
* @example Default usage (all headers with recommended defaults)
|
|
135
|
+
* ```typescript
|
|
136
|
+
* const app = new OneBunApplication(AppModule, {
|
|
137
|
+
* middleware: [SecurityHeadersMiddleware],
|
|
138
|
+
* });
|
|
139
|
+
* ```
|
|
140
|
+
*
|
|
141
|
+
* @example Custom CSP + disable HSTS (e.g. in development)
|
|
142
|
+
* ```typescript
|
|
143
|
+
* const app = new OneBunApplication(AppModule, {
|
|
144
|
+
* middleware: [
|
|
145
|
+
* SecurityHeadersMiddleware.configure({
|
|
146
|
+
* contentSecurityPolicy: "default-src 'self'; img-src *",
|
|
147
|
+
* strictTransportSecurity: false,
|
|
148
|
+
* }),
|
|
149
|
+
* ],
|
|
150
|
+
* });
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
export class SecurityHeadersMiddleware extends BaseMiddleware {
|
|
154
|
+
private readonly resolvedHeaders: Array<[string, string]>;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Create a pre-configured SecurityHeadersMiddleware class with the given options.
|
|
158
|
+
* Returns a constructor — pass the result directly to `ApplicationOptions.middleware`.
|
|
159
|
+
*/
|
|
160
|
+
static configure(options: SecurityHeadersOptions): typeof SecurityHeadersMiddleware {
|
|
161
|
+
class ConfiguredSecurityHeadersMiddleware extends SecurityHeadersMiddleware {
|
|
162
|
+
constructor() {
|
|
163
|
+
super(options);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return ConfiguredSecurityHeadersMiddleware;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
constructor(options: SecurityHeadersOptions = {}) {
|
|
171
|
+
super();
|
|
172
|
+
|
|
173
|
+
const merged: SecurityHeadersOptions = { ...DEFAULTS, ...options };
|
|
174
|
+
this.resolvedHeaders = (Object.keys(HEADER_MAP) as Array<keyof SecurityHeadersOptions>)
|
|
175
|
+
.filter((key) => merged[key] !== false && merged[key] !== undefined)
|
|
176
|
+
.map((key) => [HEADER_MAP[key], merged[key] as string]);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async use(_req: OneBunRequest, next: () => Promise<OneBunResponse>): Promise<OneBunResponse> {
|
|
180
|
+
const response = await next();
|
|
181
|
+
|
|
182
|
+
for (const [header, value] of this.resolvedHeaders) {
|
|
183
|
+
response.headers.set(header, value);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return response;
|
|
187
|
+
}
|
|
188
|
+
}
|