@tamayrp14/fire-api 0.0.0-stage → 1.0.1

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 (99) hide show
  1. package/README.md +276 -2
  2. package/bin/fire.js +139 -0
  3. package/bin/fire.mjs +670 -0
  4. package/bun.lock +20 -0
  5. package/docs/GETTING_STARTED.md +150 -0
  6. package/docs/LAUNCH_KIT.md +236 -0
  7. package/docs/superpowers/plans/2026-11-14-fire-api-framework.md +144 -0
  8. package/docs/superpowers/specs/fire-api-design-spec.md +111 -0
  9. package/examples/basic-api/index.ts +177 -0
  10. package/package.json +68 -4
  11. package/src/ai/mcp.ts +140 -0
  12. package/src/auth/auth.ts +110 -0
  13. package/src/auth/crypto.ts +70 -0
  14. package/src/auth/jwt.ts +105 -0
  15. package/src/cache/cache.ts +72 -0
  16. package/src/cli/bench.ts +118 -0
  17. package/src/cli/compile.ts +29 -0
  18. package/src/cli/create-app.ts +284 -0
  19. package/src/cli/docker.ts +51 -0
  20. package/src/cli/generator.ts +75 -0
  21. package/src/cli/routes.ts +76 -0
  22. package/src/client.ts +120 -0
  23. package/src/compat/express.ts +139 -0
  24. package/src/core/app.ts +723 -0
  25. package/src/core/context.ts +218 -0
  26. package/src/core/error.ts +42 -0
  27. package/src/core/router.ts +262 -0
  28. package/src/db/db-interface.ts +9 -0
  29. package/src/db/hub.ts +256 -0
  30. package/src/db/memory-adapter.ts +202 -0
  31. package/src/db/migrator.ts +100 -0
  32. package/src/db/sqlite-adapter.ts +58 -0
  33. package/src/docs/collection.ts +57 -0
  34. package/src/docs/scalar-ui.ts +25 -0
  35. package/src/events/event-bus.ts +61 -0
  36. package/src/index.ts +37 -0
  37. package/src/jobs/cron.ts +77 -0
  38. package/src/jobs/queue.ts +108 -0
  39. package/src/logging/logger.ts +66 -0
  40. package/src/metrics/metrics.ts +66 -0
  41. package/src/modules/product/product.route.ts +36 -0
  42. package/src/modules/product/product.schema.ts +10 -0
  43. package/src/schema/mock.ts +60 -0
  44. package/src/schema/openapi.ts +101 -0
  45. package/src/schema/schema.ts +519 -0
  46. package/src/schema/validator.ts +5 -0
  47. package/src/shield/compress.ts +61 -0
  48. package/src/shield/cors.ts +94 -0
  49. package/src/shield/headers.ts +55 -0
  50. package/src/shield/idempotency.ts +76 -0
  51. package/src/shield/rate-limiter.ts +81 -0
  52. package/src/shield/sanitizer.ts +23 -0
  53. package/src/shield/webhook.ts +101 -0
  54. package/src/static/serve.ts +51 -0
  55. package/src/storage/storage.ts +176 -0
  56. package/src/stream/sse.ts +89 -0
  57. package/src/studio/studio.ts +221 -0
  58. package/src/types.ts +241 -0
  59. package/src/ws/ws.ts +118 -0
  60. package/test/ai-mcp.test.ts +61 -0
  61. package/test/auth.test.ts +108 -0
  62. package/test/bench.test.ts +31 -0
  63. package/test/cache.test.ts +36 -0
  64. package/test/cli-create.test.ts +64 -0
  65. package/test/client.test.ts +54 -0
  66. package/test/collection-export.test.ts +52 -0
  67. package/test/compat.test.ts +48 -0
  68. package/test/compress.test.ts +48 -0
  69. package/test/context.test.ts +48 -0
  70. package/test/db.test.ts +62 -0
  71. package/test/docker.test.ts +28 -0
  72. package/test/events.test.ts +62 -0
  73. package/test/file-upload.test.ts +116 -0
  74. package/test/hooks.test.ts +77 -0
  75. package/test/idempotency.test.ts +65 -0
  76. package/test/integration.test.ts +111 -0
  77. package/test/jobs.test.ts +60 -0
  78. package/test/metrics.test.ts +40 -0
  79. package/test/mock.test.ts +33 -0
  80. package/test/multi-tenant.test.ts +80 -0
  81. package/test/observability.test.ts +95 -0
  82. package/test/openapi.test.ts +41 -0
  83. package/test/orm-relations.test.ts +85 -0
  84. package/test/orm-shortcuts.test.ts +52 -0
  85. package/test/rbac.test.ts +85 -0
  86. package/test/router-fastpath.test.ts +42 -0
  87. package/test/router.test.ts +84 -0
  88. package/test/schema.test.ts +49 -0
  89. package/test/seeder-migrator.test.ts +78 -0
  90. package/test/shield.test.ts +55 -0
  91. package/test/shorthand-dx.test.ts +31 -0
  92. package/test/sse.test.ts +43 -0
  93. package/test/static-serve.test.ts +53 -0
  94. package/test/storage.test.ts +31 -0
  95. package/test/studio.test.ts +43 -0
  96. package/test/tx.test.ts +92 -0
  97. package/test/webhook.test.ts +113 -0
  98. package/test/ws.test.ts +54 -0
  99. package/tsconfig.json +19 -0
@@ -0,0 +1,221 @@
1
+ import { FireApp } from '../core/app';
2
+ import { createRouter } from '../core/router';
3
+
4
+ export interface StudioOptions {
5
+ path?: string;
6
+ title?: string;
7
+ }
8
+
9
+ export function renderStudioHTML(basePath = '/studio', title = 'FireStudio Dashboard'): string {
10
+ return `<!DOCTYPE html>
11
+ <html lang="en">
12
+ <head>
13
+ <meta charset="utf-8" />
14
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
15
+ <title>${title}</title>
16
+ <style>
17
+ :root {
18
+ --bg: #0f172a;
19
+ --card: #1e293b;
20
+ --border: #334155;
21
+ --primary: #f97316;
22
+ --primary-hover: #ea580c;
23
+ --text: #f8fafc;
24
+ --text-muted: #94a3b8;
25
+ --accent: #38bdf8;
26
+ }
27
+ * { box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
28
+ body { background: var(--bg); color: var(--text); padding: 24px; }
29
+ header { display: flex; align-items: center; justify-content: space-between; padding-bottom: 20px; border-bottom: 1px solid var(--border); margin-bottom: 24px; }
30
+ .logo { font-size: 22px; font-weight: 800; display: flex; align-items: center; gap: 8px; color: var(--primary); }
31
+ .badge { background: #3b82f620; color: #60a5fa; font-size: 12px; padding: 4px 10px; border-radius: 999px; border: 1px solid #3b82f640; }
32
+ nav { display: flex; gap: 12px; margin-bottom: 24px; }
33
+ button.tab-btn { background: var(--card); border: 1px solid var(--border); color: var(--text-muted); padding: 8px 18px; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 600; transition: all 0.2s; }
34
+ button.tab-btn.active, button.tab-btn:hover { background: var(--primary); color: white; border-color: var(--primary); }
35
+ .card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 20px; margin-bottom: 24px; }
36
+ h2 { font-size: 18px; margin-bottom: 16px; color: var(--text); }
37
+ table { width: 100%; border-collapse: collapse; margin-top: 12px; }
38
+ th, td { text-align: left; padding: 12px; border-bottom: 1px solid var(--border); font-size: 14px; }
39
+ th { color: var(--text-muted); font-weight: 600; text-transform: uppercase; font-size: 12px; }
40
+ tr:hover { background: rgba(255, 255, 255, 0.02); }
41
+ pre { background: #090d16; padding: 12px; border-radius: 8px; overflow-x: auto; font-size: 13px; color: #a5f3fc; }
42
+ .table-pills { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
43
+ .table-pill { background: #334155; padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 13px; border: 1px solid transparent; }
44
+ .table-pill.active { background: var(--accent); color: #0f172a; font-weight: 700; }
45
+ </style>
46
+ </head>
47
+ <body>
48
+ <header>
49
+ <div class="logo">🔥 FireStudio <span>Engine</span></div>
50
+ <div class="badge">Live Connected</div>
51
+ </header>
52
+
53
+ <nav>
54
+ <button class="tab-btn active" onclick="switchTab('db')">🗄️ Database Explorer</button>
55
+ <button class="tab-btn" onclick="switchTab('routes')">⚡ Routes & Endpoints</button>
56
+ </nav>
57
+
58
+ <div id="tab-db" class="card">
59
+ <h2>Database Tables</h2>
60
+ <div id="tables-list" class="table-pills">Loading tables...</div>
61
+ <div id="table-content">
62
+ <p style="color: var(--text-muted)">Select a table above to view rows.</p>
63
+ </div>
64
+ </div>
65
+
66
+ <div id="tab-routes" class="card" style="display: none;">
67
+ <h2>Registered Routes</h2>
68
+ <div id="routes-content">Loading route definitions...</div>
69
+ </div>
70
+
71
+ <script>
72
+ const basePath = '${basePath}';
73
+
74
+ function switchTab(tab) {
75
+ document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
76
+ event.target.classList.add('active');
77
+ document.getElementById('tab-db').style.display = tab === 'db' ? 'block' : 'none';
78
+ document.getElementById('tab-routes').style.display = tab === 'routes' ? 'block' : 'none';
79
+ if (tab === 'routes') loadRoutes();
80
+ }
81
+
82
+ async function loadOverview() {
83
+ try {
84
+ const res = await fetch(basePath + '/api/overview');
85
+ const data = await res.json();
86
+ const pillsContainer = document.getElementById('tables-list');
87
+ pillsContainer.innerHTML = '';
88
+ if (data.tables.length === 0) {
89
+ pillsContainer.innerHTML = '<span style="color:var(--text-muted)">No tables found in database.</span>';
90
+ return;
91
+ }
92
+ data.tables.forEach((t, i) => {
93
+ const pill = document.createElement('div');
94
+ pill.className = 'table-pill ' + (i === 0 ? 'active' : '');
95
+ pill.innerText = t;
96
+ pill.onclick = () => {
97
+ document.querySelectorAll('.table-pill').forEach(p => p.classList.remove('active'));
98
+ pill.classList.add('active');
99
+ loadTableRows(t);
100
+ };
101
+ pillsContainer.appendChild(pill);
102
+ });
103
+ if (data.tables[0]) loadTableRows(data.tables[0]);
104
+ } catch (e) {
105
+ document.getElementById('tables-list').innerHTML = 'Error loading tables: ' + e.message;
106
+ }
107
+ }
108
+
109
+ async function loadTableRows(tableName) {
110
+ const container = document.getElementById('table-content');
111
+ container.innerHTML = '<p style="color: var(--text-muted)">Loading rows for <b>' + tableName + '</b>...</p>';
112
+ try {
113
+ const res = await fetch(basePath + '/api/tables/' + encodeURIComponent(tableName));
114
+ const rows = await res.json();
115
+ if (rows.length === 0) {
116
+ container.innerHTML = '<p style="color: var(--text-muted)">Table <b>' + tableName + '</b> is currently empty.</p>';
117
+ return;
118
+ }
119
+ const keys = Object.keys(rows[0]);
120
+ let html = '<table><thead><tr>' + keys.map(k => '<th>' + k + '</th>').join('') + '</tr></thead><tbody>';
121
+ rows.forEach(r => {
122
+ html += '<tr>' + keys.map(k => '<td>' + (typeof r[k] === 'object' ? JSON.stringify(r[k]) : r[k]) + '</td>').join('') + '</tr>';
123
+ });
124
+ html += '</tbody></table>';
125
+ container.innerHTML = html;
126
+ } catch (e) {
127
+ container.innerHTML = 'Error loading rows: ' + e.message;
128
+ }
129
+ }
130
+
131
+ async function loadRoutes() {
132
+ const container = document.getElementById('routes-content');
133
+ try {
134
+ const res = await fetch(basePath + '/api/overview');
135
+ const data = await res.json();
136
+ let html = '<table><thead><tr><th>Method</th><th>Path</th><th>Security</th></tr></thead><tbody>';
137
+ data.routes.forEach(r => {
138
+ html += '<tr><td><b style="color:var(--primary)">' + r.method + '</b></td><td>' + r.path + '</td><td>' + (r.security || 'None') + '</td></tr>';
139
+ });
140
+ html += '</tbody></table>';
141
+ container.innerHTML = html;
142
+ } catch (e) {
143
+ container.innerHTML = 'Error loading routes: ' + e.message;
144
+ }
145
+ }
146
+
147
+ loadOverview();
148
+ </script>
149
+ </body>
150
+ </html>`;
151
+ }
152
+
153
+ export function enableStudio(app: FireApp, options: StudioOptions = {}): void {
154
+ const basePath = options.path || '/studio';
155
+ const cleanBase = basePath.replace(/\/+$/, '');
156
+ const studioRouter = createRouter(cleanBase);
157
+
158
+ // 1. Studio Dashboard Web UI
159
+ studioRouter.get('/', {
160
+ handler: (ctx) => {
161
+ const html = renderStudioHTML(cleanBase, options.title);
162
+ return ctx.html(html);
163
+ },
164
+ });
165
+
166
+ // 2. Overview JSON API
167
+ studioRouter.get('/api/overview', {
168
+ handler: async (ctx) => {
169
+ // Introspect tables if in-memory database
170
+ let tables: string[] = [];
171
+ const dbStorage = (ctx.db as any).storage;
172
+ if (dbStorage instanceof Map) {
173
+ tables = Array.from(dbStorage.keys());
174
+ } else {
175
+ tables = ['users', 'products', 'logs'];
176
+ }
177
+
178
+ const routes = app.router.routesList.map((r) => ({
179
+ method: r.method,
180
+ path: r.path,
181
+ security: r.options.security ? JSON.stringify(r.options.security) : undefined,
182
+ }));
183
+
184
+ return ctx.ok({
185
+ status: 'online',
186
+ routesCount: routes.length,
187
+ tables,
188
+ routes,
189
+ });
190
+ },
191
+ });
192
+
193
+ // 3. Table Rows API
194
+ studioRouter.get('/api/tables/:name', {
195
+ handler: async (ctx) => {
196
+ const tableName = ctx.params.name;
197
+ const rows = await ctx.db.table(tableName).find();
198
+ return ctx.ok(rows);
199
+ },
200
+ });
201
+
202
+ // 4. Insert Row API
203
+ studioRouter.post('/api/tables/:name', {
204
+ handler: async (ctx) => {
205
+ const tableName = ctx.params.name;
206
+ const inserted = await ctx.db.table(tableName).insert(ctx.body || {});
207
+ return ctx.created(inserted);
208
+ },
209
+ });
210
+
211
+ // 5. Delete Row API
212
+ studioRouter.delete('/api/tables/:name/:id', {
213
+ handler: async (ctx) => {
214
+ const { name, id } = ctx.params;
215
+ const result = await ctx.db.table(name).where('id', id).delete();
216
+ return ctx.ok(result);
217
+ },
218
+ });
219
+
220
+ app.use(studioRouter);
221
+ }
package/src/types.ts ADDED
@@ -0,0 +1,241 @@
1
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD';
2
+
3
+ export interface TableModel<T = any> extends Omit<QueryBuilder, 'find' | 'findOne' | 'findById' | 'insert' | 'update' | 'delete' | 'include'> {
4
+ find(filter?: Record<string, any>): Promise<T[]>;
5
+ findOne(filter?: Record<string, any>): Promise<T | null>;
6
+ findById(id: string | number): Promise<T | null>;
7
+ insert(data: Record<string, any> | Record<string, any>[]): Promise<T>;
8
+ update(idOrFilter: string | number | Record<string, any>, data?: Record<string, any>): Promise<T>;
9
+ delete(idOrFilter?: string | number | Record<string, any>): Promise<{ count: number }>;
10
+ include(relation: string | string[]): TableModel<T>;
11
+ }
12
+
13
+ export interface FireDatabase {
14
+ table(name: string): QueryBuilder;
15
+ raw<T = any>(sql: string, params?: any[]): Promise<T>;
16
+ transaction<T>(fn: (trx: FireDatabase) => Promise<T>): Promise<T>;
17
+ tx<T>(fn: (trx: FireDatabase) => Promise<T>): Promise<T>;
18
+ seed?(tableName: string, schema: any, count?: number): Promise<any[]>;
19
+ [table: string]: any;
20
+ }
21
+
22
+ export interface QueryBuilder {
23
+ find<T = any>(): Promise<T[]>;
24
+ findOne<T = any>(): Promise<T | null>;
25
+ findById<T = any>(id: string | number): Promise<T | null>;
26
+ where(field: string, operatorOrValue: any, value?: any): QueryBuilder;
27
+ limit(count: number): QueryBuilder;
28
+ offset(count: number): QueryBuilder;
29
+ orderBy(field: string, direction?: 'asc' | 'desc'): QueryBuilder;
30
+ include(relation: string | string[]): QueryBuilder;
31
+ insert<T = any>(data: Record<string, any> | Record<string, any>[]): Promise<T>;
32
+ update<T = any>(data: Record<string, any>): Promise<T>;
33
+ delete(): Promise<{ count: number }>;
34
+ }
35
+
36
+ export interface FireCacheManager {
37
+ get<T = any>(key: string): Promise<T | null>;
38
+ set(key: string, value: any, ttlSeconds?: number, tags?: string[]): Promise<void>;
39
+ invalidate(tag: string): Promise<void>;
40
+ clear(): Promise<void>;
41
+ }
42
+
43
+ export interface FireQueueJob<T = any> {
44
+ id: string;
45
+ name: string;
46
+ data: T;
47
+ attempts: number;
48
+ maxAttempts: number;
49
+ createdAt: number;
50
+ status: 'pending' | 'processing' | 'completed' | 'failed';
51
+ }
52
+
53
+ export interface FireQueue {
54
+ dispatch<T = any>(name: string, data: T, options?: { maxAttempts?: number; delayMs?: number; retryDelayMs?: number }): Promise<FireQueueJob<T>>;
55
+ handle<T = any>(name: string, worker: (data: T, job: FireQueueJob<T>) => Promise<void> | void): void;
56
+ }
57
+
58
+ export interface FireStorage {
59
+ put(filePath: string, content: string | Uint8Array | ArrayBuffer, options?: { contentType?: string; access?: 'public' | 'private' }): Promise<{ path: string; url: string; size: number }>;
60
+ get(filePath: string): Promise<Uint8Array | null>;
61
+ getText(filePath: string): Promise<string | null>;
62
+ delete(filePath: string): Promise<boolean>;
63
+ exists(filePath: string): Promise<boolean>;
64
+ url(filePath: string): string;
65
+ }
66
+
67
+ export interface UploadedFile {
68
+ name: string;
69
+ size: number;
70
+ type: string;
71
+ bytes(): Promise<Uint8Array>;
72
+ text(): Promise<string>;
73
+ store(destinationDir?: string, options?: { fileName?: string }): Promise<{ path: string; url: string; size: number }>;
74
+ }
75
+
76
+ export interface FireContext<
77
+ TParams = Record<string, string>,
78
+ TQuery = Record<string, string | string[]>,
79
+ TBody = any,
80
+ TUser = any
81
+ > {
82
+ req: Request;
83
+ params: TParams;
84
+ query: TQuery;
85
+ body: TBody;
86
+ files: Record<string, UploadedFile>;
87
+ user: TUser;
88
+ db: FireDatabase;
89
+ cache: FireCacheManager;
90
+ queue: FireQueue;
91
+ storage: FireStorage;
92
+ state: Record<string, any>;
93
+ rawBody: string;
94
+ requestId: string;
95
+ traceId?: string;
96
+ log: any;
97
+ tenant?: { id: string; [key: string]: any };
98
+
99
+ // Event Emitter
100
+ emit<T = any>(event: string, payload?: T): Promise<void>;
101
+
102
+ // Turnkey Webhook Verifier
103
+ webhook: {
104
+ verifyHMAC(options: {
105
+ secret: string;
106
+ header?: string;
107
+ signature?: string;
108
+ algorithm?: 'SHA-256' | 'SHA-1' | 'SHA-512';
109
+ }): Promise<boolean>;
110
+ };
111
+
112
+ // Streaming helpers
113
+ stream: {
114
+ sse(handler: (stream: { send(data: any, options?: { event?: string; id?: string; retry?: number }): Promise<void>; close(): void }) => Promise<void> | void): Response;
115
+ };
116
+
117
+ // Modern HTTP helpers
118
+ earlyHints(links: string[]): void;
119
+
120
+ // Response helpers
121
+ json<T = any>(data: T, status?: number, headers?: HeadersInit): Response;
122
+ ok<T = any>(data: T): Response;
123
+ created<T = any>(data: T): Response;
124
+ noContent(): Response;
125
+ badRequest(errors: any): Response;
126
+ unauthorized(message?: string): Response;
127
+ forbidden(message?: string): Response;
128
+ notFound(message?: string): Response;
129
+ internalError(message?: string): Response;
130
+ text(content: string, status?: number, headers?: HeadersInit): Response;
131
+ html(content: string, status?: number, headers?: HeadersInit): Response;
132
+ }
133
+
134
+ export type Handler<
135
+ TParams = any,
136
+ TQuery = any,
137
+ TBody = any,
138
+ TRes = any
139
+ > = (ctx: FireContext<TParams, TQuery, TBody>) => Promise<Response | TRes> | Response | TRes;
140
+
141
+ export type Middleware = (
142
+ ctx: FireContext,
143
+ next: () => Promise<Response>
144
+ ) => Promise<Response>;
145
+
146
+ export interface RouteSchema {
147
+ params?: any;
148
+ query?: any;
149
+ body?: any;
150
+ response?: any;
151
+ }
152
+
153
+ export interface RouteAIMetadata {
154
+ description: string;
155
+ operationId?: string;
156
+ tags?: string[];
157
+ }
158
+
159
+ export interface RouteSecurityConfig {
160
+ auth?: 'bearer' | 'apiKey' | 'basic' | boolean;
161
+ rateLimit?: string | { max: number; window: string };
162
+ roles?: string[];
163
+ }
164
+
165
+ export interface RouteOptions<TParams = any, TQuery = any, TBody = any, TRes = any> {
166
+ schema?: RouteSchema;
167
+ security?: (string | RouteSecurityConfig)[];
168
+ cache?: { ttl: string | number; swr?: string | number; tags?: string[] };
169
+ idempotent?: boolean | { ttl?: number };
170
+ ai?: RouteAIMetadata;
171
+ roles?: string[];
172
+ can?: (user: any, ctx: FireContext) => boolean | Promise<boolean>;
173
+ middlewares?: Middleware[];
174
+ handler: Handler<TParams, TQuery, TBody, TRes>;
175
+ }
176
+
177
+ export interface RouteMatch {
178
+ handler: Handler;
179
+ routeOptions: RouteOptions;
180
+ params: Record<string, string>;
181
+ middlewares: Middleware[];
182
+ }
183
+
184
+ export interface FireSecurityOptions {
185
+ enabled?: boolean;
186
+ cors?: boolean | {
187
+ origin?: string | string[] | ((origin: string) => boolean);
188
+ methods?: string[];
189
+ allowedHeaders?: string[];
190
+ credentials?: boolean;
191
+ maxAge?: number;
192
+ };
193
+ rateLimit?: {
194
+ max: number;
195
+ window: string; // e.g. '1m', '15m', '1h'
196
+ };
197
+ headers?: boolean | {
198
+ contentSecurityPolicy?: boolean | string;
199
+ frameOptions?: 'DENY' | 'SAMEORIGIN' | string;
200
+ nosniff?: boolean;
201
+ hsts?: boolean | { maxAge: number; includeSubDomains?: boolean };
202
+ };
203
+ prototypePollutionGuard?: boolean;
204
+ maxBodySize?: number; // In bytes (default 1MB)
205
+ }
206
+
207
+ export interface FireDatabaseOptions {
208
+ driver?: 'memory' | 'sqlite' | 'postgres' | 'mysql';
209
+ url?: string;
210
+ autoMock?: boolean;
211
+ }
212
+
213
+ export interface FireCacheOptions {
214
+ enabled?: boolean;
215
+ defaultTTL?: number | string;
216
+ }
217
+
218
+ export type HookType = 'onRequest' | 'beforeHandle' | 'afterHandle' | 'onError';
219
+
220
+ export interface FireHealthOptions {
221
+ livenessPath?: string; // default '/healthz'
222
+ readinessPath?: string; // default '/readyz'
223
+ checkDatabase?: boolean;
224
+ checkStorage?: boolean;
225
+ customChecks?: Array<() => Promise<{ name: string; status: 'ok' | 'degraded' | 'down'; message?: string }>>;
226
+ }
227
+
228
+ export interface FireAppOptions {
229
+ port?: number;
230
+ security?: FireSecurityOptions;
231
+ database?: FireDatabaseOptions;
232
+ cache?: FireCacheOptions;
233
+ storage?: { driver?: 'memory' | 'local'; rootDir?: string; baseUrl?: string };
234
+ compression?: boolean | { threshold?: number };
235
+ docs?: string | false; // default '/docs'
236
+ openapiPath?: string | false; // default '/openapi.json'
237
+ metricsPath?: string | false; // default '/metrics'
238
+ health?: boolean | FireHealthOptions;
239
+ multiTenant?: { header?: string; query?: string; autoScopeDB?: boolean };
240
+ prefix?: string;
241
+ }
package/src/ws/ws.ts ADDED
@@ -0,0 +1,118 @@
1
+ import { FireContext } from '../types';
2
+
3
+ export interface FireWebSocket {
4
+ id: string;
5
+ data: Record<string, any>;
6
+ send(message: any): void;
7
+ close(code?: number, reason?: string): void;
8
+ join(room: string): void;
9
+ leave(room: string): void;
10
+ publish(room: string, message: any, excludeSelf?: boolean): void;
11
+ broadcast(message: any, excludeSelf?: boolean): void;
12
+ }
13
+
14
+ export interface FireWebSocketHandler {
15
+ open?(ws: FireWebSocket, ctx: FireContext): void | Promise<void>;
16
+ message?(ws: FireWebSocket, message: any, ctx: FireContext): void | Promise<void>;
17
+ close?(ws: FireWebSocket, code: number, reason: string, ctx: FireContext): void | Promise<void>;
18
+ }
19
+
20
+ export class MockWebSocket {
21
+ public receivedMessages: string[] = [];
22
+ public isClosed = false;
23
+
24
+ constructor(public id: string) {}
25
+
26
+ public send(msg: string): void {
27
+ if (!this.isClosed) {
28
+ this.receivedMessages.push(msg);
29
+ }
30
+ }
31
+
32
+ public close(): void {
33
+ this.isClosed = true;
34
+ }
35
+ }
36
+
37
+ export class WebSocketHub {
38
+ private clients: Map<string, FireWebSocket> = new Map();
39
+ private rooms: Map<string, Set<string>> = new Map(); // room -> Set of client IDs
40
+
41
+ public register(rawSocket: any, id?: string): FireWebSocket {
42
+ const socketId = id || rawSocket.id || `ws_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
43
+ const joinedRooms: Set<string> = new Set();
44
+ const data: Record<string, any> = {};
45
+
46
+ const fireWs: FireWebSocket = {
47
+ id: socketId,
48
+ data,
49
+
50
+ send(message: any) {
51
+ const payload = typeof message === 'string' ? message : JSON.stringify(message);
52
+ rawSocket.send(payload);
53
+ },
54
+
55
+ close(code = 1000, reason = '') {
56
+ rawSocket.close(code, reason);
57
+ },
58
+
59
+ join: (room: string) => {
60
+ joinedRooms.add(room);
61
+ if (!this.rooms.has(room)) {
62
+ this.rooms.set(room, new Set());
63
+ }
64
+ this.rooms.get(room)!.add(socketId);
65
+ },
66
+
67
+ leave: (room: string) => {
68
+ joinedRooms.delete(room);
69
+ const roomClients = this.rooms.get(room);
70
+ if (roomClients) {
71
+ roomClients.delete(socketId);
72
+ if (roomClients.size === 0) {
73
+ this.rooms.delete(room);
74
+ }
75
+ }
76
+ },
77
+
78
+ publish: (room: string, message: any, excludeSelf = true) => {
79
+ const targetIds = this.rooms.get(room);
80
+ if (!targetIds) return;
81
+
82
+ const payload = typeof message === 'string' ? message : JSON.stringify(message);
83
+ for (const targetId of targetIds) {
84
+ if (excludeSelf && targetId === socketId) continue;
85
+ const targetClient = this.clients.get(targetId);
86
+ if (targetClient) {
87
+ targetClient.send(payload);
88
+ }
89
+ }
90
+ },
91
+
92
+ broadcast: (message: any, excludeSelf = false) => {
93
+ const payload = typeof message === 'string' ? message : JSON.stringify(message);
94
+ for (const [targetId, targetClient] of this.clients.entries()) {
95
+ if (excludeSelf && targetId === socketId) continue;
96
+ targetClient.send(payload);
97
+ }
98
+ },
99
+ };
100
+
101
+ this.clients.set(socketId, fireWs);
102
+ return fireWs;
103
+ }
104
+
105
+ public unregister(socketId: string): void {
106
+ this.clients.delete(socketId);
107
+ for (const [room, clientSet] of this.rooms.entries()) {
108
+ clientSet.delete(socketId);
109
+ if (clientSet.size === 0) {
110
+ this.rooms.delete(room);
111
+ }
112
+ }
113
+ }
114
+
115
+ public getClient(id: string): FireWebSocket | undefined {
116
+ return this.clients.get(id);
117
+ }
118
+ }
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { Router } from '../src/core/router';
3
+ import { s } from '../src/schema/schema';
4
+ import { createMCPServer } from '../src/ai/mcp';
5
+
6
+ describe('FireAPI FireAI & Model Context Protocol (MCP) Adapter', () => {
7
+ it('discovers all routes flagged with ai metadata and builds MCP tool manifests', () => {
8
+ const router = new Router();
9
+
10
+ router.get('/users/:id', {
11
+ ai: { description: 'Fetch user account profile by unique ID' },
12
+ schema: {
13
+ params: s.object({ id: s.string().min(1) }),
14
+ },
15
+ handler: (ctx) => ctx.ok({ id: ctx.params.id, name: 'Alice' }),
16
+ });
17
+
18
+ router.post('/users', {
19
+ ai: { description: 'Create a new user account' },
20
+ schema: {
21
+ body: s.object({
22
+ name: s.string(),
23
+ email: s.string().email(),
24
+ }),
25
+ },
26
+ handler: (ctx) => ctx.created({ id: 'new_user', ...ctx.body }),
27
+ });
28
+
29
+ const mcp = createMCPServer(router, { name: 'UserServer', version: '1.0.0' });
30
+ const tools = mcp.listTools();
31
+
32
+ expect(tools.length).toBe(2);
33
+ expect(tools[0].name).toBe('get_users_id');
34
+ expect(tools[0].description).toBe('Fetch user account profile by unique ID');
35
+ expect(tools[0].inputSchema.type).toBe('object');
36
+ expect(tools[1].name).toBe('post_users');
37
+ expect(tools[1].inputSchema.properties.name).toBeDefined();
38
+ });
39
+
40
+ it('executes tool calls via callTool and invokes the route handler cleanly', async () => {
41
+ const router = new Router();
42
+ router.post('/calc/add', {
43
+ ai: { description: 'Add two numbers' },
44
+ schema: {
45
+ body: s.object({
46
+ a: s.number(),
47
+ b: s.number(),
48
+ }),
49
+ },
50
+ handler: (ctx) => ctx.ok({ result: ctx.body.a + ctx.body.b }),
51
+ });
52
+
53
+ const mcp = createMCPServer(router);
54
+ const result = await mcp.callTool('post_calc_add', { a: 15, b: 27 });
55
+
56
+ expect(result.isError).toBe(false);
57
+ expect(result.content[0].type).toBe('text');
58
+ const parsed = JSON.parse(result.content[0].text);
59
+ expect(parsed.result).toBe(42);
60
+ });
61
+ });