@tamayrp14/fire-api 0.0.0-stage → 1.0.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 (98) hide show
  1. package/README.md +276 -2
  2. package/bin/fire.js +139 -0
  3. package/bun.lock +20 -0
  4. package/docs/GETTING_STARTED.md +150 -0
  5. package/docs/LAUNCH_KIT.md +236 -0
  6. package/docs/superpowers/plans/2026-11-14-fire-api-framework.md +144 -0
  7. package/docs/superpowers/specs/fire-api-design-spec.md +111 -0
  8. package/examples/basic-api/index.ts +177 -0
  9. package/package.json +68 -4
  10. package/src/ai/mcp.ts +140 -0
  11. package/src/auth/auth.ts +110 -0
  12. package/src/auth/crypto.ts +70 -0
  13. package/src/auth/jwt.ts +105 -0
  14. package/src/cache/cache.ts +72 -0
  15. package/src/cli/bench.ts +118 -0
  16. package/src/cli/compile.ts +29 -0
  17. package/src/cli/create-app.ts +284 -0
  18. package/src/cli/docker.ts +51 -0
  19. package/src/cli/generator.ts +75 -0
  20. package/src/cli/routes.ts +76 -0
  21. package/src/client.ts +120 -0
  22. package/src/compat/express.ts +139 -0
  23. package/src/core/app.ts +723 -0
  24. package/src/core/context.ts +218 -0
  25. package/src/core/error.ts +42 -0
  26. package/src/core/router.ts +262 -0
  27. package/src/db/db-interface.ts +9 -0
  28. package/src/db/hub.ts +256 -0
  29. package/src/db/memory-adapter.ts +202 -0
  30. package/src/db/migrator.ts +100 -0
  31. package/src/db/sqlite-adapter.ts +58 -0
  32. package/src/docs/collection.ts +57 -0
  33. package/src/docs/scalar-ui.ts +25 -0
  34. package/src/events/event-bus.ts +61 -0
  35. package/src/index.ts +37 -0
  36. package/src/jobs/cron.ts +77 -0
  37. package/src/jobs/queue.ts +108 -0
  38. package/src/logging/logger.ts +66 -0
  39. package/src/metrics/metrics.ts +66 -0
  40. package/src/modules/product/product.route.ts +36 -0
  41. package/src/modules/product/product.schema.ts +10 -0
  42. package/src/schema/mock.ts +60 -0
  43. package/src/schema/openapi.ts +101 -0
  44. package/src/schema/schema.ts +519 -0
  45. package/src/schema/validator.ts +5 -0
  46. package/src/shield/compress.ts +61 -0
  47. package/src/shield/cors.ts +94 -0
  48. package/src/shield/headers.ts +55 -0
  49. package/src/shield/idempotency.ts +76 -0
  50. package/src/shield/rate-limiter.ts +81 -0
  51. package/src/shield/sanitizer.ts +23 -0
  52. package/src/shield/webhook.ts +101 -0
  53. package/src/static/serve.ts +51 -0
  54. package/src/storage/storage.ts +176 -0
  55. package/src/stream/sse.ts +89 -0
  56. package/src/studio/studio.ts +221 -0
  57. package/src/types.ts +241 -0
  58. package/src/ws/ws.ts +118 -0
  59. package/test/ai-mcp.test.ts +61 -0
  60. package/test/auth.test.ts +108 -0
  61. package/test/bench.test.ts +31 -0
  62. package/test/cache.test.ts +36 -0
  63. package/test/cli-create.test.ts +64 -0
  64. package/test/client.test.ts +54 -0
  65. package/test/collection-export.test.ts +52 -0
  66. package/test/compat.test.ts +48 -0
  67. package/test/compress.test.ts +48 -0
  68. package/test/context.test.ts +48 -0
  69. package/test/db.test.ts +62 -0
  70. package/test/docker.test.ts +28 -0
  71. package/test/events.test.ts +62 -0
  72. package/test/file-upload.test.ts +116 -0
  73. package/test/hooks.test.ts +77 -0
  74. package/test/idempotency.test.ts +65 -0
  75. package/test/integration.test.ts +111 -0
  76. package/test/jobs.test.ts +60 -0
  77. package/test/metrics.test.ts +40 -0
  78. package/test/mock.test.ts +33 -0
  79. package/test/multi-tenant.test.ts +80 -0
  80. package/test/observability.test.ts +95 -0
  81. package/test/openapi.test.ts +41 -0
  82. package/test/orm-relations.test.ts +85 -0
  83. package/test/orm-shortcuts.test.ts +52 -0
  84. package/test/rbac.test.ts +85 -0
  85. package/test/router-fastpath.test.ts +42 -0
  86. package/test/router.test.ts +84 -0
  87. package/test/schema.test.ts +49 -0
  88. package/test/seeder-migrator.test.ts +78 -0
  89. package/test/shield.test.ts +55 -0
  90. package/test/shorthand-dx.test.ts +31 -0
  91. package/test/sse.test.ts +43 -0
  92. package/test/static-serve.test.ts +53 -0
  93. package/test/storage.test.ts +31 -0
  94. package/test/studio.test.ts +43 -0
  95. package/test/tx.test.ts +92 -0
  96. package/test/webhook.test.ts +113 -0
  97. package/test/ws.test.ts +54 -0
  98. package/tsconfig.json +19 -0
@@ -0,0 +1,723 @@
1
+ import {
2
+ FireAppOptions,
3
+ FireCacheManager,
4
+ FireDatabase,
5
+ Handler,
6
+ HookType,
7
+ HttpMethod,
8
+ Middleware,
9
+ RouteOptions,
10
+ } from '../types';
11
+ import { Router } from './router';
12
+ import { createContext } from './context';
13
+ import { HttpError } from './error';
14
+ import { applySecurityHeaders } from '../shield/headers';
15
+ import { appendCorsHeaders, handleCORS } from '../shield/cors';
16
+ import { RateLimiter } from '../shield/rate-limiter';
17
+ import { sanitizePayload } from '../shield/sanitizer';
18
+ import { compressResponse } from '../shield/compress';
19
+ import { IdempotencyManager } from '../shield/idempotency';
20
+ import { createDatabaseHub, createProxyDatabase } from '../db/hub';
21
+ import { MemoryCacheManager } from '../cache/cache';
22
+ import { generateOpenAPISpec } from '../schema/openapi';
23
+ import { renderScalarDocsHTML } from '../docs/scalar-ui';
24
+ import { generatePostmanCollection } from '../docs/collection';
25
+ import { createStaticFileResponse } from '../static/serve';
26
+ import * as path from 'path';
27
+ import { createMCPServer, MCPServer } from '../ai/mcp';
28
+ import { BaseSchema } from '../schema/schema';
29
+ import { FireQueueManager } from '../jobs/queue';
30
+ import { FireCronScheduler, CronTask } from '../jobs/cron';
31
+ import { WebSocketHub, FireWebSocketHandler } from '../ws/ws';
32
+ import { createStorage, FireStorage } from '../storage/storage';
33
+ import { MetricsCollector } from '../metrics/metrics';
34
+ import { EventBus, EventListener } from '../events/event-bus';
35
+ import { UploadedFile } from '../types';
36
+
37
+ export class FireApp {
38
+ public router: Router;
39
+ public db: FireDatabase;
40
+ public cache: FireCacheManager;
41
+ public queue: FireQueueManager;
42
+ public cronScheduler: FireCronScheduler;
43
+ public wsHub: WebSocketHub;
44
+ public storage: FireStorage;
45
+ public metrics: MetricsCollector;
46
+ public events = new EventBus();
47
+ public wsHandlers: Map<string, FireWebSocketHandler> = new Map();
48
+ public mcpServer?: MCPServer;
49
+ private options: FireAppOptions;
50
+ private rateLimiter?: RateLimiter;
51
+ private idempotencyManager = new IdempotencyManager();
52
+ private globalMiddlewares: Middleware[] = [];
53
+ private hooks: Record<HookType, Array<(arg1: any, arg2?: any) => Promise<any> | any>> = {
54
+ onRequest: [],
55
+ beforeHandle: [],
56
+ afterHandle: [],
57
+ onError: [],
58
+ };
59
+
60
+ constructor(options: FireAppOptions = {}) {
61
+ this.options = {
62
+ port: 3000,
63
+ docs: '/docs',
64
+ openapiPath: '/openapi.json',
65
+ metricsPath: '/metrics',
66
+ ...options,
67
+ };
68
+
69
+ this.router = new Router(this.options.prefix || '');
70
+ this.db = createDatabaseHub(this.options.database);
71
+ this.cache = new MemoryCacheManager();
72
+ this.queue = new FireQueueManager();
73
+ this.cronScheduler = new FireCronScheduler();
74
+ this.wsHub = new WebSocketHub();
75
+ this.storage = createStorage(this.options.storage);
76
+ this.metrics = new MetricsCollector();
77
+
78
+ // Initialize Security Shield
79
+ if (this.options.security?.rateLimit) {
80
+ this.rateLimiter = new RateLimiter(this.options.security.rateLimit);
81
+ } else if (this.options.security?.enabled !== false) {
82
+ // Default rate limit: 1000 requests per minute
83
+ this.rateLimiter = new RateLimiter({ max: 1000, window: '1m' });
84
+ }
85
+
86
+ // Auto-mount Docs, OpenAPI, and Metrics endpoints if configured
87
+ this.setupDocumentationRoutes();
88
+ this.setupMetricsRoute();
89
+ this.setupHealthRoutes();
90
+ }
91
+
92
+ private setupMetricsRoute(): void {
93
+ const metricsPath = this.options.metricsPath;
94
+ if (metricsPath) {
95
+ this.router.get(metricsPath, {
96
+ handler: (ctx) => {
97
+ const content = this.metrics.formatPrometheus();
98
+ return ctx.text(content, 200, {
99
+ 'Content-Type': 'text/plain; version=0.0.4; charset=utf-8',
100
+ });
101
+ },
102
+ });
103
+ }
104
+ }
105
+
106
+ private setupDocumentationRoutes(): void {
107
+ const openapiPath = this.options.openapiPath;
108
+ const docsPath = this.options.docs;
109
+
110
+ if (openapiPath) {
111
+ this.router.get(openapiPath, {
112
+ handler: (ctx) => {
113
+ const spec = generateOpenAPISpec(this.router, {
114
+ title: 'FireAPI Documentation',
115
+ version: '1.0.0',
116
+ });
117
+ return ctx.json(spec);
118
+ },
119
+ });
120
+ }
121
+
122
+ if (docsPath && openapiPath) {
123
+ this.router.get(docsPath, {
124
+ handler: (ctx) => {
125
+ const html = renderScalarDocsHTML(openapiPath, 'FireAPI Interactive Docs');
126
+ return ctx.html(html);
127
+ },
128
+ });
129
+
130
+ this.router.get('/docs/collection.json', {
131
+ handler: (ctx) => {
132
+ const collection = generatePostmanCollection(this.router, {
133
+ name: 'FireAPI Generated Collection',
134
+ });
135
+ return ctx.json(collection);
136
+ },
137
+ });
138
+ }
139
+ }
140
+
141
+ public serveStatic(routePrefix: string, localDirectory: string): this {
142
+ const normalizedPrefix = routePrefix.endsWith('/') ? routePrefix.slice(0, -1) : routePrefix;
143
+ this.router.get(`${normalizedPrefix}/*`, {
144
+ handler: (ctx) => {
145
+ const url = new URL(ctx.req.url);
146
+ const subPath = url.pathname.slice(normalizedPrefix.length).replace(/^\/+/, '');
147
+ const filePath = path.resolve(localDirectory, subPath);
148
+ const res = createStaticFileResponse(filePath);
149
+ if (!res) {
150
+ return ctx.notFound(`File ${subPath} not found`);
151
+ }
152
+ return res;
153
+ },
154
+ });
155
+ return this;
156
+ }
157
+
158
+ public serveSPA(routePath: string, htmlFilePath: string): this {
159
+ this.router.get(routePath, {
160
+ handler: (ctx) => {
161
+ const res = createStaticFileResponse(path.resolve(htmlFilePath));
162
+ if (!res) {
163
+ return ctx.notFound('SPA index file not found');
164
+ }
165
+ return res;
166
+ },
167
+ });
168
+ return this;
169
+ }
170
+
171
+ private setupHealthRoutes(): void {
172
+ if (this.options.health === false) return;
173
+ const healthConfig = typeof this.options.health === 'object' ? this.options.health : {};
174
+ const livenessPath = healthConfig.livenessPath || '/healthz';
175
+ const readinessPath = healthConfig.readinessPath || '/readyz';
176
+
177
+ // Liveness Probe (process is alive)
178
+ this.router.get(livenessPath, {
179
+ handler: (ctx) => {
180
+ return ctx.ok({
181
+ status: 'ok',
182
+ uptime: typeof process !== 'undefined' && process.uptime ? Math.floor(process.uptime()) : 0,
183
+ timestamp: Date.now(),
184
+ });
185
+ },
186
+ });
187
+
188
+ // Readiness Probe (DB & storage dependencies ready)
189
+ this.router.get(readinessPath, {
190
+ handler: async (ctx) => {
191
+ const checks: Record<string, 'ok' | 'down'> = {};
192
+ let isHealthy = true;
193
+
194
+ if (healthConfig.checkDatabase !== false) {
195
+ try {
196
+ await this.db.raw('SELECT 1');
197
+ checks.database = 'ok';
198
+ } catch {
199
+ checks.database = 'down';
200
+ isHealthy = false;
201
+ }
202
+ }
203
+
204
+ if (healthConfig.checkStorage) {
205
+ try {
206
+ await this.storage.exists('healthcheck');
207
+ checks.storage = 'ok';
208
+ } catch {
209
+ checks.storage = 'down';
210
+ isHealthy = false;
211
+ }
212
+ }
213
+
214
+ if (healthConfig.customChecks) {
215
+ for (const checkFn of healthConfig.customChecks) {
216
+ try {
217
+ const res = await checkFn();
218
+ checks[res.name] = res.status === 'ok' ? 'ok' : 'down';
219
+ if (res.status === 'down') isHealthy = false;
220
+ } catch {
221
+ isHealthy = false;
222
+ }
223
+ }
224
+ }
225
+
226
+ if (!isHealthy) {
227
+ return ctx.json({ status: 'down', checks }, 503);
228
+ }
229
+
230
+ return ctx.ok({ status: 'ok', checks });
231
+ },
232
+ });
233
+ }
234
+
235
+ public hook(type: HookType, handler: (arg1: any, arg2?: any) => Promise<any> | any): this {
236
+ this.hooks[type].push(handler);
237
+ return this;
238
+ }
239
+
240
+ public use(middleware: Middleware): this;
241
+ public use(subRouter: Router): this;
242
+ public use(prefix: string, subRouter: Router): this;
243
+ public use(arg1: string | Middleware | Router, arg2?: Router): this {
244
+ if (typeof arg1 === 'function') {
245
+ this.globalMiddlewares.push(arg1);
246
+ return this;
247
+ }
248
+
249
+ if (arg1 instanceof Router) {
250
+ this.router.use('', arg1);
251
+ return this;
252
+ }
253
+
254
+ if (typeof arg1 === 'string' && arg2 instanceof Router) {
255
+ this.router.use(arg1, arg2);
256
+ return this;
257
+ }
258
+
259
+ return this;
260
+ }
261
+
262
+ public cron(expression: string, handler: (app: FireApp) => Promise<void> | void): CronTask {
263
+ return this.cronScheduler.schedule(expression, () => handler(this));
264
+ }
265
+
266
+ public ws(path: string, handler: FireWebSocketHandler): this {
267
+ this.wsHandlers.set(path, handler);
268
+ return this;
269
+ }
270
+
271
+ public on<T = any>(event: string, listener: EventListener<T>): () => void {
272
+ return this.events.on(event, listener);
273
+ }
274
+
275
+ public async emit<T = any>(event: string, payload: T): Promise<void> {
276
+ return this.events.emit(event, payload);
277
+ }
278
+
279
+ public get(path: string, optionsOrHandler: RouteOptions | Handler): this {
280
+ this.router.get(path, optionsOrHandler);
281
+ return this;
282
+ }
283
+
284
+ public post(path: string, optionsOrHandler: RouteOptions | Handler): this {
285
+ this.router.post(path, optionsOrHandler);
286
+ return this;
287
+ }
288
+
289
+ public put(path: string, optionsOrHandler: RouteOptions | Handler): this {
290
+ this.router.put(path, optionsOrHandler);
291
+ return this;
292
+ }
293
+
294
+ public patch(path: string, optionsOrHandler: RouteOptions | Handler): this {
295
+ this.router.patch(path, optionsOrHandler);
296
+ return this;
297
+ }
298
+
299
+ public delete(path: string, optionsOrHandler: RouteOptions | Handler): this {
300
+ this.router.delete(path, optionsOrHandler);
301
+ return this;
302
+ }
303
+
304
+ public enableMCP(options: { name?: string; version?: string; endpoint?: string } = {}): this {
305
+ this.mcpServer = createMCPServer(this.router, options);
306
+ const endpoint = options.endpoint || '/mcp';
307
+
308
+ this.router.post(endpoint, {
309
+ handler: async (ctx) => {
310
+ const body = ctx.body;
311
+ if (!body || typeof body !== 'object') {
312
+ return ctx.badRequest('Invalid JSON-RPC request');
313
+ }
314
+ const result = await this.mcpServer!.handleMCPProtocol(body);
315
+ return ctx.json(result);
316
+ },
317
+ });
318
+
319
+ return this;
320
+ }
321
+
322
+ /**
323
+ * The pure Web Standard fetch handler: (request: Request) => Promise<Response>
324
+ * Runs natively on Bun, Node.js (via adapter), Cloudflare Workers, and Deno!
325
+ */
326
+ public async handle(request: Request): Promise<Response> {
327
+ const startTime = performance.now();
328
+ const url = new URL(request.url);
329
+
330
+ // 1. CORS Preflight & Checking
331
+ if (this.options.security?.cors !== false) {
332
+ const corsConfig = typeof this.options.security?.cors === 'object' ? this.options.security.cors : {};
333
+ const corsPreflight = handleCORS(request, corsConfig);
334
+ if (corsPreflight) return corsPreflight;
335
+ }
336
+
337
+ // 2. Global Rate Limiter
338
+ if (this.rateLimiter) {
339
+ const clientIp = request.headers.get('x-forwarded-for') || request.headers.get('cf-connecting-ip') || '127.0.0.1';
340
+ const limitCheck = this.rateLimiter.check(clientIp);
341
+ if (!limitCheck.allowed) {
342
+ return new HttpError(429, 'Too Many Requests', { retryAfter: limitCheck.retryAfter }).toResponse();
343
+ }
344
+ }
345
+
346
+ // 3. Match Route in Radix Router
347
+ const method = request.method as HttpMethod;
348
+ const match = this.router.match(method, url.pathname);
349
+
350
+ if (!match) {
351
+ const notFoundRes = new HttpError(404, `Cannot ${method} ${url.pathname}`).toResponse();
352
+ return this.finalizeResponse(notFoundRes, request, startTime);
353
+ }
354
+
355
+ // Check Idempotency Key
356
+ const idempotencyOption = match.routeOptions.idempotent;
357
+ const idempotencyKey = idempotencyOption
358
+ ? (request.headers.get('x-idempotency-key') || request.headers.get('idempotency-key'))
359
+ : null;
360
+
361
+ if (idempotencyKey) {
362
+ const cached = this.idempotencyManager.get(idempotencyKey);
363
+ if (cached) {
364
+ const replayRes = this.idempotencyManager.createResponse(cached);
365
+ return this.finalizeResponse(replayRes, request, startTime);
366
+ }
367
+
368
+ if (this.idempotencyManager.isProcessing(idempotencyKey)) {
369
+ return this.finalizeResponse(
370
+ new HttpError(409, 'A request with this Idempotency-Key is currently being processed').toResponse(),
371
+ request,
372
+ startTime
373
+ );
374
+ }
375
+
376
+ this.idempotencyManager.markProcessing(idempotencyKey);
377
+ }
378
+
379
+ try {
380
+ // 4. Parse & Sanitize Request Body (if any)
381
+ let parsedBody: any = null;
382
+ let rawBody = '';
383
+ const uploadedFiles: Record<string, UploadedFile> = {};
384
+
385
+ if (method !== 'GET' && method !== 'HEAD' && request.body) {
386
+ const contentType = request.headers.get('content-type') || '';
387
+
388
+ if (contentType.includes('multipart/form-data')) {
389
+ try {
390
+ const formData = await request.formData();
391
+ parsedBody = {};
392
+ formData.forEach((value, key) => {
393
+ if (typeof value === 'object' && value !== null && 'arrayBuffer' in value) {
394
+ const fileBlob = value as Blob & { name?: string };
395
+ const fileName = fileBlob.name || 'upload.bin';
396
+ const uploadedFile: UploadedFile = {
397
+ name: fileName,
398
+ size: fileBlob.size,
399
+ type: fileBlob.type || 'application/octet-stream',
400
+ bytes: async () => new Uint8Array(await fileBlob.arrayBuffer()),
401
+ text: async () => fileBlob.text(),
402
+ store: async (destinationDir = 'uploads', opts = {}) => {
403
+ const destName = opts.fileName || `${Date.now()}_${fileName.replace(/[^a-zA-Z0-9._-]/g, '_')}`;
404
+ const targetPath = destinationDir ? `${destinationDir}/${destName}` : destName;
405
+ const content = new Uint8Array(await fileBlob.arrayBuffer());
406
+ return this.storage.put(targetPath, content, { contentType: fileBlob.type });
407
+ },
408
+ };
409
+ uploadedFiles[key] = uploadedFile;
410
+ parsedBody[key] = uploadedFile;
411
+ } else {
412
+ parsedBody[key] = value;
413
+ }
414
+ });
415
+ } catch {
416
+ return this.finalizeResponse(
417
+ new HttpError(400, 'Malformed multipart form data in request').toResponse(),
418
+ request,
419
+ startTime
420
+ );
421
+ }
422
+ } else {
423
+ try {
424
+ rawBody = await request.text();
425
+ if (contentType.includes('application/json') && rawBody.trim()) {
426
+ const rawJson = JSON.parse(rawBody);
427
+ // Prototype Pollution Guard
428
+ parsedBody = this.options.security?.prototypePollutionGuard !== false
429
+ ? sanitizePayload(rawJson)
430
+ : rawJson;
431
+ } else {
432
+ parsedBody = rawBody;
433
+ }
434
+ } catch {
435
+ return this.finalizeResponse(
436
+ new HttpError(400, 'Malformed JSON in request body').toResponse(),
437
+ request,
438
+ startTime
439
+ );
440
+ }
441
+ }
442
+ }
443
+
444
+ // 5. Schema Validation (Params, Query, Body)
445
+ const schema = match.routeOptions.schema;
446
+ if (schema) {
447
+ // Body validation
448
+ if (schema.body && schema.body instanceof BaseSchema) {
449
+ const bodyVal = schema.body.validate(parsedBody);
450
+ if (!bodyVal.success) {
451
+ return this.finalizeResponse(
452
+ new HttpError(400, 'Validation Error', bodyVal.errors).toResponse(),
453
+ request,
454
+ startTime
455
+ );
456
+ }
457
+ parsedBody = bodyVal.data;
458
+ }
459
+
460
+ // Params validation
461
+ if (schema.params && schema.params instanceof BaseSchema) {
462
+ const paramsVal = schema.params.validate(match.params);
463
+ if (!paramsVal.success) {
464
+ return this.finalizeResponse(
465
+ new HttpError(400, 'Invalid Path Parameters', paramsVal.errors).toResponse(),
466
+ request,
467
+ startTime
468
+ );
469
+ }
470
+ match.params = paramsVal.data;
471
+ }
472
+ }
473
+
474
+ // Multi-Tenant Context Resolution
475
+ let tenantObj: { id: string } | undefined;
476
+ let effectiveDb = this.db;
477
+ if (this.options.multiTenant) {
478
+ const headerName = this.options.multiTenant.header || 'x-tenant-id';
479
+ const queryName = this.options.multiTenant.query || 'tenant';
480
+ const tenantId = request.headers.get(headerName) || url.searchParams.get(queryName);
481
+ if (tenantId) {
482
+ tenantObj = { id: tenantId };
483
+ if (this.options.multiTenant.autoScopeDB !== false) {
484
+ effectiveDb = createProxyDatabase(this.db, { tenantId });
485
+ }
486
+ }
487
+ }
488
+
489
+ // 6. Construct FireContext
490
+ const ctx = createContext({
491
+ req: request,
492
+ params: match.params,
493
+ db: effectiveDb,
494
+ cache: this.cache,
495
+ queue: this.queue,
496
+ storage: this.storage,
497
+ body: parsedBody,
498
+ files: uploadedFiles,
499
+ rawBody,
500
+ tenant: tenantObj,
501
+ emit: (event, payload) => this.events.emit(event, payload),
502
+ });
503
+
504
+ // Lifecycle Hook: onRequest
505
+ for (const hook of this.hooks.onRequest) {
506
+ const hookRes = await hook(ctx);
507
+ if (hookRes instanceof Response) {
508
+ return this.finalizeResponse(hookRes, request, startTime, ctx.requestId);
509
+ }
510
+ }
511
+
512
+ // 7. Check Route Cache
513
+ const cacheRule = match.routeOptions.cache;
514
+ let cacheKey = '';
515
+ if (method === 'GET' && cacheRule) {
516
+ cacheKey = `route:${url.pathname}${url.search}`;
517
+ const cached = await this.cache.get(cacheKey);
518
+ if (cached) {
519
+ const cachedRes = ctx.json(cached);
520
+ cachedRes.headers.set('X-Cache', 'HIT');
521
+ return this.finalizeResponse(cachedRes, request, startTime, ctx.requestId);
522
+ }
523
+ }
524
+
525
+ // 8. Execute Middleware Chain & Handler
526
+ const middlewares = [...this.globalMiddlewares, ...match.middlewares];
527
+ let currentIdx = 0;
528
+
529
+ const next = async (): Promise<Response> => {
530
+ if (currentIdx < middlewares.length) {
531
+ const mw = middlewares[currentIdx++];
532
+ return mw(ctx, next);
533
+ }
534
+
535
+ // Lifecycle Hook: beforeHandle
536
+ for (const hook of this.hooks.beforeHandle) {
537
+ const hookRes = await hook(ctx);
538
+ if (hookRes instanceof Response) {
539
+ return hookRes;
540
+ }
541
+ }
542
+
543
+ // Check role requirements (RBAC)
544
+ if (match.routeOptions.roles && match.routeOptions.roles.length > 0) {
545
+ const userRole = ctx.user?.role;
546
+ const userRoles = Array.isArray(ctx.user?.roles) ? ctx.user.roles : (userRole ? [userRole] : []);
547
+ const hasRole = match.routeOptions.roles.some((r) => userRoles.includes(r));
548
+ if (!hasRole) {
549
+ return ctx.forbidden('Insufficient permissions');
550
+ }
551
+ }
552
+
553
+ // Check dynamic policy gate
554
+ if (match.routeOptions.can) {
555
+ const isAllowed = await match.routeOptions.can(ctx.user, ctx);
556
+ if (!isAllowed) {
557
+ return ctx.forbidden('Access denied by policy gate');
558
+ }
559
+ }
560
+
561
+ // Call final handler
562
+ const handlerResult = await match.handler(ctx);
563
+ if (handlerResult instanceof Response) {
564
+ return handlerResult;
565
+ }
566
+ if (typeof handlerResult === 'string') {
567
+ return ctx.text(handlerResult);
568
+ }
569
+ return ctx.ok(handlerResult);
570
+ };
571
+
572
+ let resultResponse = await next();
573
+
574
+ // Lifecycle Hook: afterHandle
575
+ for (const hook of this.hooks.afterHandle) {
576
+ const modified = await hook(ctx, resultResponse);
577
+ if (modified instanceof Response) {
578
+ resultResponse = modified;
579
+ }
580
+ }
581
+
582
+ // If caching enabled for this route, store payload
583
+ if (method === 'GET' && cacheRule && resultResponse.ok) {
584
+ try {
585
+ const cloned = resultResponse.clone();
586
+ const json = await cloned.json();
587
+ const ttlSecs = typeof cacheRule.ttl === 'number' ? cacheRule.ttl : 60;
588
+ await this.cache.set(cacheKey, json, ttlSecs, cacheRule.tags || []);
589
+ } catch {}
590
+ }
591
+
592
+ // If idempotency enabled and key provided, cache response
593
+ if (idempotencyKey && resultResponse.ok) {
594
+ const ttl = typeof idempotencyOption === 'object' && idempotencyOption.ttl ? idempotencyOption.ttl : 86400;
595
+ await this.idempotencyManager.storeResponse(idempotencyKey, resultResponse, ttl);
596
+ } else if (idempotencyKey) {
597
+ this.idempotencyManager.release(idempotencyKey);
598
+ }
599
+
600
+ return this.finalizeResponse(resultResponse, request, startTime, ctx.requestId);
601
+ } catch (err: any) {
602
+ for (const hook of this.hooks.onError) {
603
+ try {
604
+ await hook(err);
605
+ } catch {}
606
+ }
607
+ if (idempotencyKey) {
608
+ this.idempotencyManager.release(idempotencyKey);
609
+ }
610
+ if (err instanceof HttpError) {
611
+ return this.finalizeResponse(err.toResponse(), request, startTime);
612
+ }
613
+ const internalRes = new HttpError(500, err?.message || 'Internal Server Error').toResponse();
614
+ return this.finalizeResponse(internalRes, request, startTime);
615
+ }
616
+ }
617
+
618
+ private async finalizeResponse(response: Response, request: Request, startTime: number, reqId?: string): Promise<Response> {
619
+ // 1. Calculate and set response time & request ID headers
620
+ const durationMs = (performance.now() - startTime).toFixed(2);
621
+ const headers = new Headers(response.headers);
622
+ headers.set('X-Response-Time', `${durationMs}ms`);
623
+
624
+ const effectiveReqId = reqId || request.headers.get('x-request-id');
625
+ if (effectiveReqId) {
626
+ headers.set('X-Request-ID', effectiveReqId);
627
+ }
628
+
629
+ // Record metrics
630
+ try {
631
+ const url = new URL(request.url);
632
+ this.metrics.recordRequest(request.method, url.pathname, response.status, parseFloat(durationMs));
633
+ } catch {}
634
+
635
+ let finalRes = new Response(response.body, {
636
+ status: response.status,
637
+ statusText: response.statusText,
638
+ headers,
639
+ });
640
+
641
+ // 2. Append Security Headers
642
+ if (this.options.security?.headers !== false) {
643
+ finalRes = applySecurityHeaders(finalRes, this.options.security?.headers);
644
+ }
645
+
646
+ // 3. Append CORS Headers
647
+ if (this.options.security?.cors !== false) {
648
+ const corsConfig = typeof this.options.security?.cors === 'object' ? this.options.security.cors : {};
649
+ finalRes = appendCorsHeaders(finalRes, request, corsConfig);
650
+ }
651
+
652
+ // 4. Stream Compression
653
+ if (this.options.compression) {
654
+ const compConfig = typeof this.options.compression === 'object' ? this.options.compression : {};
655
+ finalRes = await compressResponse(finalRes, request, compConfig);
656
+ }
657
+
658
+ return finalRes;
659
+ }
660
+
661
+ public listen(port?: number, callback?: () => void): any {
662
+ const listenPort = port || this.options.port || 3000;
663
+
664
+ // If running in Bun
665
+ if (typeof (globalThis as any).Bun !== 'undefined') {
666
+ const server = (globalThis as any).Bun.serve({
667
+ port: listenPort,
668
+ fetch: (req: Request) => this.handle(req),
669
+ });
670
+ callback?.();
671
+ return server;
672
+ }
673
+
674
+ // If running in Node.js
675
+ const http = require('node:http');
676
+ const { createServer } = http;
677
+
678
+ const server = createServer(async (req: any, res: any) => {
679
+ try {
680
+ const protocol = req.socket?.encrypted ? 'https' : 'http';
681
+ const fullUrl = `${protocol}://${req.headers.host || 'localhost'}${req.url}`;
682
+
683
+ // Buffer incoming body for Node.js
684
+ const chunks: Uint8Array[] = [];
685
+ for await (const chunk of req) {
686
+ chunks.push(chunk);
687
+ }
688
+ const bodyBuffer = chunks.length > 0 ? Buffer.concat(chunks) : undefined;
689
+
690
+ const webReq = new Request(fullUrl, {
691
+ method: req.method,
692
+ headers: req.headers,
693
+ body: bodyBuffer,
694
+ // @ts-ignore
695
+ duplex: 'half',
696
+ });
697
+
698
+ const webRes = await this.handle(webReq);
699
+
700
+ res.statusCode = webRes.status;
701
+ webRes.headers.forEach((value, key) => {
702
+ res.setHeader(key, value);
703
+ });
704
+
705
+ const responseArrayBuffer = await webRes.arrayBuffer();
706
+ res.end(Buffer.from(responseArrayBuffer));
707
+ } catch (err: any) {
708
+ res.statusCode = 500;
709
+ res.end(JSON.stringify({ error: err?.message || 'Server Error' }));
710
+ }
711
+ });
712
+
713
+ server.listen(listenPort, () => {
714
+ callback?.();
715
+ });
716
+
717
+ return server;
718
+ }
719
+ }
720
+
721
+ export function createApp(options?: FireAppOptions): FireApp {
722
+ return new FireApp(options);
723
+ }