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.
- package/index.ts +196 -0
- package/package.json +31 -0
- package/template/README.md +62 -0
- package/template/app/controllers/ExampleController.ts +61 -0
- package/template/artisan +2 -0
- package/template/bootstrap/app.ts +44 -0
- package/template/bunfig.toml +7 -0
- package/template/config/database.ts +25 -0
- package/template/config/plugins.ts +7 -0
- package/template/config/security.ts +158 -0
- package/template/framework/cli/Command.ts +17 -0
- package/template/framework/cli/ConsoleApplication.ts +55 -0
- package/template/framework/cli/artisan.ts +16 -0
- package/template/framework/cli/commands/MakeControllerCommand.ts +41 -0
- package/template/framework/cli/commands/MakeMiddlewareCommand.ts +41 -0
- package/template/framework/cli/commands/MakeModelCommand.ts +36 -0
- package/template/framework/cli/commands/MakeValidatorCommand.ts +42 -0
- package/template/framework/controller/Controller.ts +222 -0
- package/template/framework/core/Application.ts +208 -0
- package/template/framework/core/Container.ts +100 -0
- package/template/framework/core/Kernel.ts +297 -0
- package/template/framework/database/DatabaseAdapter.ts +18 -0
- package/template/framework/database/PrismaAdapter.ts +65 -0
- package/template/framework/database/SqlAdapter.ts +117 -0
- package/template/framework/gateway/Gateway.ts +109 -0
- package/template/framework/gateway/GatewayManager.ts +150 -0
- package/template/framework/gateway/WebSocketAdapter.ts +159 -0
- package/template/framework/gateway/WebSocketGateway.ts +182 -0
- package/template/framework/http/Request.ts +608 -0
- package/template/framework/http/Response.ts +525 -0
- package/template/framework/http/Server.ts +161 -0
- package/template/framework/http/UploadedFile.ts +145 -0
- package/template/framework/middleware/Middleware.ts +50 -0
- package/template/framework/middleware/Pipeline.ts +89 -0
- package/template/framework/plugin/Plugin.ts +26 -0
- package/template/framework/plugin/PluginManager.ts +61 -0
- package/template/framework/routing/RouteRegistry.ts +185 -0
- package/template/framework/routing/Router.ts +280 -0
- package/template/framework/security/CorsMiddleware.ts +151 -0
- package/template/framework/security/CsrfMiddleware.ts +121 -0
- package/template/framework/security/HelmetMiddleware.ts +138 -0
- package/template/framework/security/InputSanitizerMiddleware.ts +134 -0
- package/template/framework/security/RateLimiterMiddleware.ts +189 -0
- package/template/framework/security/SecurityManager.ts +128 -0
- package/template/framework/validation/Validator.ts +482 -0
- package/template/package.json +24 -0
- package/template/routes/api.ts +56 -0
- package/template/server.ts +29 -0
- package/template/tsconfig.json +49 -0
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PhoenixJS - Request Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Wraps the native Bun Request with helpful methods.
|
|
5
|
+
* Inspired by Laravel's Request class.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { UploadedFile } from '@framework/http/UploadedFile';
|
|
9
|
+
|
|
10
|
+
export class FrameworkRequest {
|
|
11
|
+
private request: Request;
|
|
12
|
+
private parsedUrl: URL;
|
|
13
|
+
private queryParams: URLSearchParams;
|
|
14
|
+
private pathParams: Record<string, string> = {};
|
|
15
|
+
private parsedBody: unknown = null;
|
|
16
|
+
private bodyParsed = false;
|
|
17
|
+
private bodyText: string | null = null;
|
|
18
|
+
private bodyTextParsed = false;
|
|
19
|
+
private parsedCookies: Record<string, string> | null = null;
|
|
20
|
+
private parsedFiles: Map<string, UploadedFile[]> | null = null;
|
|
21
|
+
|
|
22
|
+
constructor(request: Request) {
|
|
23
|
+
this.request = request;
|
|
24
|
+
this.parsedUrl = new URL(request.url);
|
|
25
|
+
this.queryParams = this.parsedUrl.searchParams;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ==========================================
|
|
29
|
+
// Basic Request Information
|
|
30
|
+
// ==========================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get the original Request object
|
|
34
|
+
*/
|
|
35
|
+
getOriginal(): Request {
|
|
36
|
+
return this.request;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get the request method
|
|
41
|
+
*/
|
|
42
|
+
method(): string {
|
|
43
|
+
return this.request.method;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get the full URL
|
|
48
|
+
*/
|
|
49
|
+
url(): string {
|
|
50
|
+
return this.request.url;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get the pathname
|
|
55
|
+
*/
|
|
56
|
+
path(): string {
|
|
57
|
+
return this.parsedUrl.pathname;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get the hostname
|
|
62
|
+
*/
|
|
63
|
+
host(): string {
|
|
64
|
+
return this.parsedUrl.host;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get the protocol (http or https)
|
|
69
|
+
*/
|
|
70
|
+
protocol(): string {
|
|
71
|
+
return this.parsedUrl.protocol.replace(':', '');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Check if the request is secure (HTTPS)
|
|
76
|
+
*/
|
|
77
|
+
secure(): boolean {
|
|
78
|
+
return this.parsedUrl.protocol === 'https:';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ==========================================
|
|
82
|
+
// Query Parameters
|
|
83
|
+
// ==========================================
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get all query parameters
|
|
87
|
+
*/
|
|
88
|
+
query(): Record<string, string> {
|
|
89
|
+
const result: Record<string, string> = {};
|
|
90
|
+
this.queryParams.forEach((value, key) => {
|
|
91
|
+
result[key] = value;
|
|
92
|
+
});
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get a specific query parameter
|
|
98
|
+
*/
|
|
99
|
+
queryParam(key: string, defaultValue?: string): string | undefined {
|
|
100
|
+
return this.queryParams.get(key) ?? defaultValue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ==========================================
|
|
104
|
+
// Path Parameters
|
|
105
|
+
// ==========================================
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Set path parameters (used by router)
|
|
109
|
+
*/
|
|
110
|
+
setParams(params: Record<string, string>): void {
|
|
111
|
+
this.pathParams = params;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get all path parameters
|
|
116
|
+
*/
|
|
117
|
+
params(): Record<string, string> {
|
|
118
|
+
return { ...this.pathParams };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get a specific path parameter
|
|
123
|
+
*/
|
|
124
|
+
param(key: string, defaultValue?: string): string | undefined {
|
|
125
|
+
return this.pathParams[key] ?? defaultValue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ==========================================
|
|
129
|
+
// Headers
|
|
130
|
+
// ==========================================
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get request headers
|
|
134
|
+
*/
|
|
135
|
+
headers(): Headers {
|
|
136
|
+
return this.request.headers;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get a specific header
|
|
141
|
+
*/
|
|
142
|
+
header(name: string, defaultValue?: string): string | undefined {
|
|
143
|
+
return this.request.headers.get(name) ?? defaultValue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Check if a header exists
|
|
148
|
+
*/
|
|
149
|
+
hasHeader(name: string): boolean {
|
|
150
|
+
return this.request.headers.has(name);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ==========================================
|
|
154
|
+
// Body Parsing
|
|
155
|
+
// ==========================================
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Parse and return the request body as JSON
|
|
159
|
+
*/
|
|
160
|
+
async json<T = unknown>(): Promise<T> {
|
|
161
|
+
if (!this.bodyParsed) {
|
|
162
|
+
try {
|
|
163
|
+
const text = await this.text();
|
|
164
|
+
this.parsedBody = JSON.parse(text);
|
|
165
|
+
} catch {
|
|
166
|
+
this.parsedBody = null;
|
|
167
|
+
}
|
|
168
|
+
this.bodyParsed = true;
|
|
169
|
+
}
|
|
170
|
+
return this.parsedBody as T;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get the request body as text
|
|
175
|
+
*/
|
|
176
|
+
async text(): Promise<string> {
|
|
177
|
+
if (!this.bodyTextParsed) {
|
|
178
|
+
this.bodyText = await this.request.text();
|
|
179
|
+
this.bodyTextParsed = true;
|
|
180
|
+
}
|
|
181
|
+
return this.bodyText ?? '';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get the request body as FormData
|
|
186
|
+
*/
|
|
187
|
+
async formData() {
|
|
188
|
+
return this.request.formData();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ==========================================
|
|
192
|
+
// Input Methods (Laravel-like)
|
|
193
|
+
// ==========================================
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get all input (query + body merged)
|
|
197
|
+
* Body parameters override query parameters
|
|
198
|
+
*/
|
|
199
|
+
async all(): Promise<Record<string, unknown>> {
|
|
200
|
+
const queryData = this.query();
|
|
201
|
+
const bodyData = await this.json<Record<string, unknown>>() ?? {};
|
|
202
|
+
return { ...queryData, ...bodyData };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get an input value from query or body
|
|
207
|
+
* If no key is provided, returns all input
|
|
208
|
+
*/
|
|
209
|
+
async input<T = unknown>(key?: string, defaultValue?: T): Promise<T | Record<string, unknown>> {
|
|
210
|
+
const allInput = await this.all();
|
|
211
|
+
|
|
212
|
+
if (key === undefined) {
|
|
213
|
+
return allInput;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Support dot notation for nested values
|
|
217
|
+
const value = this.getNestedValue(allInput, key);
|
|
218
|
+
return (value !== undefined ? value : defaultValue) as T;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Check if input has given key(s)
|
|
223
|
+
*/
|
|
224
|
+
async has(...keys: string[]): Promise<boolean> {
|
|
225
|
+
const allInput = await this.all();
|
|
226
|
+
return keys.every(key => this.getNestedValue(allInput, key) !== undefined);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Check if input has non-empty value for given key(s)
|
|
231
|
+
*/
|
|
232
|
+
async filled(...keys: string[]): Promise<boolean> {
|
|
233
|
+
const allInput = await this.all();
|
|
234
|
+
return keys.every(key => {
|
|
235
|
+
const value = this.getNestedValue(allInput, key);
|
|
236
|
+
return value !== undefined && value !== null && value !== '';
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Check if any of the given keys are missing
|
|
242
|
+
*/
|
|
243
|
+
async missing(...keys: string[]): Promise<boolean> {
|
|
244
|
+
return !(await this.has(...keys));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get only specified keys from input
|
|
249
|
+
*/
|
|
250
|
+
async only(...keys: string[]): Promise<Record<string, unknown>> {
|
|
251
|
+
const allInput = await this.all();
|
|
252
|
+
const result: Record<string, unknown> = {};
|
|
253
|
+
|
|
254
|
+
for (const key of keys) {
|
|
255
|
+
const value = this.getNestedValue(allInput, key);
|
|
256
|
+
if (value !== undefined) {
|
|
257
|
+
result[key] = value;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get all input except specified keys
|
|
266
|
+
*/
|
|
267
|
+
async except(...keys: string[]): Promise<Record<string, unknown>> {
|
|
268
|
+
const allInput = await this.all();
|
|
269
|
+
const result: Record<string, unknown> = { ...allInput };
|
|
270
|
+
|
|
271
|
+
for (const key of keys) {
|
|
272
|
+
delete result[key];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get nested value using dot notation
|
|
280
|
+
*/
|
|
281
|
+
private getNestedValue(obj: Record<string, unknown>, path: string): unknown {
|
|
282
|
+
const keys = path.split('.');
|
|
283
|
+
let current: unknown = obj;
|
|
284
|
+
|
|
285
|
+
for (const key of keys) {
|
|
286
|
+
if (current === null || current === undefined || typeof current !== 'object') {
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
current = (current as Record<string, unknown>)[key];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return current;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ==========================================
|
|
296
|
+
// Authentication Helpers
|
|
297
|
+
// ==========================================
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Get the Bearer token from Authorization header
|
|
301
|
+
*/
|
|
302
|
+
bearerToken(): string | null {
|
|
303
|
+
const auth = this.header('authorization');
|
|
304
|
+
if (!auth || !auth.toLowerCase().startsWith('bearer ')) {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
return auth.substring(7).trim();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Get Basic auth credentials
|
|
312
|
+
*/
|
|
313
|
+
basicCredentials(): { username: string; password: string } | null {
|
|
314
|
+
const auth = this.header('authorization');
|
|
315
|
+
if (!auth || !auth.toLowerCase().startsWith('basic ')) {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
const decoded = Buffer.from(auth.substring(6), 'base64').toString('utf-8');
|
|
321
|
+
const [username, password] = decoded.split(':');
|
|
322
|
+
return { username, password };
|
|
323
|
+
} catch {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ==========================================
|
|
329
|
+
// Client Information
|
|
330
|
+
// ==========================================
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Get the client IP address
|
|
334
|
+
*/
|
|
335
|
+
ip(): string | null {
|
|
336
|
+
// Check common proxy headers first
|
|
337
|
+
const xForwardedFor = this.header('x-forwarded-for');
|
|
338
|
+
if (xForwardedFor) {
|
|
339
|
+
return xForwardedFor.split(',')[0].trim();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const xRealIp = this.header('x-real-ip');
|
|
343
|
+
if (xRealIp) {
|
|
344
|
+
return xRealIp;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// CF-Connecting-IP for Cloudflare
|
|
348
|
+
const cfConnectingIp = this.header('cf-connecting-ip');
|
|
349
|
+
if (cfConnectingIp) {
|
|
350
|
+
return cfConnectingIp;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Get all possible IP addresses
|
|
358
|
+
*/
|
|
359
|
+
ips(): string[] {
|
|
360
|
+
const xForwardedFor = this.header('x-forwarded-for');
|
|
361
|
+
if (xForwardedFor) {
|
|
362
|
+
return xForwardedFor.split(',').map(ip => ip.trim());
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const ip = this.ip();
|
|
366
|
+
return ip ? [ip] : [];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Get the User-Agent header
|
|
371
|
+
*/
|
|
372
|
+
userAgent(): string | null {
|
|
373
|
+
return this.header('user-agent') ?? null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Get the referer URL
|
|
378
|
+
*/
|
|
379
|
+
referer(): string | null {
|
|
380
|
+
return this.header('referer') ?? this.header('referrer') ?? null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ==========================================
|
|
384
|
+
// Route Matching
|
|
385
|
+
// ==========================================
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Check if the path matches a pattern
|
|
389
|
+
* Supports wildcards: /users/* matches /users/123
|
|
390
|
+
*/
|
|
391
|
+
is(pattern: string): boolean {
|
|
392
|
+
const path = this.path();
|
|
393
|
+
const regexPattern = pattern
|
|
394
|
+
.replace(/\*/g, '.*')
|
|
395
|
+
.replace(/\//g, '\\/');
|
|
396
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
397
|
+
return regex.test(path);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Check if the HTTP method matches
|
|
402
|
+
*/
|
|
403
|
+
isMethod(method: string | string[]): boolean {
|
|
404
|
+
const currentMethod = this.method().toUpperCase();
|
|
405
|
+
if (Array.isArray(method)) {
|
|
406
|
+
return method.some(m => m.toUpperCase() === currentMethod);
|
|
407
|
+
}
|
|
408
|
+
return method.toUpperCase() === currentMethod;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ==========================================
|
|
412
|
+
// Content Type Detection
|
|
413
|
+
// ==========================================
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Check if the request expects JSON
|
|
417
|
+
*/
|
|
418
|
+
expectsJson(): boolean {
|
|
419
|
+
const accept = this.header('accept', '');
|
|
420
|
+
return accept !== undefined && accept.includes('application/json');
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Check if the request content is JSON
|
|
425
|
+
*/
|
|
426
|
+
isJson(): boolean {
|
|
427
|
+
const contentType = this.header('content-type', '');
|
|
428
|
+
return contentType !== undefined && contentType.includes('application/json');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Check if request is form data
|
|
433
|
+
*/
|
|
434
|
+
isFormData(): boolean {
|
|
435
|
+
const contentType = this.contentType() ?? '';
|
|
436
|
+
return contentType.includes('multipart/form-data') ||
|
|
437
|
+
contentType.includes('application/x-www-form-urlencoded');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Get the content type
|
|
442
|
+
*/
|
|
443
|
+
contentType(): string | undefined {
|
|
444
|
+
return this.header('content-type');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Check if request wants HTML
|
|
449
|
+
*/
|
|
450
|
+
wantsHtml(): boolean {
|
|
451
|
+
const accept = this.header('accept', '');
|
|
452
|
+
return accept !== undefined && accept.includes('text/html');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ==========================================
|
|
456
|
+
// Cookies
|
|
457
|
+
// ==========================================
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Get all cookies
|
|
461
|
+
*/
|
|
462
|
+
cookies(): Record<string, string> {
|
|
463
|
+
if (this.parsedCookies === null) {
|
|
464
|
+
this.parsedCookies = this.parseCookieHeader();
|
|
465
|
+
}
|
|
466
|
+
return { ...this.parsedCookies };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Get a specific cookie
|
|
471
|
+
*/
|
|
472
|
+
cookie(name: string, defaultValue?: string): string | undefined {
|
|
473
|
+
const cookies = this.cookies();
|
|
474
|
+
return cookies[name] ?? defaultValue;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Check if a cookie exists
|
|
479
|
+
*/
|
|
480
|
+
hasCookie(name: string): boolean {
|
|
481
|
+
return this.cookie(name) !== undefined;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Parse the Cookie header
|
|
486
|
+
*/
|
|
487
|
+
private parseCookieHeader(): Record<string, string> {
|
|
488
|
+
const cookieHeader = this.header('cookie');
|
|
489
|
+
if (!cookieHeader) {
|
|
490
|
+
return {};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const cookies: Record<string, string> = {};
|
|
494
|
+
const pairs = cookieHeader.split(';');
|
|
495
|
+
|
|
496
|
+
for (const pair of pairs) {
|
|
497
|
+
const [name, ...valueParts] = pair.split('=');
|
|
498
|
+
if (name) {
|
|
499
|
+
const trimmedName = name.trim();
|
|
500
|
+
const value = valueParts.join('=').trim();
|
|
501
|
+
// Decode URI encoded values
|
|
502
|
+
try {
|
|
503
|
+
cookies[trimmedName] = decodeURIComponent(value);
|
|
504
|
+
} catch {
|
|
505
|
+
cookies[trimmedName] = value;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return cookies;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ==========================================
|
|
514
|
+
// File Uploads
|
|
515
|
+
// ==========================================
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Get an uploaded file by name
|
|
519
|
+
*/
|
|
520
|
+
async file(name: string): Promise<UploadedFile | null> {
|
|
521
|
+
const files = await this.files();
|
|
522
|
+
const fileList = files.get(name);
|
|
523
|
+
return fileList && fileList.length > 0 ? fileList[0] : null;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Get all uploaded files
|
|
528
|
+
*/
|
|
529
|
+
async files(): Promise<Map<string, UploadedFile[]>> {
|
|
530
|
+
if (this.parsedFiles !== null) {
|
|
531
|
+
return this.parsedFiles;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
this.parsedFiles = new Map();
|
|
535
|
+
|
|
536
|
+
if (!this.isFormData()) {
|
|
537
|
+
return this.parsedFiles;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
try {
|
|
541
|
+
const formData = await this.request.formData();
|
|
542
|
+
|
|
543
|
+
for (const [key, value] of formData.entries()) {
|
|
544
|
+
if (value instanceof File) {
|
|
545
|
+
const arrayBuffer = await value.arrayBuffer();
|
|
546
|
+
const uploadedFile = new UploadedFile({
|
|
547
|
+
name: key,
|
|
548
|
+
originalName: value.name,
|
|
549
|
+
mimeType: value.type || 'application/octet-stream',
|
|
550
|
+
size: value.size,
|
|
551
|
+
content: arrayBuffer,
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
const existing = this.parsedFiles.get(key) ?? [];
|
|
555
|
+
existing.push(uploadedFile);
|
|
556
|
+
this.parsedFiles.set(key, existing);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
} catch {
|
|
560
|
+
// If formData parsing fails, return empty map
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return this.parsedFiles;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Check if request has an uploaded file
|
|
568
|
+
*/
|
|
569
|
+
async hasFile(name: string): Promise<boolean> {
|
|
570
|
+
const file = await this.file(name);
|
|
571
|
+
return file !== null;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// ==========================================
|
|
575
|
+
// Request Fingerprinting
|
|
576
|
+
// ==========================================
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Generate a unique fingerprint for the request
|
|
580
|
+
*/
|
|
581
|
+
fingerprint(): string {
|
|
582
|
+
const components = [
|
|
583
|
+
this.method(),
|
|
584
|
+
this.path(),
|
|
585
|
+
this.ip() ?? 'unknown',
|
|
586
|
+
this.userAgent() ?? 'unknown',
|
|
587
|
+
];
|
|
588
|
+
return components.join('|');
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// ==========================================
|
|
592
|
+
// AJAX / API Detection
|
|
593
|
+
// ==========================================
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Check if request is an AJAX/XHR request
|
|
597
|
+
*/
|
|
598
|
+
ajax(): boolean {
|
|
599
|
+
return this.header('x-requested-with')?.toLowerCase() === 'xmlhttprequest';
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Check if request is a preflight request
|
|
604
|
+
*/
|
|
605
|
+
isPreflight(): boolean {
|
|
606
|
+
return this.isMethod('OPTIONS') && this.hasHeader('access-control-request-method');
|
|
607
|
+
}
|
|
608
|
+
}
|