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,134 @@
1
+ /**
2
+ * PhoenixJS - Input Sanitizer Middleware
3
+ *
4
+ * Sanitizes request input to prevent XSS and other injection attacks.
5
+ */
6
+
7
+ import type { Middleware, NextFunction } from '@framework/middleware/Middleware';
8
+ import type { FrameworkRequest } from '@framework/http/Request';
9
+ import type { SanitizerConfig } from '@/config/security';
10
+
11
+ export class InputSanitizerMiddleware implements Middleware {
12
+ constructor(private config: SanitizerConfig) { }
13
+
14
+ async handle(request: FrameworkRequest, next: NextFunction): Promise<Response> {
15
+ // Skip if sanitization is disabled
16
+ if (!this.config.enabled) {
17
+ return next(request);
18
+ }
19
+
20
+ // Sanitize the request input
21
+ // Note: We sanitize the parsed body when it's accessed
22
+ // The sanitization happens at the framework level
23
+
24
+ return next(request);
25
+ }
26
+
27
+ /**
28
+ * Sanitize a value (string, object, or array)
29
+ */
30
+ sanitize(value: unknown, fieldPath: string = ''): unknown {
31
+ // Skip if field is in skip list
32
+ if (this.shouldSkipField(fieldPath)) {
33
+ return value;
34
+ }
35
+
36
+ if (typeof value === 'string') {
37
+ return this.sanitizeString(value);
38
+ }
39
+
40
+ if (Array.isArray(value)) {
41
+ return value.map((item, index) => this.sanitize(item, `${fieldPath}[${index}]`));
42
+ }
43
+
44
+ if (value !== null && typeof value === 'object') {
45
+ const sanitized: Record<string, unknown> = {};
46
+ for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
47
+ const newPath = fieldPath ? `${fieldPath}.${key}` : key;
48
+ sanitized[key] = this.sanitize(val, newPath);
49
+ }
50
+ return sanitized;
51
+ }
52
+
53
+ return value;
54
+ }
55
+
56
+ /**
57
+ * Sanitize a string value
58
+ */
59
+ private sanitizeString(value: string): string {
60
+ let result = value;
61
+
62
+ // Trim whitespace
63
+ if (this.config.trim) {
64
+ result = result.trim();
65
+ }
66
+
67
+ // Strip HTML tags
68
+ if (this.config.stripTags) {
69
+ result = this.stripHtmlTags(result);
70
+ }
71
+
72
+ // Escape HTML entities
73
+ if (this.config.escapeHtml) {
74
+ result = this.escapeHtmlEntities(result);
75
+ }
76
+
77
+ return result;
78
+ }
79
+
80
+ /**
81
+ * Strip HTML tags from a string
82
+ */
83
+ private stripHtmlTags(value: string): string {
84
+ // Remove script and style tags completely (with content)
85
+ let result = value.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
86
+ result = result.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '');
87
+
88
+ // Remove all other HTML tags (but keep content)
89
+ result = result.replace(/<[^>]*>/g, '');
90
+
91
+ return result;
92
+ }
93
+
94
+ /**
95
+ * Escape HTML entities
96
+ */
97
+ private escapeHtmlEntities(value: string): string {
98
+ const entities: Record<string, string> = {
99
+ '&': '&amp;',
100
+ '<': '&lt;',
101
+ '>': '&gt;',
102
+ '"': '&quot;',
103
+ "'": '&#x27;',
104
+ '/': '&#x2F;',
105
+ '`': '&#x60;',
106
+ '=': '&#x3D;',
107
+ };
108
+
109
+ return value.replace(/[&<>"'`=/]/g, (char) => entities[char] || char);
110
+ }
111
+
112
+ /**
113
+ * Check if a field should be skipped
114
+ */
115
+ private shouldSkipField(fieldPath: string): boolean {
116
+ const fieldName = fieldPath.split('.').pop() || fieldPath;
117
+ return this.config.skipFields.includes(fieldName);
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Create an Input Sanitizer middleware instance with the given config
123
+ */
124
+ export function sanitizer(config: Partial<SanitizerConfig> = {}): InputSanitizerMiddleware {
125
+ const defaultConfig: SanitizerConfig = {
126
+ enabled: true,
127
+ stripTags: true,
128
+ escapeHtml: true,
129
+ trim: true,
130
+ skipFields: ['password', 'html', 'content'],
131
+ };
132
+
133
+ return new InputSanitizerMiddleware({ ...defaultConfig, ...config });
134
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * PhoenixJS - Rate Limiter Middleware
3
+ *
4
+ * Provides rate limiting to protect against abuse.
5
+ */
6
+
7
+ import type { Middleware, NextFunction } from '@framework/middleware/Middleware';
8
+ import type { FrameworkRequest } from '@framework/http/Request';
9
+ import type { RateLimitConfig } from '@/config/security';
10
+ import { FrameworkResponse } from '@framework/http/Response';
11
+
12
+ interface RateLimitEntry {
13
+ count: number;
14
+ resetTime: number;
15
+ }
16
+
17
+ export class RateLimiterMiddleware implements Middleware {
18
+ private store: Map<string, RateLimitEntry> = new Map();
19
+ private cleanupInterval: ReturnType<typeof setInterval> | null = null;
20
+
21
+ constructor(private config: RateLimitConfig) {
22
+ // Start cleanup interval to prevent memory leaks
23
+ this.startCleanup();
24
+ }
25
+
26
+ async handle(request: FrameworkRequest, next: NextFunction): Promise<Response> {
27
+ // Check if rate limiting is enabled
28
+ if (!this.config.enabled) {
29
+ return next(request);
30
+ }
31
+
32
+ // Check if this request should be skipped
33
+ if (this.config.skip && this.config.skip(request.getOriginal())) {
34
+ return next(request);
35
+ }
36
+
37
+ // Get the rate limit key (default: IP address)
38
+ const key = this.getKey(request);
39
+
40
+ // Get or create rate limit entry
41
+ const entry = this.getEntry(key);
42
+
43
+ // Check if rate limit exceeded
44
+ if (entry.count >= this.config.max) {
45
+ const retryAfter = Math.ceil((entry.resetTime - Date.now()) / 1000);
46
+ return this.createRateLimitResponse(entry, retryAfter);
47
+ }
48
+
49
+ // Increment count
50
+ entry.count++;
51
+ this.store.set(key, entry);
52
+
53
+ // Process the request
54
+ const response = await next(request);
55
+
56
+ // Add rate limit headers
57
+ return this.addRateLimitHeaders(response, entry);
58
+ }
59
+
60
+ /**
61
+ * Get the rate limit key for a request
62
+ */
63
+ private getKey(request: FrameworkRequest): string {
64
+ if (this.config.keyGenerator) {
65
+ return this.config.keyGenerator(request.getOriginal());
66
+ }
67
+ // Default: use IP address (with fallback for unknown)
68
+ return request.ip() || 'unknown';
69
+ }
70
+
71
+ /**
72
+ * Get or create a rate limit entry
73
+ */
74
+ private getEntry(key: string): RateLimitEntry {
75
+ const now = Date.now();
76
+ let entry = this.store.get(key);
77
+
78
+ // If no entry or window expired, create new one
79
+ if (!entry || now >= entry.resetTime) {
80
+ entry = {
81
+ count: 0,
82
+ resetTime: now + this.config.windowMs,
83
+ };
84
+ this.store.set(key, entry);
85
+ }
86
+
87
+ return entry;
88
+ }
89
+
90
+ /**
91
+ * Create a rate limit exceeded response
92
+ */
93
+ private createRateLimitResponse(entry: RateLimitEntry, retryAfter: number): Response {
94
+ const headers: Record<string, string> = {
95
+ 'Content-Type': 'application/json',
96
+ 'Retry-After': retryAfter.toString(),
97
+ 'X-RateLimit-Limit': this.config.max.toString(),
98
+ 'X-RateLimit-Remaining': '0',
99
+ 'X-RateLimit-Reset': Math.ceil(entry.resetTime / 1000).toString(),
100
+ };
101
+
102
+ return new Response(
103
+ JSON.stringify({
104
+ error: true,
105
+ message: this.config.message,
106
+ }),
107
+ {
108
+ status: 429,
109
+ headers,
110
+ }
111
+ );
112
+ }
113
+
114
+ /**
115
+ * Add rate limit headers to a response
116
+ */
117
+ private addRateLimitHeaders(response: Response, entry: RateLimitEntry): Response {
118
+ const headers = new Headers(response.headers);
119
+ const remaining = Math.max(0, this.config.max - entry.count);
120
+
121
+ headers.set('X-RateLimit-Limit', this.config.max.toString());
122
+ headers.set('X-RateLimit-Remaining', remaining.toString());
123
+ headers.set('X-RateLimit-Reset', Math.ceil(entry.resetTime / 1000).toString());
124
+
125
+ return new Response(response.body, {
126
+ status: response.status,
127
+ statusText: response.statusText,
128
+ headers,
129
+ });
130
+ }
131
+
132
+ /**
133
+ * Start cleanup interval to remove expired entries
134
+ */
135
+ private startCleanup(): void {
136
+ // Clean up every minute
137
+ this.cleanupInterval = setInterval(() => {
138
+ const now = Date.now();
139
+ for (const [key, entry] of this.store.entries()) {
140
+ if (now >= entry.resetTime) {
141
+ this.store.delete(key);
142
+ }
143
+ }
144
+ }, 60000);
145
+
146
+ // Allow the process to exit even if the interval is running
147
+ if (this.cleanupInterval.unref) {
148
+ this.cleanupInterval.unref();
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Stop the cleanup interval (for testing)
154
+ */
155
+ stopCleanup(): void {
156
+ if (this.cleanupInterval) {
157
+ clearInterval(this.cleanupInterval);
158
+ this.cleanupInterval = null;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Clear all rate limit entries (for testing)
164
+ */
165
+ clear(): void {
166
+ this.store.clear();
167
+ }
168
+
169
+ /**
170
+ * Get current stats (for testing/debugging)
171
+ */
172
+ getStats(): { totalKeys: number } {
173
+ return { totalKeys: this.store.size };
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Create a Rate Limiter middleware instance with the given config
179
+ */
180
+ export function rateLimit(config: Partial<RateLimitConfig> = {}): RateLimiterMiddleware {
181
+ const defaultConfig: RateLimitConfig = {
182
+ enabled: true,
183
+ windowMs: 60000, // 1 minute
184
+ max: 100,
185
+ message: 'Too many requests, please try again later.',
186
+ };
187
+
188
+ return new RateLimiterMiddleware({ ...defaultConfig, ...config });
189
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * PhoenixJS - Security Manager
3
+ *
4
+ * Central manager for all security features.
5
+ * Provides factory methods for security middleware.
6
+ */
7
+
8
+ import type { Application } from '@framework/core/Application';
9
+ import type { SecurityConfig } from '@/config/security';
10
+ import { CorsMiddleware, cors } from '@framework/security/CorsMiddleware';
11
+ import { HelmetMiddleware, helmet } from '@framework/security/HelmetMiddleware';
12
+ import { RateLimiterMiddleware, rateLimit } from '@framework/security/RateLimiterMiddleware';
13
+ import { InputSanitizerMiddleware, sanitizer } from '@framework/security/InputSanitizerMiddleware';
14
+ import { CsrfMiddleware, csrf } from '@framework/security/CsrfMiddleware';
15
+ import type { MiddlewareHandler } from '@framework/middleware/Middleware';
16
+
17
+ export class SecurityManager {
18
+ private corsMiddleware: CorsMiddleware | null = null;
19
+ private helmetMiddleware: HelmetMiddleware | null = null;
20
+ private rateLimiterMiddleware: RateLimiterMiddleware | null = null;
21
+ private sanitizerMiddleware: InputSanitizerMiddleware | null = null;
22
+ private csrfMiddleware: CsrfMiddleware | null = null;
23
+
24
+ constructor(
25
+ private app: Application,
26
+ private config?: SecurityConfig
27
+ ) { }
28
+
29
+ /**
30
+ * Get CORS middleware
31
+ */
32
+ getCorsMiddleware(): CorsMiddleware {
33
+ if (!this.corsMiddleware) {
34
+ this.corsMiddleware = cors(this.config?.cors);
35
+ }
36
+ return this.corsMiddleware;
37
+ }
38
+
39
+ /**
40
+ * Get Helmet middleware
41
+ */
42
+ getHelmetMiddleware(): HelmetMiddleware {
43
+ if (!this.helmetMiddleware) {
44
+ this.helmetMiddleware = helmet(this.config?.helmet);
45
+ }
46
+ return this.helmetMiddleware;
47
+ }
48
+
49
+ /**
50
+ * Get Rate Limiter middleware
51
+ */
52
+ getRateLimiterMiddleware(): RateLimiterMiddleware {
53
+ if (!this.rateLimiterMiddleware) {
54
+ this.rateLimiterMiddleware = rateLimit(this.config?.rateLimit);
55
+ }
56
+ return this.rateLimiterMiddleware;
57
+ }
58
+
59
+ /**
60
+ * Get Input Sanitizer middleware
61
+ */
62
+ getSanitizerMiddleware(): InputSanitizerMiddleware {
63
+ if (!this.sanitizerMiddleware) {
64
+ this.sanitizerMiddleware = sanitizer(this.config?.sanitizer);
65
+ }
66
+ return this.sanitizerMiddleware;
67
+ }
68
+
69
+ /**
70
+ * Get CSRF middleware
71
+ */
72
+ getCsrfMiddleware(): CsrfMiddleware {
73
+ if (!this.csrfMiddleware) {
74
+ this.csrfMiddleware = csrf(this.config?.csrf);
75
+ }
76
+ return this.csrfMiddleware;
77
+ }
78
+
79
+ /**
80
+ * Get all enabled security middleware as an array
81
+ */
82
+ getSecurityMiddleware(): MiddlewareHandler[] {
83
+ const middleware: MiddlewareHandler[] = [];
84
+
85
+ // Always add CORS (handles OPTIONS requests)
86
+ middleware.push(this.getCorsMiddleware());
87
+
88
+ // Add Helmet for security headers
89
+ middleware.push(this.getHelmetMiddleware());
90
+
91
+ // Add rate limiting if enabled
92
+ if (this.config?.rateLimit?.enabled !== false) {
93
+ middleware.push(this.getRateLimiterMiddleware());
94
+ }
95
+
96
+ // Add CSRF if enabled
97
+ if (this.config?.csrf?.enabled) {
98
+ middleware.push(this.getCsrfMiddleware());
99
+ }
100
+
101
+ return middleware;
102
+ }
103
+
104
+ /**
105
+ * Configure security from a config object
106
+ */
107
+ configure(config: Partial<SecurityConfig>): this {
108
+ this.config = { ...this.config, ...config } as SecurityConfig;
109
+
110
+ // Reset cached middleware so they get recreated with new config
111
+ this.corsMiddleware = null;
112
+ this.helmetMiddleware = null;
113
+ this.rateLimiterMiddleware = null;
114
+ this.sanitizerMiddleware = null;
115
+ this.csrfMiddleware = null;
116
+
117
+ return this;
118
+ }
119
+
120
+ /**
121
+ * Stop any background tasks (for cleanup)
122
+ */
123
+ shutdown(): void {
124
+ if (this.rateLimiterMiddleware) {
125
+ this.rateLimiterMiddleware.stopCleanup();
126
+ }
127
+ }
128
+ }