@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,53 @@
1
+ import { describe, expect, it, afterAll } from 'bun:test';
2
+ import { createApp } from '../src/index';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+
6
+ describe('FireAPI Static Assets & SPA Serving (app.serveStatic, app.serveSPA)', () => {
7
+ const tempDir = path.resolve('./temp_static_test');
8
+
9
+ if (!fs.existsSync(tempDir)) {
10
+ fs.mkdirSync(tempDir, { recursive: true });
11
+ fs.writeFileSync(path.join(tempDir, 'style.css'), 'body { background: #000; }');
12
+ fs.writeFileSync(path.join(tempDir, 'app.js'), 'console.log("hello");');
13
+ fs.writeFileSync(path.join(tempDir, 'index.html'), '<html><body>SPA Root</body></html>');
14
+ }
15
+
16
+ afterAll(() => {
17
+ if (fs.existsSync(tempDir)) {
18
+ fs.rmSync(tempDir, { recursive: true, force: true });
19
+ }
20
+ });
21
+
22
+ it('serves static files with correct MIME types and headers', async () => {
23
+ const app = createApp();
24
+ app.serveStatic('/assets', tempDir);
25
+
26
+ // 1. Fetch style.css
27
+ const resCss = await app.handle(new Request('http://localhost:3000/assets/style.css'));
28
+ expect(resCss.status).toBe(200);
29
+ expect(resCss.headers.get('Content-Type')).toContain('text/css');
30
+ const textCss = await resCss.text();
31
+ expect(textCss).toBe('body { background: #000; }');
32
+
33
+ // 2. Fetch app.js
34
+ const resJs = await app.handle(new Request('http://localhost:3000/assets/app.js'));
35
+ expect(resJs.status).toBe(200);
36
+ expect(resJs.headers.get('Content-Type')).toContain('application/javascript');
37
+
38
+ // 3. 404 for non-existent file
39
+ const res404 = await app.handle(new Request('http://localhost:3000/assets/missing.png'));
40
+ expect(res404.status).toBe(404);
41
+ });
42
+
43
+ it('serves SPA fallback HTML for client-side routing', async () => {
44
+ const app = createApp();
45
+ app.serveSPA('/app', path.join(tempDir, 'index.html'));
46
+
47
+ const res = await app.handle(new Request('http://localhost:3000/app'));
48
+ expect(res.status).toBe(200);
49
+ expect(res.headers.get('Content-Type')).toContain('text/html');
50
+ const textHtml = await res.text();
51
+ expect(textHtml).toContain('SPA Root');
52
+ });
53
+ });
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { MemoryStorageAdapter } from '../src/storage/storage';
3
+
4
+ describe('FireAPI Universal Object Storage Hub (fire-api/storage)', () => {
5
+ it('stores, retrieves, and checks existence of files in storage', async () => {
6
+ const storage = new MemoryStorageAdapter({ baseUrl: 'http://localhost:3000/uploads' });
7
+
8
+ const content = 'User avatar raw binary or string data';
9
+ const result = await storage.put('avatars/usr_1.png', content, { contentType: 'image/png' });
10
+
11
+ expect(result.path).toBe('avatars/usr_1.png');
12
+ expect(result.url).toBe('http://localhost:3000/uploads/avatars/usr_1.png');
13
+ expect(result.size).toBe(content.length);
14
+
15
+ expect(await storage.exists('avatars/usr_1.png')).toBe(true);
16
+ expect(await storage.exists('avatars/non_existent.png')).toBe(false);
17
+
18
+ const retrievedText = await storage.getText('avatars/usr_1.png');
19
+ expect(retrievedText).toBe(content);
20
+ });
21
+
22
+ it('deletes files from storage cleanly', async () => {
23
+ const storage = new MemoryStorageAdapter();
24
+ await storage.put('temp.txt', 'Delete me');
25
+
26
+ expect(await storage.exists('temp.txt')).toBe(true);
27
+ const deleted = await storage.delete('temp.txt');
28
+ expect(deleted).toBe(true);
29
+ expect(await storage.exists('temp.txt')).toBe(false);
30
+ });
31
+ });
@@ -0,0 +1,43 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { createApp, createRouter } from '../src/index';
3
+ import { enableStudio } from '../src/studio/studio';
4
+
5
+ describe('FireAPI FireStudio Embedded Dev Dashboard (fire-api/studio)', () => {
6
+ it('serves the Studio dashboard HTML and overview data', async () => {
7
+ const app = createApp({ database: { driver: 'memory' } });
8
+
9
+ // Seed some data
10
+ await app.db.table('users').insert({ name: 'Alice', role: 'admin' });
11
+ await app.db.table('posts').insert({ title: 'Welcome to FireAPI' });
12
+
13
+ const router = createRouter('/api');
14
+ router.get('/health', { handler: (ctx) => ctx.ok({ status: 'ok' }) });
15
+ app.use(router);
16
+
17
+ // Enable studio
18
+ enableStudio(app, { path: '/studio' });
19
+
20
+ // 1. Test HTML Dashboard
21
+ const htmlRes = await app.handle(new Request('http://localhost:3000/studio'));
22
+ expect(htmlRes.status).toBe(200);
23
+ expect(htmlRes.headers.get('content-type')).toContain('text/html');
24
+ const html = await htmlRes.text();
25
+ expect(html).toContain('FireStudio');
26
+ expect(html).toContain('Database Explorer');
27
+
28
+ // 2. Test Overview API
29
+ const overviewRes = await app.handle(new Request('http://localhost:3000/studio/api/overview'));
30
+ expect(overviewRes.status).toBe(200);
31
+ const overview = await overviewRes.json();
32
+ expect(overview.tables).toContain('users');
33
+ expect(overview.tables).toContain('posts');
34
+ expect(overview.routesCount).toBeGreaterThan(0);
35
+
36
+ // 3. Test Table Data API
37
+ const tableRes = await app.handle(new Request('http://localhost:3000/studio/api/tables/users'));
38
+ expect(tableRes.status).toBe(200);
39
+ const users = await tableRes.json();
40
+ expect(users.length).toBe(1);
41
+ expect(users[0].name).toBe('Alice');
42
+ });
43
+ });
@@ -0,0 +1,92 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { createApp, createRouter } from '../src/index';
3
+
4
+ describe('FireAPI Atomic Transaction Scope (ctx.db.tx)', () => {
5
+ it('commits multi-table operations atomically when transaction succeeds', async () => {
6
+ const app = createApp({ database: { driver: 'memory' } });
7
+ const router = createRouter();
8
+
9
+ router.post('/checkout', async (ctx) => {
10
+ const result = await ctx.db.tx(async (trx) => {
11
+ const user = await trx.users.insert({ name: 'Alice', balance: 500 });
12
+ const order = await trx.orders.insert({ userId: user.id, total: 150 });
13
+ const log = await trx.audit_logs.insert({ action: 'CHECKOUT_SUCCESS', orderId: order.id });
14
+ return { user, order, log };
15
+ });
16
+
17
+ return ctx.created(result);
18
+ });
19
+
20
+ app.use(router);
21
+
22
+ const res = await app.handle(
23
+ new Request('http://localhost:3000/checkout', { method: 'POST' })
24
+ );
25
+
26
+ expect(res.status).toBe(201);
27
+
28
+ // Verify all records were committed
29
+ const users = await app.db.users.find();
30
+ const orders = await app.db.orders.find();
31
+ const logs = await app.db.audit_logs.find();
32
+
33
+ expect(users.length).toBe(1);
34
+ expect(orders.length).toBe(1);
35
+ expect(logs.length).toBe(1);
36
+ expect(users[0].name).toBe('Alice');
37
+ expect(orders[0].total).toBe(150);
38
+ });
39
+
40
+ it('automatically rolls back all multi-table changes when transaction callback throws', async () => {
41
+ const app = createApp({ database: { driver: 'memory' } });
42
+ const router = createRouter();
43
+
44
+ // Pre-populate with initial user
45
+ await app.db.users.insert({ id: 'existing_user', name: 'Bob', balance: 1000 });
46
+
47
+ router.post('/transfer-fail', async (ctx) => {
48
+ try {
49
+ await ctx.db.tx(async (trx) => {
50
+ // 1. Deduct balance from existing user
51
+ await trx.users.update('existing_user', { balance: 800 });
52
+
53
+ // 2. Insert new recipient user
54
+ await trx.users.insert({ id: 'new_recipient', name: 'Charlie', balance: 200 });
55
+
56
+ // 3. Insert transaction record
57
+ await trx.transfers.insert({ from: 'existing_user', to: 'new_recipient', amount: 200 });
58
+
59
+ // 4. Simulate a failure (e.g. external network error or validation failure)
60
+ throw new Error('Anti-fraud check triggered: transfer rejected');
61
+ });
62
+ } catch (err: any) {
63
+ return ctx.badRequest({ message: err.message });
64
+ }
65
+
66
+ return ctx.ok({ success: true });
67
+ });
68
+
69
+ app.use(router);
70
+
71
+ const res = await app.handle(
72
+ new Request('http://localhost:3000/transfer-fail', { method: 'POST' })
73
+ );
74
+
75
+ expect(res.status).toBe(400);
76
+ const body = await res.json();
77
+ expect(body.message).toContain('Anti-fraud check triggered');
78
+
79
+ // VERIFY AUTO-ROLLBACK:
80
+ // 1. Existing user balance must still be 1000 (NOT 800)
81
+ const existingUser = await app.db.users.findById('existing_user');
82
+ expect(existingUser.balance).toBe(1000);
83
+
84
+ // 2. New recipient must NOT exist
85
+ const recipient = await app.db.users.findById('new_recipient');
86
+ expect(recipient).toBeNull();
87
+
88
+ // 3. Transfers table must have 0 rows
89
+ const transfers = await app.db.transfers.find();
90
+ expect(transfers.length).toBe(0);
91
+ });
92
+ });
@@ -0,0 +1,113 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { createApp, createRouter } from '../src/index';
3
+ import { computeHMAC } from '../src/shield/webhook';
4
+
5
+ describe('FireAPI Turnkey Webhook HMAC Verifier (ctx.webhook.verifyHMAC)', () => {
6
+ it('verifies standard SHA-256 HMAC signature using raw request body', async () => {
7
+ const app = createApp();
8
+ const router = createRouter();
9
+ const secret = 'super-secret-signing-key-999';
10
+
11
+ router.post('/webhooks/payment', async (ctx) => {
12
+ const isValid = await ctx.webhook.verifyHMAC({
13
+ secret,
14
+ header: 'x-signature-256',
15
+ });
16
+
17
+ if (!isValid) {
18
+ return ctx.unauthorized('Invalid webhook signature');
19
+ }
20
+
21
+ return ctx.ok({ verified: true, event: ctx.body.event });
22
+ });
23
+
24
+ app.use(router);
25
+
26
+ const payload = JSON.stringify({ event: 'charge.succeeded', amount: 5000 });
27
+ const validSignature = await computeHMAC(payload, secret, 'SHA-256');
28
+
29
+ // 1. Valid Signature -> 200 OK
30
+ const resValid = await app.handle(
31
+ new Request('http://localhost:3000/webhooks/payment', {
32
+ method: 'POST',
33
+ headers: {
34
+ 'Content-Type': 'application/json',
35
+ 'x-signature-256': validSignature,
36
+ },
37
+ body: payload,
38
+ })
39
+ );
40
+
41
+ expect(resValid.status).toBe(200);
42
+ const bodyValid = await resValid.json();
43
+ expect(bodyValid.verified).toBe(true);
44
+ expect(bodyValid.event).toBe('charge.succeeded');
45
+
46
+ // 2. Tampered Payload with same signature -> 401 Unauthorized
47
+ const tamperedPayload = JSON.stringify({ event: 'charge.succeeded', amount: 999999 });
48
+ const resTampered = await app.handle(
49
+ new Request('http://localhost:3000/webhooks/payment', {
50
+ method: 'POST',
51
+ headers: {
52
+ 'Content-Type': 'application/json',
53
+ 'x-signature-256': validSignature,
54
+ },
55
+ body: tamperedPayload,
56
+ })
57
+ );
58
+
59
+ expect(resTampered.status).toBe(401);
60
+
61
+ // 3. Forged Signature -> 401 Unauthorized
62
+ const resForged = await app.handle(
63
+ new Request('http://localhost:3000/webhooks/payment', {
64
+ method: 'POST',
65
+ headers: {
66
+ 'Content-Type': 'application/json',
67
+ 'x-signature-256': 'badf00d1234567890abcdef',
68
+ },
69
+ body: payload,
70
+ })
71
+ );
72
+
73
+ expect(resForged.status).toBe(401);
74
+ });
75
+
76
+ it('supports GitHub (sha256=...) and auto-detected header prefixes', async () => {
77
+ const app = createApp();
78
+ const router = createRouter();
79
+ const secret = 'github-webhook-secret';
80
+
81
+ router.post('/webhooks/github', async (ctx) => {
82
+ // Header omitted: auto-detects 'x-hub-signature-256'
83
+ const isValid = await ctx.webhook.verifyHMAC({ secret });
84
+
85
+ if (!isValid) {
86
+ return ctx.forbidden('Webhook signature mismatch');
87
+ }
88
+
89
+ return ctx.ok({ processed: true });
90
+ });
91
+
92
+ app.use(router);
93
+
94
+ const payload = JSON.stringify({ action: 'opened', repository: { name: 'API-FIRE' } });
95
+ const hexDigest = await computeHMAC(payload, secret, 'SHA-256');
96
+
97
+ // GitHub sends header with prefix 'sha256='
98
+ const res = await app.handle(
99
+ new Request('http://localhost:3000/webhooks/github', {
100
+ method: 'POST',
101
+ headers: {
102
+ 'Content-Type': 'application/json',
103
+ 'X-Hub-Signature-256': `sha256=${hexDigest}`,
104
+ },
105
+ body: payload,
106
+ })
107
+ );
108
+
109
+ expect(res.status).toBe(200);
110
+ const body = await res.json();
111
+ expect(body.processed).toBe(true);
112
+ });
113
+ });
@@ -0,0 +1,54 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { WebSocketHub, MockWebSocket } from '../src/ws/ws';
3
+
4
+ describe('FireAPI Native WebSockets with Rooms & Pub/Sub', () => {
5
+ it('manages rooms, joining, leaving, and publishing messages', () => {
6
+ const hub = new WebSocketHub();
7
+
8
+ const client1 = new MockWebSocket('ws_1');
9
+ const client2 = new MockWebSocket('ws_2');
10
+ const client3 = new MockWebSocket('ws_3');
11
+
12
+ const fireWs1 = hub.register(client1);
13
+ const fireWs2 = hub.register(client2);
14
+ const fireWs3 = hub.register(client3);
15
+
16
+ // Client 1 and 2 join room "chat:tech"
17
+ fireWs1.join('chat:tech');
18
+ fireWs2.join('chat:tech');
19
+
20
+ // Client 3 joins room "chat:random"
21
+ fireWs3.join('chat:random');
22
+
23
+ // Client 1 publishes message to "chat:tech"
24
+ fireWs1.publish('chat:tech', { user: 'Alice', text: 'Hello tech!' });
25
+
26
+ // Client 2 should receive the message
27
+ expect(client2.receivedMessages.length).toBe(1);
28
+ expect(JSON.parse(client2.receivedMessages[0])).toEqual({ user: 'Alice', text: 'Hello tech!' });
29
+
30
+ // Client 3 in another room should NOT receive it
31
+ expect(client3.receivedMessages.length).toBe(0);
32
+
33
+ // Client 2 leaves the room
34
+ fireWs2.leave('chat:tech');
35
+
36
+ // Client 1 publishes again
37
+ fireWs1.publish('chat:tech', { user: 'Alice', text: 'Anyone here?' });
38
+ expect(client2.receivedMessages.length).toBe(1); // Still 1, didn't receive second
39
+ });
40
+
41
+ it('broadcasts messages to all connected clients globally', () => {
42
+ const hub = new WebSocketHub();
43
+ const c1 = new MockWebSocket('c1');
44
+ const c2 = new MockWebSocket('c2');
45
+
46
+ const ws1 = hub.register(c1);
47
+ hub.register(c2);
48
+
49
+ ws1.broadcast({ announcement: 'System update in 5 mins' });
50
+
51
+ expect(c1.receivedMessages.length).toBe(1);
52
+ expect(c2.receivedMessages.length).toBe(1);
53
+ });
54
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "sourceMap": true,
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "isolatedModules": true,
14
+ "rootDir": "./src",
15
+ "outDir": "./dist"
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist", "test"]
19
+ }