@kozojs/core 0.3.7 → 0.3.8

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.
@@ -42,6 +42,11 @@ function rateLimit(options) {
42
42
  const windowMs = window * 1e3;
43
43
  let record = store.get(key);
44
44
  if (!record || now > record.resetAt) {
45
+ if (store.size > 1e4) {
46
+ for (const [k, v] of store) {
47
+ if (now > v.resetAt) store.delete(k);
48
+ }
49
+ }
45
50
  record = { count: 0, resetAt: now + windowMs };
46
51
  }
47
52
  record.count++;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/middleware/logger.ts","../../src/middleware/cors.ts","../../src/middleware/rate-limit.ts","../../src/middleware/error-handler.ts","../../src/middleware/fileSystemRouting.ts"],"sourcesContent":["import type { Context, Next } from 'hono';\r\n\r\nexport interface LoggerOptions {\r\n prefix?: string;\r\n colorize?: boolean;\r\n}\r\n\r\n/**\r\n * Request logger middleware\r\n */\r\nexport function logger(options: LoggerOptions = {}) {\r\n const { prefix = '🌐', colorize = true } = options;\r\n\r\n return async (c: Context, next: Next) => {\r\n const start = Date.now();\r\n const method = c.req.method;\r\n const path = new URL(c.req.url).pathname;\r\n\r\n await next();\r\n\r\n const duration = Date.now() - start;\r\n const status = c.res.status;\r\n\r\n const statusColor = status >= 500 ? 'šŸ”“' : status >= 400 ? '🟔' : '🟢';\r\n const log = `${prefix} ${method.padEnd(6)} ${path} ${statusColor} ${status} ${duration}ms`;\r\n \r\n console.log(log);\r\n };\r\n}\r\n","import { cors as honoCors } from 'hono/cors';\r\n\r\nexport interface CorsOptions {\r\n origin?: string | string[] | ((origin: string) => string | undefined | null);\r\n allowMethods?: string[];\r\n allowHeaders?: string[];\r\n exposeHeaders?: string[];\r\n maxAge?: number;\r\n credentials?: boolean;\r\n}\r\n\r\n/**\r\n * CORS middleware wrapper\r\n */\r\nexport function cors(options: CorsOptions = {}) {\r\n return honoCors({\r\n origin: options.origin || '*',\r\n allowMethods: options.allowMethods || ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],\r\n allowHeaders: options.allowHeaders || ['Content-Type', 'Authorization'],\r\n exposeHeaders: options.exposeHeaders || [],\r\n maxAge: options.maxAge || 86400,\r\n credentials: options.credentials || false\r\n });\r\n}\r\n","import type { Context, Next } from 'hono';\n\nexport interface RateLimitOptions {\n max: number;\n window: number; // in seconds\n keyGenerator?: (c: Context) => string;\n message?: string;\n}\n\n// In-memory store (use Redis in production)\nconst store = new Map<string, { count: number; resetAt: number }>();\n\n/**\n * Simple rate limiting middleware\n * For production, replace in-memory store with Redis\n */\nexport function rateLimit(options: RateLimitOptions) {\n const {\n max = 100,\n window = 60,\n keyGenerator = (c: Context) =>\n c.req.header('x-forwarded-for') ?? c.req.header('x-real-ip') ?? 'anonymous',\n message = 'Too many requests',\n } = options;\n\n return async (c: Context, next: Next) => {\n const key = keyGenerator(c);\n const now = Date.now();\n const windowMs = window * 1000;\n\n let record = store.get(key);\n\n if (!record || now > record.resetAt) {\n record = { count: 0, resetAt: now + windowMs };\n }\n\n record.count++;\n store.set(key, record);\n\n // Set rate limit headers\n c.header('X-RateLimit-Limit', String(max));\n c.header('X-RateLimit-Remaining', String(Math.max(0, max - record.count)));\n c.header('X-RateLimit-Reset', String(Math.ceil(record.resetAt / 1000)));\n\n if (record.count > max) {\n return c.json({ error: message }, 429);\n }\n\n await next();\n };\n}\n\n/**\n * Clear rate limit store (for testing)\n */\nexport function clearRateLimitStore() {\n store.clear();\n}\n","import type { Context, Next } from 'hono';\r\n\r\nexport class HttpError extends Error {\r\n constructor(\r\n public statusCode: number,\r\n message: string,\r\n public details?: unknown\r\n ) {\r\n super(message);\r\n this.name = 'HttpError';\r\n }\r\n}\r\n\r\nexport class BadRequestError extends HttpError {\r\n constructor(message = 'Bad Request', details?: unknown) {\r\n super(400, message, details);\r\n }\r\n}\r\n\r\nexport class UnauthorizedError extends HttpError {\r\n constructor(message = 'Unauthorized') {\r\n super(401, message);\r\n }\r\n}\r\n\r\nexport class ForbiddenError extends HttpError {\r\n constructor(message = 'Forbidden') {\r\n super(403, message);\r\n }\r\n}\r\n\r\nexport class NotFoundError extends HttpError {\r\n constructor(message = 'Not Found') {\r\n super(404, message);\r\n }\r\n}\r\n\r\nexport class ConflictError extends HttpError {\r\n constructor(message = 'Conflict', details?: unknown) {\r\n super(409, message, details);\r\n }\r\n}\r\n\r\nexport class InternalServerError extends HttpError {\r\n constructor(message = 'Internal Server Error') {\r\n super(500, message);\r\n }\r\n}\r\n\r\n/**\r\n * Global error handler middleware\r\n */\r\nexport function errorHandler() {\r\n return async (c: Context, next: Next) => {\r\n try {\r\n await next();\r\n } catch (err) {\r\n if (err instanceof HttpError) {\r\n return c.json({\r\n error: err.message,\r\n status: err.statusCode,\r\n ...(err.details ? { details: err.details } : {})\r\n }, err.statusCode as any);\r\n }\r\n\r\n // Unknown error\r\n console.error('Unhandled error:', err);\r\n return c.json({\r\n error: 'Internal Server Error',\r\n status: 500\r\n }, 500);\r\n }\r\n };\r\n}\r\n","import { readFile } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport type { Hono } from 'hono';\n\n// ============================================\n// MANIFEST TYPES\n// ============================================\n\nexport type ManifestHttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete';\n\n/**\n * A single route entry as written to routes-manifest.json\n */\nexport interface ManifestRoute {\n /** URL path, e.g. /users/:id */\n path: string;\n /** HTTP method (lowercase) */\n method: ManifestHttpMethod;\n /** Absolute or project-relative path to the handler file */\n handler: string;\n /** Named URL params extracted from the path, e.g. ['id'] */\n params: string[];\n /** Whether the handler module exports a body schema */\n hasBodySchema: boolean;\n /** Whether the handler module exports a query schema */\n hasQuerySchema: boolean;\n}\n\n/**\n * The shape of routes-manifest.json\n */\nexport interface RoutesManifest {\n version: number;\n generatedAt: string;\n routes: ManifestRoute[];\n}\n\n// ============================================\n// OPTIONS\n// ============================================\n\nexport interface FileSystemRoutingOptions {\n /**\n * Path to the routes-manifest.json file.\n * Defaults to `./routes-manifest.json` relative to cwd.\n */\n manifestPath?: string;\n\n /**\n * If true, log registered routes to stdout.\n * @default false\n */\n verbose?: boolean;\n\n /**\n * Called when the manifest is missing or unreadable.\n * Defaults to a silent no-op (backward-compatible behaviour).\n */\n onMissingManifest?: (reason: Error) => void;\n\n /**\n * Custom log function used when `verbose` is true.\n * Defaults to `console.log`.\n */\n logger?: (...args: unknown[]) => void;\n}\n\n// ============================================\n// INTERNAL HELPERS\n// ============================================\n\n/**\n * Read and parse routes-manifest.json.\n * Returns null when the file does not exist or is malformed.\n */\nasync function readManifest(\n manifestPath: string,\n onMissing: (err: Error) => void,\n): Promise<RoutesManifest | null> {\n try {\n const raw = await readFile(manifestPath, 'utf-8');\n return JSON.parse(raw) as RoutesManifest;\n } catch (err) {\n onMissing(err instanceof Error ? err : new Error(String(err)));\n return null;\n }\n}\n\n/**\n * Dynamically import a route handler module and return its default export.\n * Accepts both absolute paths and file:// URLs.\n */\nasync function importHandler(handlerPath: string): Promise<((...args: any[]) => any) | null> {\n try {\n const url = handlerPath.startsWith('file://')\n ? handlerPath\n : pathToFileURL(handlerPath).href;\n const mod = await import(url);\n if (typeof mod.default !== 'function') {\n console.warn(\n `[kozo:fsr] Skipping ${handlerPath}: no default export function`,\n );\n return null;\n }\n return mod.default as (...args: any[]) => any;\n } catch (err) {\n console.warn(\n `[kozo:fsr] Failed to import handler ${handlerPath}:`,\n (err as Error).message,\n );\n return null;\n }\n}\n\n// ============================================\n// MIDDLEWARE FACTORY\n// ============================================\n\n/**\n * Register all routes declared in `routes-manifest.json` onto a Hono app.\n *\n * This function is **not** a Hono middleware in the classical sense — it is an\n * *async initializer* that must be awaited before the server starts accepting\n * requests. Calling it early (before user-defined routes) guarantees that\n * manifest routes take precedence.\n *\n * @example\n * ```ts\n * import { Hono } from 'hono';\n * import { applyFileSystemRouting } from '@kozojs/core/middleware';\n *\n * const app = new Hono();\n * await applyFileSystemRouting(app, { manifestPath: './routes-manifest.json' });\n *\n * // User-defined routes registered AFTER are appended normally\n * app.get('/health', c => c.json({ ok: true }));\n * ```\n */\nexport async function applyFileSystemRouting(\n app: Hono<any>,\n options: FileSystemRoutingOptions = {},\n): Promise<void> {\n const {\n manifestPath = resolve(process.cwd(), 'routes-manifest.json'),\n verbose = false,\n onMissingManifest = () => {\n // Silent by default — backward-compatible\n },\n logger = console.log,\n } = options;\n\n const manifest = await readManifest(manifestPath, onMissingManifest);\n\n // Gracefully skip when no manifest exists\n if (!manifest) return;\n\n const log = logger;\n\n if (verbose) {\n log(\n `\\nšŸ“‹ [kozo:fsr] Loading ${manifest.routes.length} route(s) from manifest\\n`,\n );\n }\n\n for (const route of manifest.routes) {\n const handler = await importHandler(route.handler);\n if (!handler) continue;\n\n // Register on the Hono app using the correct HTTP method\n (app as any)[route.method](route.path, handler);\n\n if (verbose) {\n log(\n ` ${route.method.toUpperCase().padEnd(6)} ${route.path} → ${route.handler}`,\n );\n }\n }\n\n if (verbose) {\n log('');\n }\n}\n\n// ============================================\n// CONVENIENCE: createFileSystemRouting\n// ============================================\n\n/**\n * Alternative factory that returns an async function you can call with a Hono\n * app. Useful when you want to pre-configure options and apply them later.\n *\n * @example\n * ```ts\n * const fsr = createFileSystemRouting({ verbose: true });\n * await fsr(app);\n * ```\n */\nexport function createFileSystemRouting(options: FileSystemRoutingOptions = {}) {\n return (app: Hono<any>) => applyFileSystemRouting(app, options);\n}\n"],"mappings":"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","names":["logger"]}
1
+ {"version":3,"sources":["../../src/middleware/logger.ts","../../src/middleware/cors.ts","../../src/middleware/rate-limit.ts","../../src/middleware/error-handler.ts","../../src/middleware/fileSystemRouting.ts"],"sourcesContent":["import type { Context, Next } from 'hono';\r\n\r\nexport interface LoggerOptions {\r\n prefix?: string;\r\n colorize?: boolean;\r\n}\r\n\r\n/**\r\n * Request logger middleware\r\n */\r\nexport function logger(options: LoggerOptions = {}) {\r\n const { prefix = '🌐', colorize = true } = options;\r\n\r\n return async (c: Context, next: Next) => {\r\n const start = Date.now();\r\n const method = c.req.method;\r\n const path = new URL(c.req.url).pathname;\r\n\r\n await next();\r\n\r\n const duration = Date.now() - start;\r\n const status = c.res.status;\r\n\r\n const statusColor = status >= 500 ? 'šŸ”“' : status >= 400 ? '🟔' : '🟢';\r\n const log = `${prefix} ${method.padEnd(6)} ${path} ${statusColor} ${status} ${duration}ms`;\r\n \r\n console.log(log);\r\n };\r\n}\r\n","import { cors as honoCors } from 'hono/cors';\r\n\r\nexport interface CorsOptions {\r\n origin?: string | string[] | ((origin: string) => string | undefined | null);\r\n allowMethods?: string[];\r\n allowHeaders?: string[];\r\n exposeHeaders?: string[];\r\n maxAge?: number;\r\n credentials?: boolean;\r\n}\r\n\r\n/**\r\n * CORS middleware wrapper\r\n */\r\nexport function cors(options: CorsOptions = {}) {\r\n return honoCors({\r\n origin: options.origin || '*',\r\n allowMethods: options.allowMethods || ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],\r\n allowHeaders: options.allowHeaders || ['Content-Type', 'Authorization'],\r\n exposeHeaders: options.exposeHeaders || [],\r\n maxAge: options.maxAge || 86400,\r\n credentials: options.credentials || false\r\n });\r\n}\r\n","import type { Context, Next } from 'hono';\n\nexport interface RateLimitOptions {\n max: number;\n window: number; // in seconds\n keyGenerator?: (c: Context) => string;\n message?: string;\n}\n\n// In-memory store (use Redis in production)\nconst store = new Map<string, { count: number; resetAt: number }>();\n\n/**\n * Simple rate limiting middleware\n * For production, replace in-memory store with Redis\n */\nexport function rateLimit(options: RateLimitOptions) {\n const {\n max = 100,\n window = 60,\n keyGenerator = (c: Context) =>\n c.req.header('x-forwarded-for') ?? c.req.header('x-real-ip') ?? 'anonymous',\n message = 'Too many requests',\n } = options;\n\n return async (c: Context, next: Next) => {\n const key = keyGenerator(c);\n const now = Date.now();\n const windowMs = window * 1000;\n\n let record = store.get(key);\n\n if (!record || now > record.resetAt) {\n // Evict expired entries when the store grows too large to prevent unbounded memory growth\n if (store.size > 10_000) {\n for (const [k, v] of store) {\n if (now > v.resetAt) store.delete(k);\n }\n }\n record = { count: 0, resetAt: now + windowMs };\n }\n\n record.count++;\n store.set(key, record);\n\n // Set rate limit headers\n c.header('X-RateLimit-Limit', String(max));\n c.header('X-RateLimit-Remaining', String(Math.max(0, max - record.count)));\n c.header('X-RateLimit-Reset', String(Math.ceil(record.resetAt / 1000)));\n\n if (record.count > max) {\n return c.json({ error: message }, 429);\n }\n\n await next();\n };\n}\n\n/**\n * Clear rate limit store (for testing)\n */\nexport function clearRateLimitStore() {\n store.clear();\n}\n","import type { Context, Next } from 'hono';\r\n\r\nexport class HttpError extends Error {\r\n constructor(\r\n public statusCode: number,\r\n message: string,\r\n public details?: unknown\r\n ) {\r\n super(message);\r\n this.name = 'HttpError';\r\n }\r\n}\r\n\r\nexport class BadRequestError extends HttpError {\r\n constructor(message = 'Bad Request', details?: unknown) {\r\n super(400, message, details);\r\n }\r\n}\r\n\r\nexport class UnauthorizedError extends HttpError {\r\n constructor(message = 'Unauthorized') {\r\n super(401, message);\r\n }\r\n}\r\n\r\nexport class ForbiddenError extends HttpError {\r\n constructor(message = 'Forbidden') {\r\n super(403, message);\r\n }\r\n}\r\n\r\nexport class NotFoundError extends HttpError {\r\n constructor(message = 'Not Found') {\r\n super(404, message);\r\n }\r\n}\r\n\r\nexport class ConflictError extends HttpError {\r\n constructor(message = 'Conflict', details?: unknown) {\r\n super(409, message, details);\r\n }\r\n}\r\n\r\nexport class InternalServerError extends HttpError {\r\n constructor(message = 'Internal Server Error') {\r\n super(500, message);\r\n }\r\n}\r\n\r\n/**\r\n * Global error handler middleware\r\n */\r\nexport function errorHandler() {\r\n return async (c: Context, next: Next) => {\r\n try {\r\n await next();\r\n } catch (err) {\r\n if (err instanceof HttpError) {\r\n return c.json({\r\n error: err.message,\r\n status: err.statusCode,\r\n ...(err.details ? { details: err.details } : {})\r\n }, err.statusCode as any);\r\n }\r\n\r\n // Unknown error\r\n console.error('Unhandled error:', err);\r\n return c.json({\r\n error: 'Internal Server Error',\r\n status: 500\r\n }, 500);\r\n }\r\n };\r\n}\r\n","import { readFile } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport type { Hono } from 'hono';\n\n// ============================================\n// MANIFEST TYPES\n// ============================================\n\nexport type ManifestHttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete';\n\n/**\n * A single route entry as written to routes-manifest.json\n */\nexport interface ManifestRoute {\n /** URL path, e.g. /users/:id */\n path: string;\n /** HTTP method (lowercase) */\n method: ManifestHttpMethod;\n /** Absolute or project-relative path to the handler file */\n handler: string;\n /** Named URL params extracted from the path, e.g. ['id'] */\n params: string[];\n /** Whether the handler module exports a body schema */\n hasBodySchema: boolean;\n /** Whether the handler module exports a query schema */\n hasQuerySchema: boolean;\n}\n\n/**\n * The shape of routes-manifest.json\n */\nexport interface RoutesManifest {\n version: number;\n generatedAt: string;\n routes: ManifestRoute[];\n}\n\n// ============================================\n// OPTIONS\n// ============================================\n\nexport interface FileSystemRoutingOptions {\n /**\n * Path to the routes-manifest.json file.\n * Defaults to `./routes-manifest.json` relative to cwd.\n */\n manifestPath?: string;\n\n /**\n * If true, log registered routes to stdout.\n * @default false\n */\n verbose?: boolean;\n\n /**\n * Called when the manifest is missing or unreadable.\n * Defaults to a silent no-op (backward-compatible behaviour).\n */\n onMissingManifest?: (reason: Error) => void;\n\n /**\n * Custom log function used when `verbose` is true.\n * Defaults to `console.log`.\n */\n logger?: (...args: unknown[]) => void;\n}\n\n// ============================================\n// INTERNAL HELPERS\n// ============================================\n\n/**\n * Read and parse routes-manifest.json.\n * Returns null when the file does not exist or is malformed.\n */\nasync function readManifest(\n manifestPath: string,\n onMissing: (err: Error) => void,\n): Promise<RoutesManifest | null> {\n try {\n const raw = await readFile(manifestPath, 'utf-8');\n return JSON.parse(raw) as RoutesManifest;\n } catch (err) {\n onMissing(err instanceof Error ? err : new Error(String(err)));\n return null;\n }\n}\n\n/**\n * Dynamically import a route handler module and return its default export.\n * Accepts both absolute paths and file:// URLs.\n */\nasync function importHandler(handlerPath: string): Promise<((...args: any[]) => any) | null> {\n try {\n const url = handlerPath.startsWith('file://')\n ? handlerPath\n : pathToFileURL(handlerPath).href;\n const mod = await import(url);\n if (typeof mod.default !== 'function') {\n console.warn(\n `[kozo:fsr] Skipping ${handlerPath}: no default export function`,\n );\n return null;\n }\n return mod.default as (...args: any[]) => any;\n } catch (err) {\n console.warn(\n `[kozo:fsr] Failed to import handler ${handlerPath}:`,\n (err as Error).message,\n );\n return null;\n }\n}\n\n// ============================================\n// MIDDLEWARE FACTORY\n// ============================================\n\n/**\n * Register all routes declared in `routes-manifest.json` onto a Hono app.\n *\n * This function is **not** a Hono middleware in the classical sense — it is an\n * *async initializer* that must be awaited before the server starts accepting\n * requests. Calling it early (before user-defined routes) guarantees that\n * manifest routes take precedence.\n *\n * @example\n * ```ts\n * import { Hono } from 'hono';\n * import { applyFileSystemRouting } from '@kozojs/core/middleware';\n *\n * const app = new Hono();\n * await applyFileSystemRouting(app, { manifestPath: './routes-manifest.json' });\n *\n * // User-defined routes registered AFTER are appended normally\n * app.get('/health', c => c.json({ ok: true }));\n * ```\n */\nexport async function applyFileSystemRouting(\n app: Hono<any>,\n options: FileSystemRoutingOptions = {},\n): Promise<void> {\n const {\n manifestPath = resolve(process.cwd(), 'routes-manifest.json'),\n verbose = false,\n onMissingManifest = () => {\n // Silent by default — backward-compatible\n },\n logger = console.log,\n } = options;\n\n const manifest = await readManifest(manifestPath, onMissingManifest);\n\n // Gracefully skip when no manifest exists\n if (!manifest) return;\n\n const log = logger;\n\n if (verbose) {\n log(\n `\\nšŸ“‹ [kozo:fsr] Loading ${manifest.routes.length} route(s) from manifest\\n`,\n );\n }\n\n for (const route of manifest.routes) {\n const handler = await importHandler(route.handler);\n if (!handler) continue;\n\n // Register on the Hono app using the correct HTTP method\n (app as any)[route.method](route.path, handler);\n\n if (verbose) {\n log(\n ` ${route.method.toUpperCase().padEnd(6)} ${route.path} → ${route.handler}`,\n );\n }\n }\n\n if (verbose) {\n log('');\n }\n}\n\n// ============================================\n// CONVENIENCE: createFileSystemRouting\n// ============================================\n\n/**\n * Alternative factory that returns an async function you can call with a Hono\n * app. Useful when you want to pre-configure options and apply them later.\n *\n * @example\n * ```ts\n * const fsr = createFileSystemRouting({ verbose: true });\n * await fsr(app);\n * ```\n */\nexport function createFileSystemRouting(options: FileSystemRoutingOptions = {}) {\n return (app: Hono<any>) => applyFileSystemRouting(app, options);\n}\n"],"mappings":"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","names":["logger"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kozojs/core",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "High-performance TypeScript framework with type-safe client generation",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",