@igosprojects/rivet
Advanced tools
| /* | ||
| This file is licensed under the MIT license, the terms must be followed! | ||
| Copyright(c) 2026 EyeDev | ||
| */ | ||
| export { Rivet } from './rivet'; | ||
| export type { RivetResponse } from './types'; |
+23
| /* | ||
| This file is licensed under the MIT license, the terms must be followed! | ||
| Copyright(c) 2026 EyeDev | ||
| */ | ||
| // Returns the MIME type of any file | ||
| export function GetMimeType(filePath: string): string { | ||
| const ext = filePath.split('.').pop()?.toLowerCase() || ''; | ||
| const mimeTypes: Record<string, string> = { | ||
| html: 'text/html', | ||
| css: 'text/css', | ||
| js: 'application/javascript', | ||
| json: 'application/json', | ||
| png: 'image/png', | ||
| jpg: 'image/jpeg', | ||
| jpeg: 'image/jpeg', | ||
| gif: 'image/gif', | ||
| svg: 'image/svg+xml', | ||
| txt: 'text/plain', | ||
| pdf: 'application/pdf', | ||
| }; | ||
| return mimeTypes[ext] || 'application/octet-stream'; | ||
| } |
| import fs from 'fs'; | ||
| import path from 'path'; | ||
| import { RivetResponse } from './types'; | ||
| import { GetMimeType } from './mime'; | ||
| export function InjectResponseHelpers(res: RivetResponse): void { | ||
| res.send = (data: any) => { | ||
| res.writeHead(200, { 'Content-Type': 'text/plain' }); | ||
| res.end(String(data)); | ||
| }; | ||
| res.json = (data: any) => { | ||
| res.writeHead(200, { 'Content-Type': 'application/json' }); | ||
| res.end(JSON.stringify(data)); | ||
| }; | ||
| res.SendFile = (filePath: string, contentType?: string) => { | ||
| const fullPath = path.resolve(filePath); | ||
| if (!fullPath.startsWith(process.cwd())) { | ||
| res.writeHead(403, { 'Content-Type': 'text/plain' }); | ||
| res.end('403: Forbidden'); | ||
| return; | ||
| } | ||
| fs.access(fullPath, fs.constants.R_OK, (err) => { | ||
| if (err) { | ||
| res.writeHead(404, { 'Content-Type': 'text/plain' }); | ||
| res.end('404: File not found'); | ||
| return; | ||
| } | ||
| const mimeType = contentType || GetMimeType(fullPath); | ||
| const stream = fs.createReadStream(fullPath); | ||
| res.writeHead(200, { 'Content-Type': mimeType }); | ||
| stream.pipe(res); | ||
| stream.on('error', () => { | ||
| res.writeHead(500, { 'Content-Type': 'text/plain' }); | ||
| res.end('500: Internal Server Error'); | ||
| }); | ||
| }); | ||
| }; | ||
| } |
+223
| /* | ||
| This file is licensed under the MIT license, the terms must be followed! | ||
| Copyright(c) 2026 EyeDev | ||
| */ | ||
| import http from 'http'; | ||
| import https from 'https'; | ||
| import type { RivetResponse, RouteHandlers } from './types'; | ||
| import { InjectResponseHelpers } from './response'; | ||
| import { ServeStatic } from './static'; | ||
| import type { IncomingMessage, Server, ServerResponse } from 'http'; | ||
| // Parses the request body | ||
| async function ParseBody(req: IncomingMessage): Promise<any> { | ||
| return new Promise((resolve, reject) => { | ||
| const chunks: Buffer[] = []; | ||
| req.on('data', (chunk) => { | ||
| chunks.push(chunk); | ||
| }); | ||
| req.on('end', () => { | ||
| const body = Buffer.concat(chunks).toString(); | ||
| // No body | ||
| if (!body) { | ||
| resolve({}); | ||
| return; | ||
| } | ||
| // Check content type | ||
| const contentType = req.headers['content-type'] || ''; | ||
| if (contentType.includes('application/json')) { | ||
| try { | ||
| resolve(JSON.parse(body)); | ||
| } catch (e) { | ||
| resolve({}); | ||
| } | ||
| } else if (contentType.includes('application/x-www-form-urlencoded')) { | ||
| // Parse form data: "name=Alice&age=30" → { name: "Alice", age: "30" } | ||
| const params = new URLSearchParams(body); | ||
| const result: Record<string, string> = {}; | ||
| params.forEach((value, key) => { | ||
| result[key] = value; | ||
| }); | ||
| resolve(result); | ||
| } else { | ||
| // Plain text | ||
| resolve(body); | ||
| } | ||
| }); | ||
| req.on('error', (err) => { | ||
| reject(err); | ||
| }); | ||
| }); | ||
| } | ||
| // Main library class | ||
| export class Rivet { | ||
| private server: Server | null = null; | ||
| routes: { | ||
| GET: RouteHandlers; | ||
| POST: RouteHandlers; | ||
| PUT: RouteHandlers; | ||
| DELETE: RouteHandlers; | ||
| }; | ||
| constructor() { | ||
| // Route function handlers are defined in here | ||
| this.routes = { | ||
| GET: {}, | ||
| POST: {}, | ||
| PUT: {}, | ||
| DELETE: {}, | ||
| }; | ||
| } | ||
| // Starts the HTTP server and serves on the provided port | ||
| start(port: number, callback?: () => void): void { | ||
| this.server = http.createServer((req: IncomingMessage, res: ServerResponse) => { | ||
| this.OnRequest(req, res as RivetResponse); | ||
| }); | ||
| this.server.listen(port, () => { | ||
| console.log(`Rivet fastened on ${port}`); | ||
| callback?.(); | ||
| }); | ||
| } | ||
| // Parses a path and returns the regex and parameters | ||
| private ParsePath(path: string): { regex: RegExp; paramNames: string[] } { | ||
| const paramNames: string[] = []; | ||
| // Replace :param with regex capture groups | ||
| const regexPattern = path.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, paramName) => { | ||
| paramNames.push(paramName); | ||
| return '([^/]+)'; // Capture anything except slash | ||
| }); | ||
| console.log('Path:', path, '→ Regex:', regexPattern); // Debug | ||
| // Create regex that matches exactly | ||
| const regex = new RegExp(`^${regexPattern}$`); | ||
| return { regex, paramNames }; | ||
| } | ||
| // Registers a new GET handler | ||
| get(path: string, handler: Function): void { | ||
| const { regex, paramNames } = this.ParsePath(path); | ||
| this.routes.GET[path] = { | ||
| handler, | ||
| params: paramNames, | ||
| regex, | ||
| }; | ||
| } | ||
| // Registers a new POST handler | ||
| post(path: string, handler: Function): void { | ||
| const { regex, paramNames } = this.ParsePath(path); | ||
| this.routes.POST[path] = { | ||
| handler, | ||
| params: paramNames, | ||
| regex, | ||
| }; | ||
| } | ||
| // Called when an request happends on the server | ||
| async OnRequest(req: IncomingMessage, res: RivetResponse): Promise<void> { | ||
| const method = req.method as keyof typeof this.routes; // Get the HTTP method | ||
| const url = req.url?.split('?')[0] || '/'; // Get the URL and strip the query params | ||
| // Parse query parameters | ||
| const urlObj = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`); | ||
| const query = Object.fromEntries(urlObj.searchParams); | ||
| (req as any).query = query; | ||
| // Parse body for POST, PUT, PATCH requests | ||
| if (method === 'POST' || method === 'PUT') { | ||
| try { | ||
| const body = await ParseBody(req); | ||
| (req as any).body = body; | ||
| } catch (err) { | ||
| res.writeHead(400, { 'Content-Type': 'application/json' }); | ||
| res.end(JSON.stringify({ error: 'Invalid request body' })); | ||
| return; | ||
| } | ||
| } | ||
| const methodRoutes = this.routes[method]; | ||
| if (!methodRoutes) { | ||
| res.writeHead(405, { 'Content-Type': 'text/plain' }); | ||
| res.end('405: Method Not Allowed'); | ||
| return; | ||
| } | ||
| // Try exact match first (fast path) | ||
| let route = methodRoutes[url]; | ||
| let params: Record<string, string> = {}; | ||
| // If no exact match, try regex | ||
| if (!route) { | ||
| console.log('No exact match for:', url); | ||
| console.log('Available patterns:', Object.keys(methodRoutes)); | ||
| for (const [pattern, r] of Object.entries(methodRoutes)) { | ||
| if (pattern.endsWith('*')) continue; // Check if "/*" or such | ||
| const match = url.match(r.regex); | ||
| if (match) { | ||
| route = r; | ||
| // Extract params from capture groups | ||
| r.params.forEach((paramName, index) => { | ||
| params[paramName] = match[index + 1]; | ||
| }); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| // If still no match, try wildcard (static files) | ||
| if (!route) { | ||
| for (const [pattern, r] of Object.entries(methodRoutes)) { | ||
| if (pattern.endsWith('*') && url.startsWith(pattern.slice(0, -1))) { | ||
| route = r; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| // If a handler was registered, call it | ||
| if (route && route.handler) { | ||
| InjectResponseHelpers(res); // Injects response helpers into the res object | ||
| (req as any).params = params; | ||
| await route.handler(req, res); // Now finnaly, call the handler | ||
| } else { | ||
| res.writeHead(404, { 'Content-Type': 'text/plain' }); | ||
| res.end('404: Page not found!'); | ||
| } | ||
| } | ||
| // Serves a static directory on the server | ||
| static(prefix: string, directory: string): void { | ||
| const StaticHandler = ServeStatic(directory); | ||
| // Register a catch-all handler for this prefix | ||
| this.routes.GET[`${prefix}*`] = { | ||
| handler: async (req: IncomingMessage, res: RivetResponse) => { | ||
| const served = await StaticHandler(req, res); | ||
| if (!served) { | ||
| res.writeHead(404, { 'Content-Type': 'text/plain' }); | ||
| res.end('404: File not found!'); | ||
| } | ||
| }, | ||
| params: [], | ||
| regex: new RegExp(`^${prefix}.*$`), | ||
| }; | ||
| } | ||
| } |
| import fs from 'fs'; | ||
| import path from 'path'; | ||
| import { IncomingMessage, ServerResponse } from 'http'; | ||
| import { GetMimeType } from './mime'; | ||
| // Serves the contents of a folder as static files on the server | ||
| export function ServeStatic(rootDir: string) { | ||
| const absoluteRoot = path.resolve(rootDir); | ||
| return async (req: IncomingMessage, res: ServerResponse): Promise<boolean> => { | ||
| const url = req.url?.split('?')[0] || '/'; | ||
| // Convert URL to file path | ||
| let filePath = path.join(absoluteRoot, url); | ||
| // Security: prevent directory traversal | ||
| if (!filePath.startsWith(absoluteRoot)) { | ||
| return false; | ||
| } | ||
| // Check if file exists | ||
| try { | ||
| const stat = await fs.promises.stat(filePath); | ||
| if (stat.isDirectory()) { | ||
| // Try index.html, if its there make it so the root returns index.html | ||
| filePath = path.join(filePath, 'index.html'); | ||
| try { | ||
| await fs.promises.stat(filePath); | ||
| } catch { | ||
| return false; // No index.html | ||
| } | ||
| } | ||
| // Serve the file | ||
| const mimeType = GetMimeType(filePath); | ||
| const stream = fs.createReadStream(filePath); | ||
| res.writeHead(200, { 'Content-Type': mimeType }); | ||
| stream.pipe(res); | ||
| stream.on('error', () => { | ||
| res.writeHead(500); | ||
| res.end(); | ||
| }); | ||
| return true; | ||
| } catch { | ||
| return false; // File not found | ||
| } | ||
| }; | ||
| } |
+25
| /* | ||
| This file is licensed under the MIT license, the terms must be followed! | ||
| Copyright(c) 2026 EyeDev | ||
| */ | ||
| import { ServerResponse } from 'node:http'; | ||
| // New route type, replaced old simple one | ||
| export type Route = { | ||
| handler: Function; | ||
| params: string[]; // Store param names like ['id', 'name'] | ||
| regex: RegExp; // Converted regex for matching | ||
| }; | ||
| // Route handler type | ||
| export type RouteHandlers = { | ||
| [key: string]: Route; // Changed from Function to Route | ||
| }; | ||
| // HTTP response type | ||
| export type RivetResponse = ServerResponse & { | ||
| send: (data: any) => void; | ||
| json: (data: any) => void; | ||
| SendFile: (filePath: string, contentType?: string) => void; | ||
| }; |
+2
-1
| { | ||
| "name": "@igosprojects/rivet", | ||
| "version": "1.0.1", | ||
| "version": "1.0.2", | ||
| "description": "Rivet is a small server framework for NodeJS", | ||
@@ -9,2 +9,3 @@ "main": "./dist/index.js", | ||
| "dist", | ||
| "src", | ||
| "README.md", | ||
@@ -11,0 +12,0 @@ "LICENSE" |
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
41502
40.48%33
22.22%682
87.36%4
100%5
150%