@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,177 @@
1
+ import { createApp, createRouter, s, createAuth, enableStudio } from '../../src/index';
2
+
3
+ // 1. Initialize FireAPI Application
4
+ const app = createApp({
5
+ port: 3000,
6
+ database: {
7
+ driver: 'memory',
8
+ autoMock: true,
9
+ },
10
+ storage: {
11
+ driver: 'memory',
12
+ baseUrl: 'http://localhost:3000/uploads',
13
+ },
14
+ security: {
15
+ rateLimit: { max: 100, window: '1m' },
16
+ cors: true,
17
+ headers: true,
18
+ },
19
+ docs: '/docs',
20
+ openapiPath: '/openapi.json',
21
+ metricsPath: '/metrics', // Standard Prometheus scraping endpoint
22
+ });
23
+
24
+ // 2. Turnkey Authentication & Embedded Dev Studio
25
+ app.use('/auth', createAuth({ jwtSecret: 'jwt-super-secret-key' }));
26
+ enableStudio(app, { path: '/studio' });
27
+
28
+ // 3. Background Job Worker & In-Process Typed Event Bus
29
+ app.queue.handle('send-email', async (data) => {
30
+ console.log(`[Queue Worker] 📧 Sending email to ${data.to}: "${data.subject}"`);
31
+ });
32
+
33
+ app.on('order.*', async (order) => {
34
+ console.log(`[Event Bus] 📡 Order event captured: total=$${order.total}`);
35
+ });
36
+
37
+ // 4. Lifecycle Interceptor Hook
38
+ app.hook('afterHandle', async (ctx, response) => {
39
+ response.headers.set('X-Powered-By', 'FireAPI Engine');
40
+ return response;
41
+ });
42
+
43
+ // 5. Scheduled Cron Task
44
+ app.cron('@hourly', async (appInstance) => {
45
+ console.log('[FireCron] ⏰ Hourly background maintenance running...');
46
+ });
47
+
48
+ // 5. Define Book Validation Schema
49
+ const BookSchema = s.object({
50
+ title: s.string().min(2).description('Title of the book'),
51
+ author: s.string().min(2).description('Author name'),
52
+ price: s.number().min(0).description('Price in USD'),
53
+ category: s.enum(['tech', 'fiction', 'science']).default('tech'),
54
+ });
55
+
56
+ const bookRouter = createRouter('/books');
57
+
58
+ // GET /books - With route-level caching and eager-loaded relations
59
+ bookRouter.get('/', {
60
+ cache: { ttl: 60, tags: ['books'] },
61
+ ai: { description: 'Retrieve all books from the store', tags: ['Books'] },
62
+ handler: async (ctx) => {
63
+ return ctx.db.books.include('reviews').find();
64
+ },
65
+ });
66
+
67
+ // POST /books/checkout - Idempotency Shield (Safe Replays with X-Idempotency-Key)
68
+ bookRouter.post('/checkout', {
69
+ idempotent: true,
70
+ handler: async (ctx) => {
71
+ const order = await ctx.db.orders.insert({
72
+ items: ctx.body.items,
73
+ total: ctx.body.total,
74
+ orderedAt: new Date().toISOString(),
75
+ });
76
+ return ctx.created(order);
77
+ },
78
+ });
79
+
80
+ // POST /books/transfer - Atomic Transaction Scope with auto-rollback
81
+ bookRouter.post('/transfer', async (ctx) => {
82
+ const result = await ctx.db.tx(async (trx) => {
83
+ await trx.inventories.update(ctx.body.warehouseA, { stock: 10 });
84
+ await trx.inventories.update(ctx.body.warehouseB, { stock: 90 });
85
+ await trx.transfers.insert({ from: ctx.body.warehouseA, to: ctx.body.warehouseB, qty: 40 });
86
+ return { success: true };
87
+ });
88
+ return ctx.ok(result);
89
+ });
90
+
91
+ // POST /webhooks/payment - Turnkey HMAC Webhook Verifier
92
+ bookRouter.post('/webhooks/payment', async (ctx) => {
93
+ const valid = await ctx.webhook.verifyHMAC({
94
+ secret: 'whsec_sample_secret_key',
95
+ });
96
+ if (!valid) return ctx.unauthorized('Invalid webhook signature');
97
+ return ctx.ok({ received: true, event: ctx.body.event });
98
+ });
99
+
100
+ // POST /books - With validation, background email dispatch & cache invalidation
101
+ bookRouter.post('/', {
102
+ schema: { body: BookSchema },
103
+ ai: { description: 'Add a new book to the library', tags: ['Books'] },
104
+ handler: async (ctx) => {
105
+ const newBook = await ctx.db.books.insert(ctx.body);
106
+ await ctx.cache.invalidate('books');
107
+
108
+ // Dispatch background email without blocking the HTTP response
109
+ await ctx.queue.dispatch('send-email', {
110
+ to: 'admin@fire.dev',
111
+ subject: `New book added: ${newBook.title}`,
112
+ });
113
+
114
+ return ctx.created(newBook);
115
+ },
116
+ });
117
+
118
+ // DELETE /books/:id - Ultra-simple 1-line delete
119
+ bookRouter.delete('/:id', async (ctx) => {
120
+ await ctx.db.books.delete(ctx.params.id);
121
+ return { deleted: true };
122
+ });
123
+
124
+ // POST /books/cover - Universal Object Storage Upload
125
+ bookRouter.post('/cover', {
126
+ ai: { description: 'Upload book cover image', tags: ['Storage'] },
127
+ handler: async (ctx) => {
128
+ const uploaded = await ctx.storage.put('covers/sample.png', 'fake_png_binary_data', {
129
+ contentType: 'image/png',
130
+ });
131
+ return ctx.created(uploaded);
132
+ },
133
+ });
134
+
135
+ // GET /books/stream/ai - Real-Time AI Token Streaming via Server-Sent Events (SSE)
136
+ bookRouter.get('/stream/ai', {
137
+ ai: { description: 'Simulate streaming AI thoughts token-by-token', tags: ['AI'] },
138
+ handler: (ctx) => {
139
+ return ctx.stream.sse(async (stream) => {
140
+ const tokens = ['FireAPI', 'is', 'the', 'fastest', 'modern', 'TypeScript', 'framework!'];
141
+ for (const token of tokens) {
142
+ await stream.send(token, { event: 'token' });
143
+ await new Promise((r) => setTimeout(r, 100)); // Simulate LLM latency
144
+ }
145
+ await stream.send('[DONE]', { event: 'done' });
146
+ stream.close();
147
+ });
148
+ },
149
+ });
150
+
151
+ // 6. Native Real-Time WebSockets with Rooms
152
+ app.ws('/chat/:room', {
153
+ open(ws, ctx) {
154
+ ws.join(ctx.params.room);
155
+ ws.publish(ctx.params.room, { notice: `User connected to room ${ctx.params.room}` });
156
+ },
157
+ message(ws, msg, ctx) {
158
+ ws.publish(ctx.params.room, { message: msg });
159
+ },
160
+ });
161
+
162
+ // 7. Mount Router & Enable Model Context Protocol (MCP) for AI Agents
163
+ app.use(bookRouter);
164
+ app.enableMCP({ name: 'BookStoreMCP', version: '1.0.0' });
165
+
166
+ // 8. Start Server
167
+ console.log('🔥 FireAPI server launching at http://localhost:3000');
168
+ console.log('🖥️ FireStudio Dashboard: http://localhost:3000/studio');
169
+ console.log('📖 Interactive Scalar API Docs: http://localhost:3000/docs');
170
+ console.log('📊 Prometheus Metrics: http://localhost:3000/metrics');
171
+ console.log('⚡ OpenAPI 3.1 Spec: http://localhost:3000/openapi.json');
172
+ console.log('🤖 MCP AI Endpoint: http://localhost:3000/mcp');
173
+
174
+ app.listen(3000);
175
+
176
+ export type AppRouter = typeof app;
177
+ export default app;
package/package.json CHANGED
@@ -1,6 +1,70 @@
1
1
  {
2
2
  "name": "@tamayrp14/fire-api",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.1",
4
+ "description": "Ultra-lightweight, blazing-fast, type-safe API framework with universal DB, security shield, turnkey auth, express compat, dev studio, and native AI/MCP integration",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "type": "module",
8
+ "bin": {
9
+ "fire-api": "./bin/fire.mjs",
10
+ "fire": "./bin/fire.mjs"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/yogarizyapratama/FIRE-API.git"
15
+ },
16
+ "homepage": "https://github.com/yogarizyapratama/FIRE-API#readme",
17
+ "bugs": {
18
+ "url": "https://github.com/yogarizyapratama/FIRE-API/issues"
19
+ },
20
+ "author": "Yoga Rizya Pratama",
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "exports": {
25
+ ".": {
26
+ "import": "./dist/index.js",
27
+ "types": "./dist/index.d.ts"
28
+ },
29
+ "./client": {
30
+ "import": "./dist/client.js",
31
+ "types": "./dist/client.d.ts"
32
+ },
33
+ "./auth": {
34
+ "import": "./dist/auth/auth.js",
35
+ "types": "./dist/auth/auth.d.ts"
36
+ },
37
+ "./compat": {
38
+ "import": "./dist/compat/express.js",
39
+ "types": "./dist/compat/express.d.ts"
40
+ },
41
+ "./studio": {
42
+ "import": "./dist/studio/studio.js",
43
+ "types": "./dist/studio/studio.d.ts"
44
+ }
45
+ },
46
+ "scripts": {
47
+ "test": "bun test",
48
+ "build": "bun build ./bin/fire.js --outfile ./bin/fire.mjs --target node --format esm && chmod +x ./bin/fire.mjs && bun build ./src/index.ts ./src/client.ts ./src/auth/auth.ts ./src/compat/express.ts ./src/studio/studio.ts --outdir ./dist --target node && tsc --module ESNext --moduleResolution bundler --declaration --emitDeclarationOnly --outDir dist",
49
+ "prepublishOnly": "bun run build"
50
+ },
51
+ "keywords": [
52
+ "framework",
53
+ "api",
54
+ "typescript",
55
+ "fast",
56
+ "bun",
57
+ "node",
58
+ "mcp",
59
+ "sqlite",
60
+ "postgres",
61
+ "auth",
62
+ "studio",
63
+ "rest"
64
+ ],
65
+ "license": "MIT",
66
+ "devDependencies": {
67
+ "@types/node": "^22.13.0",
68
+ "typescript": "^5.7.3"
69
+ }
70
+ }
package/src/ai/mcp.ts ADDED
@@ -0,0 +1,140 @@
1
+ import { Router } from '../core/router';
2
+ import { createContext } from '../core/context';
3
+ import { BaseSchema } from '../schema/schema';
4
+
5
+ export interface MCPToolManifest {
6
+ name: string;
7
+ description: string;
8
+ inputSchema: Record<string, any>;
9
+ }
10
+
11
+ export interface MCPToolResult {
12
+ content: { type: 'text'; text: string }[];
13
+ isError?: boolean;
14
+ }
15
+
16
+ export interface MCPServerOptions {
17
+ name?: string;
18
+ version?: string;
19
+ }
20
+
21
+ export function routeToToolName(method: string, path: string): string {
22
+ const clean = path
23
+ .replace(/^\//, '')
24
+ .replace(/:([a-zA-Z0-9_]+)/g, '$1')
25
+ .replace(/[^a-zA-Z0-9_]/g, '_')
26
+ .replace(/_+/g, '_')
27
+ .replace(/_$/, '');
28
+ return `${method.toLowerCase()}_${clean || 'root'}`;
29
+ }
30
+
31
+ export class MCPServer {
32
+ constructor(private router: Router, private options: MCPServerOptions = {}) {}
33
+
34
+ public listTools(): MCPToolManifest[] {
35
+ const tools: MCPToolManifest[] = [];
36
+
37
+ for (const route of this.router.routesList) {
38
+ if (!route.options.ai) continue;
39
+
40
+ const toolName = route.options.ai.operationId || routeToToolName(route.method, route.path);
41
+ const schema = route.options.schema;
42
+
43
+ let inputSchema: Record<string, any> = { type: 'object', properties: {} };
44
+
45
+ if (schema?.body && schema.body instanceof BaseSchema) {
46
+ inputSchema = schema.body.toOpenAPI();
47
+ } else if (schema?.params && schema.params instanceof BaseSchema) {
48
+ inputSchema = schema.params.toOpenAPI();
49
+ } else if (schema?.query && schema.query instanceof BaseSchema) {
50
+ inputSchema = schema.query.toOpenAPI();
51
+ }
52
+
53
+ tools.push({
54
+ name: toolName,
55
+ description: route.options.ai.description || `${route.method} ${route.path}`,
56
+ inputSchema,
57
+ });
58
+ }
59
+
60
+ return tools;
61
+ }
62
+
63
+ public async callTool(name: string, args: Record<string, any> = {}): Promise<MCPToolResult> {
64
+ for (const route of this.router.routesList) {
65
+ if (!route.options.ai) continue;
66
+ const toolName = route.options.ai.operationId || routeToToolName(route.method, route.path);
67
+
68
+ if (toolName === name) {
69
+ try {
70
+ // Construct simulated request and context
71
+ const req = new Request(`http://localhost${route.path}`, {
72
+ method: route.method,
73
+ headers: { 'Content-Type': 'application/json' },
74
+ body: route.method !== 'GET' && route.method !== 'HEAD' ? JSON.stringify(args) : undefined,
75
+ });
76
+
77
+ const ctx = createContext({
78
+ req,
79
+ params: args,
80
+ body: args,
81
+ });
82
+
83
+ const res = await route.options.handler(ctx);
84
+ let outputText = '';
85
+
86
+ if (res instanceof Response) {
87
+ outputText = await res.text();
88
+ } else {
89
+ outputText = typeof res === 'string' ? res : JSON.stringify(res);
90
+ }
91
+
92
+ return {
93
+ content: [{ type: 'text', text: outputText }],
94
+ isError: false,
95
+ };
96
+ } catch (err: any) {
97
+ return {
98
+ content: [{ type: 'text', text: err?.message || 'Error executing tool' }],
99
+ isError: true,
100
+ };
101
+ }
102
+ }
103
+ }
104
+
105
+ return {
106
+ content: [{ type: 'text', text: `Tool not found: ${name}` }],
107
+ isError: true,
108
+ };
109
+ }
110
+
111
+ public handleMCPProtocol(message: { method: string; params?: any; id?: any }): Promise<any> {
112
+ const { method, params, id } = message;
113
+
114
+ if (method === 'tools/list') {
115
+ return Promise.resolve({
116
+ jsonrpc: '2.0',
117
+ id,
118
+ result: { tools: this.listTools() },
119
+ });
120
+ }
121
+
122
+ if (method === 'tools/call') {
123
+ return this.callTool(params?.name, params?.arguments).then((res) => ({
124
+ jsonrpc: '2.0',
125
+ id,
126
+ result: res,
127
+ }));
128
+ }
129
+
130
+ return Promise.resolve({
131
+ jsonrpc: '2.0',
132
+ id,
133
+ error: { code: -32601, message: 'Method not found' },
134
+ });
135
+ }
136
+ }
137
+
138
+ export function createMCPServer(router: Router, options: MCPServerOptions = {}): MCPServer {
139
+ return new MCPServer(router, options);
140
+ }
@@ -0,0 +1,110 @@
1
+ import { createRouter, Router } from '../core/router';
2
+ import { Middleware } from '../types';
3
+ import { s } from '../schema/schema';
4
+ import { hashPassword, verifyPassword } from './crypto';
5
+ import { signJWT, verifyJWT } from './jwt';
6
+
7
+ export interface CreateAuthOptions {
8
+ jwtSecret: string;
9
+ tableName?: string;
10
+ tokenExpirySeconds?: number;
11
+ }
12
+
13
+ export function createAuth(options: CreateAuthOptions): Router {
14
+ const router = createRouter();
15
+ const tableName = options.tableName || 'users';
16
+ const secret = options.jwtSecret;
17
+ const expiry = options.tokenExpirySeconds || 86400; // 24 hours
18
+
19
+ // POST /register
20
+ router.post('/register', {
21
+ schema: {
22
+ body: s.object({
23
+ email: s.string().email(),
24
+ password: s.string().min(6),
25
+ name: s.string().optional(),
26
+ }),
27
+ },
28
+ ai: { description: 'Register a new user account with credentials', tags: ['Auth'] },
29
+ handler: async (ctx) => {
30
+ const { email, password, name } = ctx.body;
31
+
32
+ // Check if user already exists
33
+ const existing = await ctx.db.table(tableName).where('email', email).findOne();
34
+ if (existing) {
35
+ return ctx.badRequest('A user with this email already exists');
36
+ }
37
+
38
+ const hashedPassword = await hashPassword(password);
39
+ const newUser = await ctx.db.table(tableName).insert({
40
+ email,
41
+ password: hashedPassword,
42
+ name: name || email.split('@')[0],
43
+ });
44
+
45
+ // Strip password from returned user object
46
+ const safeUser = { id: newUser.id, email: newUser.email, name: newUser.name };
47
+ const token = await signJWT(safeUser, secret, expiry);
48
+
49
+ return ctx.created({ user: safeUser, token });
50
+ },
51
+ });
52
+
53
+ // POST /login
54
+ router.post('/login', {
55
+ schema: {
56
+ body: s.object({
57
+ email: s.string().email(),
58
+ password: s.string(),
59
+ }),
60
+ },
61
+ ai: { description: 'Authenticate user with email and password', tags: ['Auth'] },
62
+ handler: async (ctx) => {
63
+ const { email, password } = ctx.body;
64
+
65
+ const user = await ctx.db.table(tableName).where('email', email).findOne();
66
+ if (!user) {
67
+ return ctx.unauthorized('Invalid email or password');
68
+ }
69
+
70
+ const isValid = await verifyPassword(password, user.password);
71
+ if (!isValid) {
72
+ return ctx.unauthorized('Invalid email or password');
73
+ }
74
+
75
+ const safeUser = { id: user.id, email: user.email, name: user.name };
76
+ const token = await signJWT(safeUser, secret, expiry);
77
+
78
+ return ctx.ok({ user: safeUser, token });
79
+ },
80
+ });
81
+
82
+ // GET /me
83
+ router.get('/me', {
84
+ middlewares: [requireAuth(secret)],
85
+ ai: { description: 'Get authenticated user profile', tags: ['Auth'] },
86
+ handler: (ctx) => {
87
+ return ctx.ok({ user: ctx.user });
88
+ },
89
+ });
90
+
91
+ return router;
92
+ }
93
+
94
+ export function requireAuth(secret: string): Middleware {
95
+ return async (ctx, next) => {
96
+ const authHeader = ctx.req.headers.get('authorization') || ctx.req.headers.get('Authorization');
97
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
98
+ return ctx.unauthorized('Missing or invalid Authorization header');
99
+ }
100
+
101
+ const token = authHeader.slice(7).trim();
102
+ try {
103
+ const payload = await verifyJWT(token, secret);
104
+ ctx.user = payload;
105
+ return next();
106
+ } catch {
107
+ return ctx.unauthorized('Invalid or expired token');
108
+ }
109
+ };
110
+ }
@@ -0,0 +1,70 @@
1
+ function bufferToHex(buffer: ArrayBuffer | Uint8Array): string {
2
+ return Array.from(new Uint8Array(buffer as any))
3
+ .map((b) => b.toString(16).padStart(2, '0'))
4
+ .join('');
5
+ }
6
+
7
+ function hexToBuffer(hex: string): Uint8Array {
8
+ const bytes = new Uint8Array(hex.length / 2);
9
+ for (let i = 0; i < hex.length; i += 2) {
10
+ bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
11
+ }
12
+ return bytes;
13
+ }
14
+
15
+ export async function hashPassword(password: string): Promise<string> {
16
+ const salt = new Uint8Array(16);
17
+ crypto.getRandomValues(salt);
18
+
19
+ const enc = new TextEncoder();
20
+ const keyMaterial = await crypto.subtle.importKey(
21
+ 'raw',
22
+ enc.encode(password),
23
+ 'PBKDF2',
24
+ false,
25
+ ['deriveBits', 'deriveKey']
26
+ );
27
+
28
+ const derived = await crypto.subtle.deriveBits(
29
+ {
30
+ name: 'PBKDF2',
31
+ salt: salt as any,
32
+ iterations: 100000,
33
+ hash: 'SHA-256',
34
+ },
35
+ keyMaterial,
36
+ 256
37
+ );
38
+
39
+ return `${bufferToHex(salt)}:${bufferToHex(derived)}`;
40
+ }
41
+
42
+ export async function verifyPassword(password: string, storedHash: string): Promise<boolean> {
43
+ const parts = storedHash.split(':');
44
+ if (parts.length !== 2) return false;
45
+
46
+ const [saltHex, originalHashHex] = parts;
47
+ const salt = hexToBuffer(saltHex);
48
+
49
+ const enc = new TextEncoder();
50
+ const keyMaterial = await crypto.subtle.importKey(
51
+ 'raw',
52
+ enc.encode(password),
53
+ 'PBKDF2',
54
+ false,
55
+ ['deriveBits', 'deriveKey']
56
+ );
57
+
58
+ const derived = await crypto.subtle.deriveBits(
59
+ {
60
+ name: 'PBKDF2',
61
+ salt: salt as any,
62
+ iterations: 100000,
63
+ hash: 'SHA-256',
64
+ },
65
+ keyMaterial,
66
+ 256
67
+ );
68
+
69
+ return bufferToHex(derived) === originalHashHex;
70
+ }
@@ -0,0 +1,105 @@
1
+ function base64UrlEncode(str: string): string {
2
+ const bytes = new TextEncoder().encode(str);
3
+ let binary = '';
4
+ for (let i = 0; i < bytes.byteLength; i++) {
5
+ binary += String.fromCharCode(bytes[i]);
6
+ }
7
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
8
+ }
9
+
10
+ function base64UrlDecode(str: string): string {
11
+ let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
12
+ while (base64.length % 4) {
13
+ base64 += '=';
14
+ }
15
+ const binary = atob(base64);
16
+ const bytes = new Uint8Array(binary.length);
17
+ for (let i = 0; i < binary.length; i++) {
18
+ bytes[i] = binary.charCodeAt(i);
19
+ }
20
+ return new TextDecoder().decode(bytes);
21
+ }
22
+
23
+ function arrayBufferToBase64Url(buffer: ArrayBuffer): string {
24
+ const bytes = new Uint8Array(buffer);
25
+ let binary = '';
26
+ for (let i = 0; i < bytes.byteLength; i++) {
27
+ binary += String.fromCharCode(bytes[i]);
28
+ }
29
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
30
+ }
31
+
32
+ export async function signJWT(
33
+ payload: Record<string, any>,
34
+ secret: string,
35
+ expiresInSeconds = 86400
36
+ ): Promise<string> {
37
+ const header = { alg: 'HS256', typ: 'JWT' };
38
+ const now = Math.floor(Date.now() / 1000);
39
+ const fullPayload = {
40
+ ...payload,
41
+ iat: now,
42
+ exp: now + expiresInSeconds,
43
+ };
44
+
45
+ const encodedHeader = base64UrlEncode(JSON.stringify(header));
46
+ const encodedPayload = base64UrlEncode(JSON.stringify(fullPayload));
47
+ const dataToSign = `${encodedHeader}.${encodedPayload}`;
48
+
49
+ const enc = new TextEncoder();
50
+ const key = await crypto.subtle.importKey(
51
+ 'raw',
52
+ enc.encode(secret),
53
+ { name: 'HMAC', hash: 'SHA-256' },
54
+ false,
55
+ ['sign']
56
+ );
57
+
58
+ const signature = await crypto.subtle.sign('HMAC', key, enc.encode(dataToSign));
59
+ const encodedSignature = arrayBufferToBase64Url(signature);
60
+
61
+ return `${dataToSign}.${encodedSignature}`;
62
+ }
63
+
64
+ export async function verifyJWT<T = any>(token: string, secret: string): Promise<T> {
65
+ const parts = token.split('.');
66
+ if (parts.length !== 3) {
67
+ throw new Error('Invalid JWT format');
68
+ }
69
+
70
+ const [encodedHeader, encodedPayload, encodedSignature] = parts;
71
+ const dataToVerify = `${encodedHeader}.${encodedPayload}`;
72
+
73
+ const enc = new TextEncoder();
74
+ const key = await crypto.subtle.importKey(
75
+ 'raw',
76
+ enc.encode(secret),
77
+ { name: 'HMAC', hash: 'SHA-256' },
78
+ false,
79
+ ['verify']
80
+ );
81
+
82
+ // Decode signature to ArrayBuffer
83
+ let base64Sig = encodedSignature.replace(/-/g, '+').replace(/_/g, '/');
84
+ while (base64Sig.length % 4) {
85
+ base64Sig += '=';
86
+ }
87
+ const binarySig = atob(base64Sig);
88
+ const sigBytes = new Uint8Array(binarySig.length);
89
+ for (let i = 0; i < binarySig.length; i++) {
90
+ sigBytes[i] = binarySig.charCodeAt(i);
91
+ }
92
+
93
+ const isValid = await crypto.subtle.verify('HMAC', key, sigBytes, enc.encode(dataToVerify));
94
+ if (!isValid) {
95
+ throw new Error('Invalid JWT signature');
96
+ }
97
+
98
+ const payload: any = JSON.parse(base64UrlDecode(encodedPayload));
99
+ const now = Math.floor(Date.now() / 1000);
100
+ if (payload.exp && payload.exp < now) {
101
+ throw new Error('JWT token has expired');
102
+ }
103
+
104
+ return payload as T;
105
+ }