@fishka/express 0.9.5

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 (38) hide show
  1. package/README.md +137 -0
  2. package/dist/cjs/api.types.js +38 -0
  3. package/dist/cjs/auth/auth-strategy.js +62 -0
  4. package/dist/cjs/auth/auth.types.js +2 -0
  5. package/dist/cjs/auth/auth.utils.js +64 -0
  6. package/dist/cjs/auth/bearer-auth-strategy.js +59 -0
  7. package/dist/cjs/config.js +29 -0
  8. package/dist/cjs/error-handling.js +70 -0
  9. package/dist/cjs/http.types.js +38 -0
  10. package/dist/cjs/index.js +31 -0
  11. package/dist/cjs/rate-limit/in-memory-rate-limiter.js +88 -0
  12. package/dist/cjs/rate-limit/rate-limit.js +34 -0
  13. package/dist/cjs/rate-limit/rate-limit.types.js +2 -0
  14. package/dist/cjs/route-table.js +45 -0
  15. package/dist/cjs/router.js +245 -0
  16. package/dist/cjs/thread-local/thread-local-storage-middleware.js +27 -0
  17. package/dist/cjs/thread-local/thread-local-storage.js +31 -0
  18. package/dist/cjs/utils/conversion.utils.js +26 -0
  19. package/dist/cjs/utils/express.utils.js +2 -0
  20. package/dist/esm/api.types.js +31 -0
  21. package/dist/esm/auth/auth-strategy.js +58 -0
  22. package/dist/esm/auth/auth.types.js +1 -0
  23. package/dist/esm/auth/auth.utils.js +59 -0
  24. package/dist/esm/auth/bearer-auth-strategy.js +55 -0
  25. package/dist/esm/config.js +24 -0
  26. package/dist/esm/error-handling.js +66 -0
  27. package/dist/esm/http.types.js +35 -0
  28. package/dist/esm/index.js +15 -0
  29. package/dist/esm/rate-limit/in-memory-rate-limiter.js +88 -0
  30. package/dist/esm/rate-limit/rate-limit.js +30 -0
  31. package/dist/esm/rate-limit/rate-limit.types.js +1 -0
  32. package/dist/esm/route-table.js +40 -0
  33. package/dist/esm/router.js +203 -0
  34. package/dist/esm/thread-local/thread-local-storage-middleware.js +24 -0
  35. package/dist/esm/thread-local/thread-local-storage.js +27 -0
  36. package/dist/esm/utils/conversion.utils.js +22 -0
  37. package/dist/esm/utils/express.utils.js +1 -0
  38. package/package.json +70 -0
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Common HTTP status codes as numbers.
3
+ * @Internal
4
+ */
5
+ export const INTERNAL_ERROR_STATUS = 500;
6
+ /**
7
+ * Common HTTP status codes as numbers.
8
+ * @Internal
9
+ */
10
+ export const BAD_REQUEST_STATUS = 400;
11
+ /**
12
+ * Common HTTP status codes as numbers.
13
+ * @Internal
14
+ */
15
+ export const UNAUTHORIZED_STATUS = 401;
16
+ /**
17
+ * Common HTTP status codes as numbers.
18
+ * @Internal
19
+ */
20
+ export const FORBIDDEN_STATUS = 403;
21
+ /**
22
+ * Common HTTP status codes as numbers.
23
+ * @Internal
24
+ */
25
+ export const NOT_FOUND_STATUS = 404;
26
+ /**
27
+ * Common HTTP status codes as numbers.
28
+ * @Internal
29
+ */
30
+ export const OK_STATUS = 200;
31
+ /**
32
+ * Common HTTP status codes as numbers.
33
+ * @Internal
34
+ */
35
+ export const TOO_MANY_REQUESTS_STATUS = 429;
@@ -0,0 +1,15 @@
1
+ export * from './api.types';
2
+ export * from './auth/auth-strategy';
3
+ export * from './auth/auth.types';
4
+ export * from './auth/auth.utils';
5
+ export * from './auth/bearer-auth-strategy';
6
+ export * from './config';
7
+ export * from './error-handling';
8
+ export * from './http.types';
9
+ export * from './rate-limit/in-memory-rate-limiter';
10
+ export * from './rate-limit/rate-limit.types';
11
+ export * from './route-table';
12
+ export * from './router';
13
+ export * from './thread-local/thread-local-storage';
14
+ export * from './thread-local/thread-local-storage-middleware';
15
+ export * from './utils/express.utils';
@@ -0,0 +1,88 @@
1
+ import { addRateLimitHeaders, msToSeconds } from './rate-limit';
2
+ const MILLIS_PER_SECOND = 1000;
3
+ /**
4
+ * In-memory rate limiter using sliding window counter.
5
+ * Tracks request counts per key with time-based window expiration.
6
+ */
7
+ class InMemoryRateLimiter {
8
+ constructor(points, durationSeconds) {
9
+ this.limits = new Map();
10
+ this.points = points;
11
+ this.durationMs = durationSeconds * MILLIS_PER_SECOND;
12
+ }
13
+ /**
14
+ * Try to consume points from the rate limit.
15
+ * Returns result if successful, throws if limit exceeded.
16
+ *
17
+ * @param key - Unique identifier for the client (e.g., IP address)
18
+ * @returns Rate limit result with remaining points and ms until reset
19
+ * @throws RateLimitResult if limit exceeded
20
+ */
21
+ consume(key) {
22
+ const now = Date.now();
23
+ const existing = this.limits.get(key);
24
+ // Reset expired entry
25
+ if (existing && now >= existing.resetTime) {
26
+ this.limits.delete(key);
27
+ }
28
+ const current = this.limits.get(key) || { count: 0, resetTime: now + this.durationMs };
29
+ if (current.count >= this.points) {
30
+ const msBeforeNext = Math.max(0, current.resetTime - now);
31
+ throw {
32
+ remainingPoints: 0,
33
+ msBeforeNext,
34
+ };
35
+ }
36
+ current.count++;
37
+ this.limits.set(key, current);
38
+ const msBeforeNext = Math.max(0, current.resetTime - now);
39
+ return {
40
+ remainingPoints: this.points - current.count,
41
+ msBeforeNext,
42
+ };
43
+ }
44
+ }
45
+ /**
46
+ * Creates a rate limiter middleware using in-memory implementation.
47
+ *
48
+ * Separate limiters are used for read (GET) and write (POST/PATCH/PUT/DELETE) requests.
49
+ *
50
+ * @param config - Rate limit configuration
51
+ * @returns Express middleware function
52
+ */
53
+ export async function createRateLimiterMiddleware(config) {
54
+ const readLimiter = new InMemoryRateLimiter(config.points.read, config.duration);
55
+ const writeLimiter = new InMemoryRateLimiter(config.points.write, config.duration);
56
+ const whitelist = config.rateLimitWhitelist || ['/v1', '/health'];
57
+ /**
58
+ * The actual middleware function.
59
+ */
60
+ return async (req, res, next) => {
61
+ // Check if the path is whitelisted
62
+ if (whitelist.some(path => req.path.includes(path))) {
63
+ next();
64
+ return;
65
+ }
66
+ const isReadRequest = req.method === 'GET';
67
+ const limiter = isReadRequest ? readLimiter : writeLimiter;
68
+ const limitPoints = isReadRequest ? config.points.read : config.points.write;
69
+ const clientKey = req.ip || 'unknown';
70
+ try {
71
+ const result = limiter.consume(clientKey);
72
+ addRateLimitHeaders(res, result, limitPoints, config.duration);
73
+ next();
74
+ }
75
+ catch (error) {
76
+ const result = error;
77
+ addRateLimitHeaders(res, result, limitPoints, config.duration)
78
+ .header('Retry-After', `${msToSeconds(result.msBeforeNext)}`)
79
+ .status(429)
80
+ .send({ error: 'Too Many Requests' });
81
+ }
82
+ };
83
+ }
84
+ /**
85
+ * Export the in-memory limiter class for advanced use cases where
86
+ * you might want direct control over rate limit management.
87
+ */
88
+ export { InMemoryRateLimiter };
@@ -0,0 +1,30 @@
1
+ const MILLIS_PER_SECOND = 1000;
2
+ /**
3
+ * Adds rate limit state headers to the response.
4
+ * See https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/
5
+ *
6
+ * Headers added:
7
+ * - X-RateLimit-Limit: Server's quota for requests in the time window
8
+ * - X-RateLimit-Remaining: Remaining quota in the current window
9
+ * - X-RateLimit-Reset: Time remaining in the current window (in seconds)
10
+ * - X-RateLimit-Policy: Quota policies associated with the client
11
+ * @Internal
12
+ */
13
+ export function addRateLimitHeaders(res, result, limitPoints, duration) {
14
+ return (res
15
+ // The server's quota for requests by the client in the time window.
16
+ .header('X-RateLimit-Limit', `${limitPoints}`)
17
+ // The remaining quota in the current window.
18
+ .header('X-RateLimit-Remaining', `${result.remainingPoints}`)
19
+ // The time remaining in the current window specified in seconds.
20
+ .header('X-RateLimit-Reset', `${Math.ceil(result.msBeforeNext / MILLIS_PER_SECOND)}`)
21
+ // Indicates the quota policies currently associated with the client.
22
+ .header('X-RateLimit-Policy', `${limitPoints};w=${duration};comment="fixed window"`));
23
+ }
24
+ /**
25
+ * Converts milliseconds to seconds, rounding up.
26
+ * @Internal
27
+ */
28
+ export function msToSeconds(ms) {
29
+ return Math.ceil(ms / MILLIS_PER_SECOND);
30
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ import { mountDelete, mountGet, mountPatch, mountPost, mountPut, } from './router';
2
+ /**
3
+ * Helper utility for organizing and mounting routes.
4
+ * Provides a fluent interface for registering multiple handlers.
5
+ */
6
+ export class RouteTable {
7
+ constructor(app) {
8
+ this.app = app;
9
+ }
10
+ get(path, endpointOrRun) {
11
+ const endpoint = typeof endpointOrRun === 'function' ? { run: endpointOrRun } : endpointOrRun;
12
+ mountGet(this.app, path, endpoint);
13
+ return this;
14
+ }
15
+ post(path, endpoint) {
16
+ mountPost(this.app, path, endpoint);
17
+ return this;
18
+ }
19
+ patch(path, endpoint) {
20
+ mountPatch(this.app, path, endpoint);
21
+ return this;
22
+ }
23
+ put(path, endpoint) {
24
+ mountPut(this.app, path, endpoint);
25
+ return this;
26
+ }
27
+ delete(path, endpointOrRun) {
28
+ const endpoint = typeof endpointOrRun === 'function' ? { run: endpointOrRun } : endpointOrRun;
29
+ mountDelete(this.app, path, endpoint);
30
+ return this;
31
+ }
32
+ }
33
+ /**
34
+ * Factory function to create a new route table.
35
+ * @param app Express application instance
36
+ * @returns RouteTable instance with fluent API
37
+ */
38
+ export function createRouteTable(app) {
39
+ return new RouteTable(app);
40
+ }
@@ -0,0 +1,203 @@
1
+ import { assertTruthy, callValueAssertion, getMessageFromError, validateObject, } from '@fishka/assertions';
2
+ import * as url from 'url';
3
+ import { HttpError, URL_PARAMETER_INFO } from './api.types';
4
+ import { BAD_REQUEST_STATUS, OK_STATUS } from './http.types';
5
+ import { catchRouteErrors } from './error-handling';
6
+ import { getRequestLocalStorage } from './thread-local/thread-local-storage';
7
+ import { wrapAsApiResponse } from './utils/conversion.utils';
8
+ // ============================================================================
9
+ // Internal implementation details
10
+ // ============================================================================
11
+ /**
12
+ * Registers a GET route.
13
+ */
14
+ export const mountGet = (app, path, endpoint) => mount(app, { method: 'get', route: endpoint, path });
15
+ /**
16
+ * Registers a POST route.
17
+ */
18
+ export const mountPost = (app, path, endpoint) => mount(app, { method: 'post', route: endpoint, path });
19
+ /**
20
+ * Registers a PATCH route.
21
+ */
22
+ export const mountPatch = (app, path, endpoint) => mount(app, { method: 'patch', route: endpoint, path });
23
+ /**
24
+ * Registers a PUT route.
25
+ */
26
+ export const mountPut = (app, path, endpoint) => mount(app, { method: 'put', route: endpoint, path });
27
+ /**
28
+ * Registers a DELETE route.
29
+ */
30
+ export const mountDelete = (app, path, endpoint) => mount(app, { method: 'delete', route: endpoint, path });
31
+ /**
32
+ * Mounts a route with the given method, endpoint, and path.
33
+ */
34
+ export function mount(app, { method, route, path }) {
35
+ const fullPath = `/${path}`;
36
+ const handler = createRouteHandler(method, route);
37
+ app[method](fullPath, catchRouteErrors(handler));
38
+ }
39
+ /**
40
+ * @Internal
41
+ * Creates a route handler from an endpoint definition.
42
+ */
43
+ function createRouteHandler(method, endpoint) {
44
+ return async (req, res, _next) => {
45
+ let result;
46
+ switch (method) {
47
+ case 'post':
48
+ case 'put':
49
+ case 'patch':
50
+ result = await executeBodyEndpoint(endpoint, req, res);
51
+ break;
52
+ case 'delete':
53
+ result = await executeDeleteEndpoint(endpoint, req, res);
54
+ break;
55
+ case 'get':
56
+ result = await executeGetEndpoint(endpoint, req, res);
57
+ break;
58
+ }
59
+ const response = wrapAsApiResponse(result);
60
+ const tls = getRequestLocalStorage();
61
+ if (tls?.requestId) {
62
+ response.requestId = tls.requestId;
63
+ }
64
+ response.status = response.status || OK_STATUS;
65
+ res.status(response.status);
66
+ res.send(response);
67
+ };
68
+ }
69
+ /**
70
+ * @Internal
71
+ * Validates request parameters using custom validators.
72
+ */
73
+ function validateUrlParameters(req, { $path, $query, }) {
74
+ try {
75
+ for (const key in req.params) {
76
+ const value = req.params[key];
77
+ // Run Global Validation if registered.
78
+ const globalValidator = URL_PARAMETER_INFO[key]?.validator;
79
+ if (globalValidator) {
80
+ callValueAssertion(value, globalValidator, `${BAD_REQUEST_STATUS}`);
81
+ }
82
+ // Run Local Validation.
83
+ const validator = $path?.[key];
84
+ if (validator) {
85
+ callValueAssertion(value, validator, `${BAD_REQUEST_STATUS}`);
86
+ }
87
+ }
88
+ const parsedUrl = url.parse(req.url, true);
89
+ for (const key in parsedUrl.query) {
90
+ const value = parsedUrl.query[key];
91
+ // Global Validation if registered (also applies to query params if names match).
92
+ const globalValidator = URL_PARAMETER_INFO[key]?.validator;
93
+ if (globalValidator) {
94
+ // Query params can be string | string[] | undefined. Global validators usually expect string.
95
+ // We only validate if it's a single value or handle array in validator.
96
+ // For simplicity, we pass value as is (unknown) to assertion.
97
+ callValueAssertion(value, globalValidator, `${BAD_REQUEST_STATUS}`);
98
+ }
99
+ const validator = $query?.[key];
100
+ if (validator) {
101
+ callValueAssertion(value, validator, `${BAD_REQUEST_STATUS}`);
102
+ }
103
+ }
104
+ }
105
+ catch (error) {
106
+ throw new HttpError(BAD_REQUEST_STATUS, getMessageFromError(error));
107
+ }
108
+ }
109
+ /**
110
+ * @Internal
111
+ * Runs GET handler with optional middleware.
112
+ */
113
+ async function executeGetEndpoint(route, req, res) {
114
+ const requestContext = newRequestContext(undefined, req, res);
115
+ validateUrlParameters(req, { $path: route.$path, $query: route.$query });
116
+ return await executeWithMiddleware(() => route.run(requestContext), route.middlewares || [], requestContext);
117
+ }
118
+ /**
119
+ * @Internal
120
+ * Runs DELETE handler with optional middleware.
121
+ */
122
+ async function executeDeleteEndpoint(route, req, res) {
123
+ const requestContext = newRequestContext(undefined, req, res);
124
+ validateUrlParameters(req, { $path: route.$path, $query: route.$query });
125
+ await executeWithMiddleware(() => route.run(requestContext), route.middlewares || [], requestContext);
126
+ return undefined;
127
+ }
128
+ /**
129
+ * @Internal
130
+ * Runs POST/PUT/PATCH handler with optional middleware.
131
+ */
132
+ async function executeBodyEndpoint(route, req, res) {
133
+ const validator = route.$body;
134
+ const apiRequest = req.body;
135
+ try {
136
+ // Handle validation based on whether validator is an object or function
137
+ if (typeof validator === 'function') {
138
+ // It's a ValueAssertion (function)
139
+ callValueAssertion(apiRequest, validator, `${BAD_REQUEST_STATUS}: request body`);
140
+ }
141
+ else {
142
+ // It's an ObjectAssertion - use validateObject
143
+ // We strictly assume it is an object because of the type definition (function | object)
144
+ const objectValidator = validator;
145
+ const isEmptyValidator = Object.keys(objectValidator).length === 0;
146
+ const error = validateObject(apiRequest, objectValidator, `${BAD_REQUEST_STATUS}: request body`, {
147
+ failOnUnknownFields: !isEmptyValidator,
148
+ });
149
+ assertTruthy(!error, error);
150
+ }
151
+ }
152
+ catch (error) {
153
+ if (error instanceof HttpError)
154
+ throw error;
155
+ throw new HttpError(BAD_REQUEST_STATUS, getMessageFromError(error));
156
+ }
157
+ const requestContext = newRequestContext(apiRequest, req, res);
158
+ validateUrlParameters(req, { $path: route.$path, $query: route.$query });
159
+ requestContext.body = req.body;
160
+ return await executeWithMiddleware(() => route.run(requestContext), (route.middlewares || []), requestContext);
161
+ }
162
+ /**
163
+ * @Internal
164
+ * Executes handler with middleware chain.
165
+ */
166
+ async function executeWithMiddleware(run, middlewares, context) {
167
+ const current = async (index) => {
168
+ if (index >= middlewares.length) {
169
+ const result = await run();
170
+ return wrapAsApiResponse(result);
171
+ }
172
+ const middleware = middlewares[index];
173
+ return (await middleware(() => current(index + 1), context));
174
+ };
175
+ return await current(0);
176
+ }
177
+ /**
178
+ * @Internal
179
+ * Creates a new RequestContext instance.
180
+ */
181
+ function newRequestContext(requestBody, req, res) {
182
+ return {
183
+ body: requestBody,
184
+ req,
185
+ res,
186
+ params: {
187
+ get: (key) => {
188
+ const value = req.params[key];
189
+ assertTruthy(value, `Path parameter '${key}' not found`);
190
+ return value;
191
+ },
192
+ tryGet: (key) => req.params[key],
193
+ },
194
+ query: {
195
+ get: (key) => {
196
+ const parsedUrl = url.parse(req.url, true);
197
+ const value = parsedUrl.query[key];
198
+ return Array.isArray(value) ? value[0] : value;
199
+ },
200
+ },
201
+ state: new Map(),
202
+ };
203
+ }
@@ -0,0 +1,24 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { getExpressApiConfig } from '../config';
3
+ import { runWithRequestTlsData } from './thread-local-storage';
4
+ /**
5
+ * Creates middleware that initializes thread-local storage for each request.
6
+ * Automatically generates a unique request ID and makes it available throughout
7
+ * the request lifecycle.
8
+ *
9
+ * @returns Express middleware function
10
+ */
11
+ export function createTlsMiddleware() {
12
+ return async (req, _res, next) => {
13
+ const config = getExpressApiConfig();
14
+ const headerId = config.trustRequestIdHeader ? req.headers['x-request-id'] : undefined;
15
+ const existingId = req.requestId || headerId;
16
+ const requestId = typeof existingId === 'string' ? existingId : randomUUID();
17
+ // Run the next handler within the TLS context
18
+ await runWithRequestTlsData({
19
+ requestId,
20
+ }, async () => {
21
+ next();
22
+ });
23
+ };
24
+ }
@@ -0,0 +1,27 @@
1
+ import { AsyncLocalStorage } from 'async_hooks';
2
+ /**
3
+ * AsyncLocalStorage instance for managing per-request context.
4
+ * This ensures that each async operation associated with a request
5
+ * can access the request-specific data even across async boundaries.
6
+ * @Internal
7
+ */
8
+ const asyncLocalStorage = new AsyncLocalStorage();
9
+ /**
10
+ * Gets all thread-local data for the current request context.
11
+ * Returns undefined if called outside an async context managed by API.
12
+ * @Internal
13
+ */
14
+ export function getRequestLocalStorage() {
15
+ return asyncLocalStorage.getStore();
16
+ }
17
+ /**
18
+ * Executes a callback within a request context with the given thread-local data.
19
+ * Used by middleware to set up the context for handlers.
20
+ * @Internal
21
+ * @param data - Thread-local data to establish
22
+ * @param callback - Function to execute within the context
23
+ * @returns Result of the callback
24
+ */
25
+ export async function runWithRequestTlsData(data, callback) {
26
+ return asyncLocalStorage.run(data, callback);
27
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Converts JS timestamp or date to ISO 8601 format (without milliseconds).
3
+ * Example: "2012-07-20T01:19:13Z".
4
+ * @Internal
5
+ */
6
+ export function toApiDateString(value) {
7
+ const resultWithMillis = (typeof value === 'number' ? new Date(value) : value).toISOString();
8
+ return `${resultWithMillis.substring(0, resultWithMillis.length - 5)}Z`;
9
+ }
10
+ /**
11
+ * Wraps the response into the correct API form.
12
+ * Add necessary fields, like 'requestId'.
13
+ * If the response is already in the correct form, returns it as-is.
14
+ * @Internal
15
+ */
16
+ export function wrapAsApiResponse(apiResponseOrResultValue) {
17
+ let apiResponse = apiResponseOrResultValue;
18
+ apiResponse = apiResponse?.result
19
+ ? apiResponse // The value is in the correct 'ApiResponse' form: just return it.
20
+ : { result: apiResponseOrResultValue }; // Wrap the raw value into the correct ApiResponse form.
21
+ return apiResponse;
22
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@fishka/express",
3
+ "version": "0.9.5",
4
+ "description": "Express.js extension with built-in validation, and type safety",
5
+ "main": "./dist/cjs/index.js",
6
+ "module": "./dist/esm/index.js",
7
+ "types": "./dist/esm/index.d.ts",
8
+ "private": false,
9
+ "license": "MIT",
10
+ "sideEffects": false,
11
+ "scripts": {
12
+ "prebuild": "npm run clean",
13
+ "build:esm": "tsc -p tsconfig.esm.json",
14
+ "build:cjs": "tsc -p tsconfig.cjs.json",
15
+ "build": "npm run build:esm && npm run build:cjs",
16
+ "clean": "rimraf ./dist",
17
+ "format": "prettier --write \"./**/*.{ts,js,md,json}\"",
18
+ "lint": "eslint .",
19
+ "test": "jest",
20
+ "typecheck": "tsc --noEmit",
21
+ "release": "npm run lint && npm run test && npm run build && npm publish"
22
+ },
23
+ "keywords": [
24
+ "express",
25
+ "expressjs",
26
+ "typescript",
27
+ "rest-api",
28
+ "rest"
29
+ ],
30
+ "dependencies": {
31
+ "@fishka/assertions": "^1.0.0",
32
+ "express": "^5.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/express": "^5.0.6",
36
+ "@types/jest": "^30.0.0",
37
+ "@types/node": "^20.19.27",
38
+ "@typescript-eslint/eslint-plugin": "^8.51.0",
39
+ "@typescript-eslint/parser": "^8.51.0",
40
+ "eslint": "^9.39.2",
41
+ "eslint-plugin-unused-imports": "^4.3.0",
42
+ "jest": "^30.2.0",
43
+ "prettier": "^3.7.4",
44
+ "prettier-plugin-organize-imports": "^4.3.0",
45
+ "rimraf": "^6.1.2",
46
+ "ts-jest": "^29.4.6",
47
+ "typescript": "^5.9.3"
48
+ },
49
+ "files": [
50
+ "dist"
51
+ ],
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "https://gitea.com/fishka/express.git"
55
+ },
56
+ "exports": {
57
+ ".": {
58
+ "types": "./dist/esm/index.d.ts",
59
+ "import": "./dist/esm/index.js",
60
+ "require": "./dist/cjs/index.js"
61
+ }
62
+ },
63
+ "publishConfig": {
64
+ "access": "public"
65
+ },
66
+ "engines": {
67
+ "node": ">=20.0.0",
68
+ "npm": ">=9.0.0"
69
+ }
70
+ }