create-phoenixjs 0.1.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 (49) hide show
  1. package/index.ts +196 -0
  2. package/package.json +31 -0
  3. package/template/README.md +62 -0
  4. package/template/app/controllers/ExampleController.ts +61 -0
  5. package/template/artisan +2 -0
  6. package/template/bootstrap/app.ts +44 -0
  7. package/template/bunfig.toml +7 -0
  8. package/template/config/database.ts +25 -0
  9. package/template/config/plugins.ts +7 -0
  10. package/template/config/security.ts +158 -0
  11. package/template/framework/cli/Command.ts +17 -0
  12. package/template/framework/cli/ConsoleApplication.ts +55 -0
  13. package/template/framework/cli/artisan.ts +16 -0
  14. package/template/framework/cli/commands/MakeControllerCommand.ts +41 -0
  15. package/template/framework/cli/commands/MakeMiddlewareCommand.ts +41 -0
  16. package/template/framework/cli/commands/MakeModelCommand.ts +36 -0
  17. package/template/framework/cli/commands/MakeValidatorCommand.ts +42 -0
  18. package/template/framework/controller/Controller.ts +222 -0
  19. package/template/framework/core/Application.ts +208 -0
  20. package/template/framework/core/Container.ts +100 -0
  21. package/template/framework/core/Kernel.ts +297 -0
  22. package/template/framework/database/DatabaseAdapter.ts +18 -0
  23. package/template/framework/database/PrismaAdapter.ts +65 -0
  24. package/template/framework/database/SqlAdapter.ts +117 -0
  25. package/template/framework/gateway/Gateway.ts +109 -0
  26. package/template/framework/gateway/GatewayManager.ts +150 -0
  27. package/template/framework/gateway/WebSocketAdapter.ts +159 -0
  28. package/template/framework/gateway/WebSocketGateway.ts +182 -0
  29. package/template/framework/http/Request.ts +608 -0
  30. package/template/framework/http/Response.ts +525 -0
  31. package/template/framework/http/Server.ts +161 -0
  32. package/template/framework/http/UploadedFile.ts +145 -0
  33. package/template/framework/middleware/Middleware.ts +50 -0
  34. package/template/framework/middleware/Pipeline.ts +89 -0
  35. package/template/framework/plugin/Plugin.ts +26 -0
  36. package/template/framework/plugin/PluginManager.ts +61 -0
  37. package/template/framework/routing/RouteRegistry.ts +185 -0
  38. package/template/framework/routing/Router.ts +280 -0
  39. package/template/framework/security/CorsMiddleware.ts +151 -0
  40. package/template/framework/security/CsrfMiddleware.ts +121 -0
  41. package/template/framework/security/HelmetMiddleware.ts +138 -0
  42. package/template/framework/security/InputSanitizerMiddleware.ts +134 -0
  43. package/template/framework/security/RateLimiterMiddleware.ts +189 -0
  44. package/template/framework/security/SecurityManager.ts +128 -0
  45. package/template/framework/validation/Validator.ts +482 -0
  46. package/template/package.json +24 -0
  47. package/template/routes/api.ts +56 -0
  48. package/template/server.ts +29 -0
  49. package/template/tsconfig.json +49 -0
@@ -0,0 +1,280 @@
1
+ /**
2
+ * PhoenixJS - Router
3
+ *
4
+ * Laravel-style router with fluent API, route groups, and middleware support.
5
+ */
6
+
7
+ import { RouteRegistry, type HttpMethod, type RouteHandler, type Route, type RouteMatch } from '@framework/routing/RouteRegistry';
8
+ import type { MiddlewareResolvable } from '@framework/middleware/Middleware';
9
+
10
+ /**
11
+ * Route group options
12
+ */
13
+ export interface RouteGroupOptions {
14
+ /** Prefix to add to all routes in the group */
15
+ prefix?: string;
16
+ /** Middleware to apply to all routes in the group */
17
+ middleware?: MiddlewareResolvable[];
18
+ /** Name prefix for named routes */
19
+ as?: string;
20
+ }
21
+
22
+ /**
23
+ * Context for the current route group
24
+ */
25
+ interface GroupContext {
26
+ prefix: string;
27
+ middleware: MiddlewareResolvable[];
28
+ namePrefix: string;
29
+ }
30
+
31
+ /**
32
+ * Router class
33
+ *
34
+ * Provides a fluent API for defining routes with Laravel-like syntax.
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * Router.get('/users', 'UserController@index');
39
+ * Router.post('/users', 'UserController@store').middleware('auth');
40
+ *
41
+ * Router.group({ prefix: '/api', middleware: ['auth'] }, () => {
42
+ * Router.get('/users/:id', 'UserController@show');
43
+ * });
44
+ * ```
45
+ */
46
+ export class Router {
47
+ private static instance: Router | null = null;
48
+ private registry: RouteRegistry;
49
+ private groupStack: GroupContext[] = [];
50
+ private lastRoute: Route | null = null;
51
+ private middlewareMap: Map<string, MiddlewareResolvable> = new Map();
52
+
53
+ private constructor() {
54
+ this.registry = new RouteRegistry();
55
+ }
56
+
57
+ /**
58
+ * Get the singleton router instance
59
+ */
60
+ static getInstance(): Router {
61
+ if (!Router.instance) {
62
+ Router.instance = new Router();
63
+ }
64
+ return Router.instance;
65
+ }
66
+
67
+ /**
68
+ * Reset the router (for testing)
69
+ */
70
+ static reset(): void {
71
+ if (Router.instance) {
72
+ Router.instance.registry.clear();
73
+ Router.instance.groupStack = [];
74
+ Router.instance.lastRoute = null;
75
+ Router.instance.middlewareMap.clear();
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Register a route alias for middleware
81
+ */
82
+ static aliasMiddleware(name: string, middleware: MiddlewareResolvable): void {
83
+ Router.getInstance().middlewareMap.set(name, middleware);
84
+ }
85
+
86
+ /**
87
+ * GET route
88
+ */
89
+ static get(path: string, handler: RouteHandler): typeof Router {
90
+ return Router.addRoute('GET', path, handler);
91
+ }
92
+
93
+ /**
94
+ * POST route
95
+ */
96
+ static post(path: string, handler: RouteHandler): typeof Router {
97
+ return Router.addRoute('POST', path, handler);
98
+ }
99
+
100
+ /**
101
+ * PUT route
102
+ */
103
+ static put(path: string, handler: RouteHandler): typeof Router {
104
+ return Router.addRoute('PUT', path, handler);
105
+ }
106
+
107
+ /**
108
+ * PATCH route
109
+ */
110
+ static patch(path: string, handler: RouteHandler): typeof Router {
111
+ return Router.addRoute('PATCH', path, handler);
112
+ }
113
+
114
+ /**
115
+ * DELETE route
116
+ */
117
+ static delete(path: string, handler: RouteHandler): typeof Router {
118
+ return Router.addRoute('DELETE', path, handler);
119
+ }
120
+
121
+ /**
122
+ * OPTIONS route
123
+ */
124
+ static options(path: string, handler: RouteHandler): typeof Router {
125
+ return Router.addRoute('OPTIONS', path, handler);
126
+ }
127
+
128
+ /**
129
+ * Match multiple HTTP methods
130
+ */
131
+ static match(methods: HttpMethod[], path: string, handler: RouteHandler): typeof Router {
132
+ const router = Router.getInstance();
133
+ for (const method of methods) {
134
+ Router.addRoute(method, path, handler);
135
+ }
136
+ return Router;
137
+ }
138
+
139
+ /**
140
+ * Match any HTTP method
141
+ */
142
+ static any(path: string, handler: RouteHandler): typeof Router {
143
+ return Router.match(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], path, handler);
144
+ }
145
+
146
+ /**
147
+ * Create a route group
148
+ */
149
+ static group(options: RouteGroupOptions, callback: () => void): void {
150
+ const router = Router.getInstance();
151
+
152
+ // Get current context
153
+ const currentPrefix = router.getCurrentPrefix();
154
+ const currentMiddleware = router.getCurrentMiddleware();
155
+ const currentNamePrefix = router.getCurrentNamePrefix();
156
+
157
+ // Push new context
158
+ router.groupStack.push({
159
+ prefix: currentPrefix + (options.prefix || ''),
160
+ middleware: [...currentMiddleware, ...(options.middleware || [])],
161
+ namePrefix: currentNamePrefix + (options.as || ''),
162
+ });
163
+
164
+ // Execute callback
165
+ callback();
166
+
167
+ // Pop context
168
+ router.groupStack.pop();
169
+ }
170
+
171
+ /**
172
+ * Add middleware to the last registered route
173
+ * For fluent API: Router.get('/').middleware('auth')
174
+ */
175
+ static middleware(...middleware: MiddlewareResolvable[]): typeof Router {
176
+ const router = Router.getInstance();
177
+ if (router.lastRoute) {
178
+ router.lastRoute.middleware.push(...middleware);
179
+ }
180
+ return Router;
181
+ }
182
+
183
+ /**
184
+ * Name the last registered route
185
+ * For fluent API: Router.get('/').name('home')
186
+ */
187
+ static name(name: string): typeof Router {
188
+ const router = Router.getInstance();
189
+ if (router.lastRoute) {
190
+ const prefix = router.getCurrentNamePrefix();
191
+ router.lastRoute.name = prefix + name;
192
+ }
193
+ return Router;
194
+ }
195
+
196
+ /**
197
+ * Resolve a route from method and path
198
+ */
199
+ static resolve(method: string, path: string): RouteMatch | null {
200
+ return Router.getInstance().registry.match(method, path);
201
+ }
202
+
203
+ /**
204
+ * Get all registered routes
205
+ */
206
+ static routes(): Route[] {
207
+ return Router.getInstance().registry.all();
208
+ }
209
+
210
+ /**
211
+ * Find a route by name
212
+ */
213
+ static route(name: string): Route | undefined {
214
+ return Router.getInstance().registry.findByName(name);
215
+ }
216
+
217
+ /**
218
+ * Get route count
219
+ */
220
+ static count(): number {
221
+ return Router.getInstance().registry.count();
222
+ }
223
+
224
+ /**
225
+ * Resolve middleware alias to actual middleware
226
+ */
227
+ static resolveMiddleware(name: string): MiddlewareResolvable | undefined {
228
+ return Router.getInstance().middlewareMap.get(name);
229
+ }
230
+
231
+ /**
232
+ * Get registered middleware map
233
+ */
234
+ static getMiddlewareMap(): Map<string, MiddlewareResolvable> {
235
+ return Router.getInstance().middlewareMap;
236
+ }
237
+
238
+ /**
239
+ * Add a route to the registry
240
+ */
241
+ private static addRoute(method: HttpMethod, path: string, handler: RouteHandler): typeof Router {
242
+ const router = Router.getInstance();
243
+
244
+ // Apply group prefix
245
+ const fullPath = router.getCurrentPrefix() + path;
246
+
247
+ // Apply group middleware
248
+ const middleware = [...router.getCurrentMiddleware()];
249
+
250
+ // Register the route
251
+ router.lastRoute = router.registry.add(method, fullPath, handler, middleware);
252
+
253
+
254
+ return Router;
255
+ }
256
+
257
+ /**
258
+ * Get current prefix from group stack
259
+ */
260
+ private getCurrentPrefix(): string {
261
+ if (this.groupStack.length === 0) return '';
262
+ return this.groupStack[this.groupStack.length - 1].prefix;
263
+ }
264
+
265
+ /**
266
+ * Get current middleware from group stack
267
+ */
268
+ private getCurrentMiddleware(): MiddlewareResolvable[] {
269
+ if (this.groupStack.length === 0) return [];
270
+ return [...this.groupStack[this.groupStack.length - 1].middleware];
271
+ }
272
+
273
+ /**
274
+ * Get current name prefix from group stack
275
+ */
276
+ private getCurrentNamePrefix(): string {
277
+ if (this.groupStack.length === 0) return '';
278
+ return this.groupStack[this.groupStack.length - 1].namePrefix;
279
+ }
280
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * PhoenixJS - CORS Middleware
3
+ *
4
+ * Handles Cross-Origin Resource Sharing (CORS) headers and preflight requests.
5
+ */
6
+
7
+ import type { Middleware, NextFunction } from '@framework/middleware/Middleware';
8
+ import type { FrameworkRequest } from '@framework/http/Request';
9
+ import type { CorsConfig } from '@/config/security';
10
+
11
+ export class CorsMiddleware implements Middleware {
12
+ constructor(private config: CorsConfig) { }
13
+
14
+ async handle(request: FrameworkRequest, next: NextFunction): Promise<Response> {
15
+ const origin = request.header('Origin') || '';
16
+
17
+ // Handle preflight requests
18
+ if (request.method() === 'OPTIONS') {
19
+ return this.handlePreflight(request, origin);
20
+ }
21
+
22
+ // Process the request through the pipeline
23
+ const response = await next(request);
24
+
25
+ // Add CORS headers to the response
26
+ return this.addCorsHeaders(response, origin);
27
+ }
28
+
29
+ /**
30
+ * Handle preflight (OPTIONS) requests
31
+ */
32
+ private handlePreflight(request: FrameworkRequest, origin: string): Response {
33
+ const headers: Record<string, string> = {};
34
+
35
+ // Check if origin is allowed
36
+ if (this.isOriginAllowed(origin)) {
37
+ headers['Access-Control-Allow-Origin'] = this.getAllowedOrigin(origin);
38
+
39
+ if (this.config.credentials) {
40
+ headers['Access-Control-Allow-Credentials'] = 'true';
41
+ }
42
+ }
43
+
44
+ // Set allowed methods
45
+ headers['Access-Control-Allow-Methods'] = this.config.methods.join(', ');
46
+
47
+ // Set allowed headers
48
+ const requestedHeaders = request.header('Access-Control-Request-Headers');
49
+ if (requestedHeaders) {
50
+ headers['Access-Control-Allow-Headers'] = this.config.allowedHeaders.join(', ');
51
+ }
52
+
53
+ // Set max age
54
+ headers['Access-Control-Max-Age'] = this.config.maxAge.toString();
55
+
56
+ return new Response(null, {
57
+ status: 204,
58
+ headers,
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Add CORS headers to a response
64
+ */
65
+ private addCorsHeaders(response: Response, origin: string): Response {
66
+ const headers = new Headers(response.headers);
67
+
68
+ // Check if origin is allowed
69
+ if (this.isOriginAllowed(origin)) {
70
+ headers.set('Access-Control-Allow-Origin', this.getAllowedOrigin(origin));
71
+
72
+ if (this.config.credentials) {
73
+ headers.set('Access-Control-Allow-Credentials', 'true');
74
+ }
75
+ }
76
+
77
+ // Set exposed headers
78
+ if (this.config.exposedHeaders.length > 0) {
79
+ headers.set('Access-Control-Expose-Headers', this.config.exposedHeaders.join(', '));
80
+ }
81
+
82
+ return new Response(response.body, {
83
+ status: response.status,
84
+ statusText: response.statusText,
85
+ headers,
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Check if an origin is allowed
91
+ */
92
+ private isOriginAllowed(origin: string): boolean {
93
+ const configOrigin = this.config.origin;
94
+
95
+ // Wildcard allows all
96
+ if (configOrigin === '*') {
97
+ return true;
98
+ }
99
+
100
+ // String match
101
+ if (typeof configOrigin === 'string') {
102
+ return origin === configOrigin;
103
+ }
104
+
105
+ // Array of origins
106
+ if (Array.isArray(configOrigin)) {
107
+ return configOrigin.includes(origin);
108
+ }
109
+
110
+ // Regex match
111
+ if (configOrigin instanceof RegExp) {
112
+ return configOrigin.test(origin);
113
+ }
114
+
115
+ // Function check
116
+ if (typeof configOrigin === 'function') {
117
+ return configOrigin(origin);
118
+ }
119
+
120
+ return false;
121
+ }
122
+
123
+ /**
124
+ * Get the allowed origin value for the header
125
+ */
126
+ private getAllowedOrigin(origin: string): string {
127
+ // If wildcard and no credentials, return *
128
+ if (this.config.origin === '*' && !this.config.credentials) {
129
+ return '*';
130
+ }
131
+
132
+ // Otherwise return the requesting origin (if allowed)
133
+ return origin;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Create a CORS middleware instance with the given config
139
+ */
140
+ export function cors(config: Partial<CorsConfig> = {}): CorsMiddleware {
141
+ const defaultConfig: CorsConfig = {
142
+ origin: '*',
143
+ methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
144
+ allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
145
+ exposedHeaders: [],
146
+ credentials: false,
147
+ maxAge: 86400,
148
+ };
149
+
150
+ return new CorsMiddleware({ ...defaultConfig, ...config });
151
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * PhoenixJS - CSRF Middleware
3
+ *
4
+ * Provides Cross-Site Request Forgery protection.
5
+ */
6
+
7
+ import type { Middleware, NextFunction } from '@framework/middleware/Middleware';
8
+ import type { FrameworkRequest } from '@framework/http/Request';
9
+ import type { CsrfConfig } from '@/config/security';
10
+ import { FrameworkResponse } from '@framework/http/Response';
11
+
12
+ export class CsrfMiddleware implements Middleware {
13
+ private tokens: Map<string, { token: string; expires: number }> = new Map();
14
+
15
+ constructor(private config: CsrfConfig) { }
16
+
17
+ async handle(request: FrameworkRequest, next: NextFunction): Promise<Response> {
18
+ // Skip if CSRF is disabled
19
+ if (!this.config.enabled) {
20
+ return next(request);
21
+ }
22
+
23
+ // Skip safe methods (GET, HEAD, OPTIONS)
24
+ if (this.config.safeMethods.includes(request.method())) {
25
+ const response = await next(request);
26
+ return this.attachCsrfCookie(response, request);
27
+ }
28
+
29
+ // Validate CSRF token for non-safe methods
30
+ if (!this.validateToken(request)) {
31
+ return FrameworkResponse.json(
32
+ {
33
+ error: true,
34
+ message: 'CSRF token mismatch',
35
+ },
36
+ 403
37
+ );
38
+ }
39
+
40
+ return next(request);
41
+ }
42
+
43
+ /**
44
+ * Validate the CSRF token from the request
45
+ */
46
+ private validateToken(request: FrameworkRequest): boolean {
47
+ // Get token from header
48
+ const headerToken = request.header(this.config.headerName);
49
+
50
+ // Get token from cookie
51
+ const cookieToken = request.cookie(this.config.cookieName);
52
+
53
+ // Both must exist and match
54
+ if (!headerToken || !cookieToken) {
55
+ return false;
56
+ }
57
+
58
+ // Tokens must match
59
+ return headerToken === cookieToken;
60
+ }
61
+
62
+ /**
63
+ * Attach CSRF cookie to response
64
+ */
65
+ private attachCsrfCookie(response: Response, request: FrameworkRequest): Response {
66
+ // Check if token already exists in cookie
67
+ const existingToken = request.cookie(this.config.cookieName);
68
+
69
+ // Generate or use existing token
70
+ const token = existingToken || this.generateToken();
71
+
72
+ // Clone response with CSRF cookie
73
+ const headers = new Headers(response.headers);
74
+ const cookieValue = `${this.config.cookieName}=${token}; Path=/; SameSite=Strict`;
75
+ headers.append('Set-Cookie', cookieValue);
76
+
77
+ return new Response(response.body, {
78
+ status: response.status,
79
+ statusText: response.statusText,
80
+ headers,
81
+ });
82
+ }
83
+
84
+ /**
85
+ * Generate a cryptographically secure random token
86
+ */
87
+ generateToken(): string {
88
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
89
+ const array = new Uint8Array(this.config.tokenLength);
90
+ crypto.getRandomValues(array);
91
+
92
+ let token = '';
93
+ for (let i = 0; i < this.config.tokenLength; i++) {
94
+ token += chars[array[i] % chars.length];
95
+ }
96
+
97
+ return token;
98
+ }
99
+
100
+ /**
101
+ * Create a token for testing
102
+ */
103
+ createTestToken(): string {
104
+ return this.generateToken();
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Create a CSRF middleware instance with the given config
110
+ */
111
+ export function csrf(config: Partial<CsrfConfig> = {}): CsrfMiddleware {
112
+ const defaultConfig: CsrfConfig = {
113
+ enabled: true,
114
+ cookieName: 'XSRF-TOKEN',
115
+ headerName: 'X-CSRF-Token',
116
+ tokenLength: 32,
117
+ safeMethods: ['GET', 'HEAD', 'OPTIONS'],
118
+ };
119
+
120
+ return new CsrfMiddleware({ ...defaultConfig, ...config });
121
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * PhoenixJS - Helmet Middleware
3
+ *
4
+ * Sets security-related HTTP headers (inspired by Helmet.js).
5
+ */
6
+
7
+ import type { Middleware, NextFunction } from '@framework/middleware/Middleware';
8
+ import type { FrameworkRequest } from '@framework/http/Request';
9
+ import type { HelmetConfig } from '@/config/security';
10
+
11
+ export class HelmetMiddleware implements Middleware {
12
+ constructor(private config: HelmetConfig) { }
13
+
14
+ async handle(request: FrameworkRequest, next: NextFunction): Promise<Response> {
15
+ // Process the request through the pipeline
16
+ const response = await next(request);
17
+
18
+ // Add security headers to the response
19
+ return this.addSecurityHeaders(response);
20
+ }
21
+
22
+ /**
23
+ * Add security headers to a response
24
+ */
25
+ private addSecurityHeaders(response: Response): Response {
26
+ const headers = new Headers(response.headers);
27
+
28
+ // X-Frame-Options
29
+ if (this.config.frameOptions) {
30
+ headers.set('X-Frame-Options', this.config.frameOptions);
31
+ }
32
+
33
+ // X-Content-Type-Options
34
+ if (this.config.contentTypeOptions) {
35
+ headers.set('X-Content-Type-Options', 'nosniff');
36
+ }
37
+
38
+ // X-XSS-Protection
39
+ if (this.config.xssProtection) {
40
+ headers.set('X-XSS-Protection', '1; mode=block');
41
+ }
42
+
43
+ // Strict-Transport-Security (HSTS)
44
+ if (this.config.hsts.enabled) {
45
+ let hstsValue = `max-age=${this.config.hsts.maxAge}`;
46
+ if (this.config.hsts.includeSubDomains) {
47
+ hstsValue += '; includeSubDomains';
48
+ }
49
+ if (this.config.hsts.preload) {
50
+ hstsValue += '; preload';
51
+ }
52
+ headers.set('Strict-Transport-Security', hstsValue);
53
+ }
54
+
55
+ // Content-Security-Policy
56
+ if (this.config.contentSecurityPolicy.enabled) {
57
+ const csp = this.buildCsp(this.config.contentSecurityPolicy.directives);
58
+ headers.set('Content-Security-Policy', csp);
59
+ }
60
+
61
+ // Referrer-Policy
62
+ if (this.config.referrerPolicy) {
63
+ headers.set('Referrer-Policy', this.config.referrerPolicy);
64
+ }
65
+
66
+ // X-Permitted-Cross-Domain-Policies
67
+ if (this.config.crossDomainPolicy) {
68
+ headers.set('X-Permitted-Cross-Domain-Policies', this.config.crossDomainPolicy);
69
+ }
70
+
71
+ // X-Download-Options (IE)
72
+ if (this.config.downloadOptions) {
73
+ headers.set('X-Download-Options', 'noopen');
74
+ }
75
+
76
+ return new Response(response.body, {
77
+ status: response.status,
78
+ statusText: response.statusText,
79
+ headers,
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Build Content-Security-Policy header value
85
+ */
86
+ private buildCsp(directives: Record<string, string | string[]>): string {
87
+ const parts: string[] = [];
88
+
89
+ for (const [directive, value] of Object.entries(directives)) {
90
+ const directiveName = this.camelToKebab(directive);
91
+ const directiveValue = Array.isArray(value) ? value.join(' ') : value;
92
+ parts.push(`${directiveName} ${directiveValue}`);
93
+ }
94
+
95
+ return parts.join('; ');
96
+ }
97
+
98
+ /**
99
+ * Convert camelCase to kebab-case
100
+ */
101
+ private camelToKebab(str: string): string {
102
+ return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Create a Helmet middleware instance with the given config
108
+ */
109
+ export function helmet(config: Partial<HelmetConfig> = {}): HelmetMiddleware {
110
+ const defaultConfig: HelmetConfig = {
111
+ frameOptions: 'DENY',
112
+ contentTypeOptions: true,
113
+ xssProtection: true,
114
+ hsts: {
115
+ enabled: true,
116
+ maxAge: 31536000,
117
+ includeSubDomains: true,
118
+ preload: false,
119
+ },
120
+ contentSecurityPolicy: {
121
+ enabled: false,
122
+ directives: {},
123
+ },
124
+ referrerPolicy: 'no-referrer',
125
+ crossDomainPolicy: 'none',
126
+ downloadOptions: true,
127
+ };
128
+
129
+ return new HelmetMiddleware({
130
+ ...defaultConfig,
131
+ ...config,
132
+ hsts: { ...defaultConfig.hsts, ...(config.hsts || {}) },
133
+ contentSecurityPolicy: {
134
+ ...defaultConfig.contentSecurityPolicy,
135
+ ...(config.contentSecurityPolicy || {}),
136
+ },
137
+ });
138
+ }