@hemia/core 0.0.12 → 0.0.14
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.
package/dist/hemia-core.esm.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
2
|
import express, { Router } from 'express';
|
|
3
|
-
import { METADATA_KEYS, ParamType, ApiResponse, isRedirectResponse, ControllerRegistry } from '@hemia/common';
|
|
3
|
+
import { METADATA_KEYS, ParamType, ApiResponse, isRedirectResponse, HttpError, ControllerRegistry, CustomHttpError } from '@hemia/common';
|
|
4
4
|
import { TRACE_METADATA_KEY } from '@hemia/trace-manager';
|
|
5
5
|
import { traceMiddleware } from '@hemia/app-context';
|
|
6
6
|
import { plainToInstance } from 'class-transformer';
|
|
7
7
|
import multer from 'multer';
|
|
8
8
|
import { injectable, inject } from 'inversify';
|
|
9
|
-
import { AUTH_SERVICE_ID, AuthService } from '@hemia/auth-sdk';
|
|
9
|
+
import { AUTH_SERVICE_ID, AuthService, OAuthService } from '@hemia/auth-sdk';
|
|
10
10
|
|
|
11
11
|
class GuardsConsumer {
|
|
12
12
|
/**
|
|
@@ -227,7 +227,9 @@ async function registerRoutes(app, container, controllerIdentifiers, onTraceFini
|
|
|
227
227
|
console.error(`Error en ${route.method.toUpperCase()} ${basePath}${route.path}:`, error);
|
|
228
228
|
const status = error.statusCode || error.status || 500;
|
|
229
229
|
const message = error.message || 'Internal Server Error';
|
|
230
|
-
const
|
|
230
|
+
const errorDetail = error instanceof HttpError ? error.error : undefined;
|
|
231
|
+
const isDev = process.env.NODE_ENV === 'development';
|
|
232
|
+
const errorResponse = ApiResponse.error(message, errorDetail || (isDev ? error.stack : undefined), status);
|
|
231
233
|
const { status: errStatus, ...errorData } = errorResponse;
|
|
232
234
|
res.status(errStatus).json(errorData);
|
|
233
235
|
}
|
|
@@ -338,9 +340,9 @@ class AppFactory {
|
|
|
338
340
|
const defaultHeaders = {
|
|
339
341
|
'Access-Control-Allow-Origin': req.headers.origin || '*',
|
|
340
342
|
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,PATCH,OPTIONS',
|
|
341
|
-
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Api-Key, Language, X-No-Cookies, x-session',
|
|
343
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Api-Key, Language, X-No-Cookies, x-session, x-client-id, X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After',
|
|
342
344
|
'Access-Control-Allow-Credentials': 'true',
|
|
343
|
-
'Access-Control-Expose-Headers': 'Authorization, X-Correlation-Id, X-Trace-Id, x-session',
|
|
345
|
+
'Access-Control-Expose-Headers': 'Authorization, X-Correlation-Id, X-Trace-Id, x-session, X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After',
|
|
344
346
|
};
|
|
345
347
|
const headers = { ...defaultHeaders, ...(options.corsHeaders || {}) };
|
|
346
348
|
Object.entries(headers).forEach(([key, value]) => {
|
|
@@ -525,4 +527,112 @@ JWTGuard = __decorate([
|
|
|
525
527
|
__metadata("design:paramtypes", [AuthService])
|
|
526
528
|
], JWTGuard);
|
|
527
529
|
|
|
528
|
-
|
|
530
|
+
let PublicClientGuard = class PublicClientGuard {
|
|
531
|
+
constructor(oauthService) {
|
|
532
|
+
this.oauthService = oauthService;
|
|
533
|
+
}
|
|
534
|
+
async canActivate(context) {
|
|
535
|
+
const request = context.switchToHttp().getRequest();
|
|
536
|
+
const clientId = request.headers['x-client-id'] || request.body.client_id;
|
|
537
|
+
if (!clientId) {
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
const client = await this.oauthService.getByClientId(clientId);
|
|
541
|
+
if (!client || !client.isActive) {
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
request.oauthClient = client;
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
PublicClientGuard = __decorate([
|
|
549
|
+
injectable(),
|
|
550
|
+
__param(0, inject(OAuthService)),
|
|
551
|
+
__metadata("design:paramtypes", [OAuthService])
|
|
552
|
+
], PublicClientGuard);
|
|
553
|
+
|
|
554
|
+
var ThrottleGuard_1;
|
|
555
|
+
let ThrottleGuard = ThrottleGuard_1 = class ThrottleGuard {
|
|
556
|
+
constructor() {
|
|
557
|
+
if (!ThrottleGuard_1.cleanupInterval) {
|
|
558
|
+
ThrottleGuard_1.cleanupInterval = setInterval(() => {
|
|
559
|
+
this.cleanup();
|
|
560
|
+
}, 60000);
|
|
561
|
+
ThrottleGuard_1.cleanupInterval.unref();
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
async canActivate(context) {
|
|
565
|
+
const request = context.switchToHttp().getRequest();
|
|
566
|
+
const handler = context.getHandler();
|
|
567
|
+
const options = Reflect.getMetadata(METADATA_KEYS.THROTTLE, handler);
|
|
568
|
+
if (!options) {
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
if (options.skipIf && options.skipIf(context)) {
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
const ip = this.getClientIp(request);
|
|
575
|
+
const endpointKey = `${context.getClass().name}:${handler.name}`;
|
|
576
|
+
const key = `throttle:${endpointKey}:${ip}`;
|
|
577
|
+
const now = Date.now();
|
|
578
|
+
const ttlMs = options.ttl * 1000;
|
|
579
|
+
let record = ThrottleGuard_1.storage.get(key);
|
|
580
|
+
if (!record || record.expiresAt < now) {
|
|
581
|
+
record = {
|
|
582
|
+
hits: 0,
|
|
583
|
+
expiresAt: now + ttlMs
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
record.hits++;
|
|
587
|
+
ThrottleGuard_1.storage.set(key, record);
|
|
588
|
+
if (record.hits > options.limit) {
|
|
589
|
+
const response = context.switchToHttp().getResponse();
|
|
590
|
+
const retryAfter = Math.ceil((record.expiresAt - now) / 1000);
|
|
591
|
+
this.setHeaders(response, options.limit, 0, retryAfter);
|
|
592
|
+
throw new CustomHttpError('Too Many Requests', 429, 'rate_limit_exceeded');
|
|
593
|
+
}
|
|
594
|
+
const response = context.switchToHttp().getResponse();
|
|
595
|
+
const remaining = Math.max(0, options.limit - record.hits);
|
|
596
|
+
this.setHeaders(response, options.limit, remaining, options.ttl);
|
|
597
|
+
return true;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Limpia las entradas expiradas para evitar fugas de memoria
|
|
601
|
+
*/
|
|
602
|
+
cleanup() {
|
|
603
|
+
const now = Date.now();
|
|
604
|
+
ThrottleGuard_1.storage.forEach((value, key) => {
|
|
605
|
+
if (value.expiresAt < now) {
|
|
606
|
+
ThrottleGuard_1.storage.delete(key);
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
getClientIp(request) {
|
|
611
|
+
const headers = request.headers;
|
|
612
|
+
if (headers['cf-connecting-ip'])
|
|
613
|
+
return headers['cf-connecting-ip'];
|
|
614
|
+
if (headers['x-forwarded-for']) {
|
|
615
|
+
return headers['x-forwarded-for'].split(',')[0].trim();
|
|
616
|
+
}
|
|
617
|
+
if (headers['x-real-ip'])
|
|
618
|
+
return headers['x-real-ip'];
|
|
619
|
+
return request.socket?.remoteAddress || request.ip || 'unknown';
|
|
620
|
+
}
|
|
621
|
+
setHeaders(res, limit, remaining, resetOrRetry) {
|
|
622
|
+
if (res && typeof res.setHeader === 'function') {
|
|
623
|
+
res.setHeader('X-RateLimit-Limit', limit);
|
|
624
|
+
res.setHeader('X-RateLimit-Remaining', remaining);
|
|
625
|
+
if (remaining === 0) {
|
|
626
|
+
res.setHeader('Retry-After', resetOrRetry);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
ThrottleGuard.storage = new Map();
|
|
632
|
+
ThrottleGuard.cleanupInterval = null;
|
|
633
|
+
ThrottleGuard = ThrottleGuard_1 = __decorate([
|
|
634
|
+
injectable(),
|
|
635
|
+
__metadata("design:paramtypes", [])
|
|
636
|
+
], ThrottleGuard);
|
|
637
|
+
|
|
638
|
+
export { ApiKeyGuard, AppFactory, AuthGuard, GuardsConsumer, HemiaExecutionContext, JWTGuard, PublicClientGuard, Reflector, ResponseSerializer, ThrottleGuard, registerRoutes };
|
package/dist/hemia-core.js
CHANGED
|
@@ -229,7 +229,9 @@ async function registerRoutes(app, container, controllerIdentifiers, onTraceFini
|
|
|
229
229
|
console.error(`Error en ${route.method.toUpperCase()} ${basePath}${route.path}:`, error);
|
|
230
230
|
const status = error.statusCode || error.status || 500;
|
|
231
231
|
const message = error.message || 'Internal Server Error';
|
|
232
|
-
const
|
|
232
|
+
const errorDetail = error instanceof common.HttpError ? error.error : undefined;
|
|
233
|
+
const isDev = process.env.NODE_ENV === 'development';
|
|
234
|
+
const errorResponse = common.ApiResponse.error(message, errorDetail || (isDev ? error.stack : undefined), status);
|
|
233
235
|
const { status: errStatus, ...errorData } = errorResponse;
|
|
234
236
|
res.status(errStatus).json(errorData);
|
|
235
237
|
}
|
|
@@ -340,9 +342,9 @@ class AppFactory {
|
|
|
340
342
|
const defaultHeaders = {
|
|
341
343
|
'Access-Control-Allow-Origin': req.headers.origin || '*',
|
|
342
344
|
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,PATCH,OPTIONS',
|
|
343
|
-
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Api-Key, Language, X-No-Cookies, x-session',
|
|
345
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Api-Key, Language, X-No-Cookies, x-session, x-client-id, X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After',
|
|
344
346
|
'Access-Control-Allow-Credentials': 'true',
|
|
345
|
-
'Access-Control-Expose-Headers': 'Authorization, X-Correlation-Id, X-Trace-Id, x-session',
|
|
347
|
+
'Access-Control-Expose-Headers': 'Authorization, X-Correlation-Id, X-Trace-Id, x-session, X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After',
|
|
346
348
|
};
|
|
347
349
|
const headers = { ...defaultHeaders, ...(options.corsHeaders || {}) };
|
|
348
350
|
Object.entries(headers).forEach(([key, value]) => {
|
|
@@ -527,6 +529,114 @@ exports.JWTGuard = __decorate([
|
|
|
527
529
|
__metadata("design:paramtypes", [authSdk.AuthService])
|
|
528
530
|
], exports.JWTGuard);
|
|
529
531
|
|
|
532
|
+
exports.PublicClientGuard = class PublicClientGuard {
|
|
533
|
+
constructor(oauthService) {
|
|
534
|
+
this.oauthService = oauthService;
|
|
535
|
+
}
|
|
536
|
+
async canActivate(context) {
|
|
537
|
+
const request = context.switchToHttp().getRequest();
|
|
538
|
+
const clientId = request.headers['x-client-id'] || request.body.client_id;
|
|
539
|
+
if (!clientId) {
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
const client = await this.oauthService.getByClientId(clientId);
|
|
543
|
+
if (!client || !client.isActive) {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
request.oauthClient = client;
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
exports.PublicClientGuard = __decorate([
|
|
551
|
+
inversify.injectable(),
|
|
552
|
+
__param(0, inversify.inject(authSdk.OAuthService)),
|
|
553
|
+
__metadata("design:paramtypes", [authSdk.OAuthService])
|
|
554
|
+
], exports.PublicClientGuard);
|
|
555
|
+
|
|
556
|
+
var ThrottleGuard_1;
|
|
557
|
+
exports.ThrottleGuard = ThrottleGuard_1 = class ThrottleGuard {
|
|
558
|
+
constructor() {
|
|
559
|
+
if (!ThrottleGuard_1.cleanupInterval) {
|
|
560
|
+
ThrottleGuard_1.cleanupInterval = setInterval(() => {
|
|
561
|
+
this.cleanup();
|
|
562
|
+
}, 60000);
|
|
563
|
+
ThrottleGuard_1.cleanupInterval.unref();
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
async canActivate(context) {
|
|
567
|
+
const request = context.switchToHttp().getRequest();
|
|
568
|
+
const handler = context.getHandler();
|
|
569
|
+
const options = Reflect.getMetadata(common.METADATA_KEYS.THROTTLE, handler);
|
|
570
|
+
if (!options) {
|
|
571
|
+
return true;
|
|
572
|
+
}
|
|
573
|
+
if (options.skipIf && options.skipIf(context)) {
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
const ip = this.getClientIp(request);
|
|
577
|
+
const endpointKey = `${context.getClass().name}:${handler.name}`;
|
|
578
|
+
const key = `throttle:${endpointKey}:${ip}`;
|
|
579
|
+
const now = Date.now();
|
|
580
|
+
const ttlMs = options.ttl * 1000;
|
|
581
|
+
let record = ThrottleGuard_1.storage.get(key);
|
|
582
|
+
if (!record || record.expiresAt < now) {
|
|
583
|
+
record = {
|
|
584
|
+
hits: 0,
|
|
585
|
+
expiresAt: now + ttlMs
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
record.hits++;
|
|
589
|
+
ThrottleGuard_1.storage.set(key, record);
|
|
590
|
+
if (record.hits > options.limit) {
|
|
591
|
+
const response = context.switchToHttp().getResponse();
|
|
592
|
+
const retryAfter = Math.ceil((record.expiresAt - now) / 1000);
|
|
593
|
+
this.setHeaders(response, options.limit, 0, retryAfter);
|
|
594
|
+
throw new common.CustomHttpError('Too Many Requests', 429, 'rate_limit_exceeded');
|
|
595
|
+
}
|
|
596
|
+
const response = context.switchToHttp().getResponse();
|
|
597
|
+
const remaining = Math.max(0, options.limit - record.hits);
|
|
598
|
+
this.setHeaders(response, options.limit, remaining, options.ttl);
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Limpia las entradas expiradas para evitar fugas de memoria
|
|
603
|
+
*/
|
|
604
|
+
cleanup() {
|
|
605
|
+
const now = Date.now();
|
|
606
|
+
ThrottleGuard_1.storage.forEach((value, key) => {
|
|
607
|
+
if (value.expiresAt < now) {
|
|
608
|
+
ThrottleGuard_1.storage.delete(key);
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
getClientIp(request) {
|
|
613
|
+
const headers = request.headers;
|
|
614
|
+
if (headers['cf-connecting-ip'])
|
|
615
|
+
return headers['cf-connecting-ip'];
|
|
616
|
+
if (headers['x-forwarded-for']) {
|
|
617
|
+
return headers['x-forwarded-for'].split(',')[0].trim();
|
|
618
|
+
}
|
|
619
|
+
if (headers['x-real-ip'])
|
|
620
|
+
return headers['x-real-ip'];
|
|
621
|
+
return request.socket?.remoteAddress || request.ip || 'unknown';
|
|
622
|
+
}
|
|
623
|
+
setHeaders(res, limit, remaining, resetOrRetry) {
|
|
624
|
+
if (res && typeof res.setHeader === 'function') {
|
|
625
|
+
res.setHeader('X-RateLimit-Limit', limit);
|
|
626
|
+
res.setHeader('X-RateLimit-Remaining', remaining);
|
|
627
|
+
if (remaining === 0) {
|
|
628
|
+
res.setHeader('Retry-After', resetOrRetry);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
exports.ThrottleGuard.storage = new Map();
|
|
634
|
+
exports.ThrottleGuard.cleanupInterval = null;
|
|
635
|
+
exports.ThrottleGuard = ThrottleGuard_1 = __decorate([
|
|
636
|
+
inversify.injectable(),
|
|
637
|
+
__metadata("design:paramtypes", [])
|
|
638
|
+
], exports.ThrottleGuard);
|
|
639
|
+
|
|
530
640
|
exports.AppFactory = AppFactory;
|
|
531
641
|
exports.GuardsConsumer = GuardsConsumer;
|
|
532
642
|
exports.HemiaExecutionContext = HemiaExecutionContext;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { CanActivate, ExecutionContext } from "@hemia/common";
|
|
2
|
+
import { OAuthService } from "@hemia/auth-sdk";
|
|
3
|
+
export declare class PublicClientGuard implements CanActivate {
|
|
4
|
+
private readonly oauthService;
|
|
5
|
+
constructor(oauthService: OAuthService);
|
|
6
|
+
canActivate(context: ExecutionContext): Promise<boolean>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { CanActivate, ExecutionContext } from "@hemia/common";
|
|
2
|
+
export declare class ThrottleGuard implements CanActivate {
|
|
3
|
+
private static storage;
|
|
4
|
+
private static cleanupInterval;
|
|
5
|
+
constructor();
|
|
6
|
+
canActivate(context: ExecutionContext): Promise<boolean>;
|
|
7
|
+
/**
|
|
8
|
+
* Limpia las entradas expiradas para evitar fugas de memoria
|
|
9
|
+
*/
|
|
10
|
+
private cleanup;
|
|
11
|
+
private getClientIp;
|
|
12
|
+
private setHeaders;
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hemia/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"description": "Core utilities for Hemia projects",
|
|
5
5
|
"main": "dist/hemia-core.js",
|
|
6
6
|
"module": "dist/hemia-core.esm.js",
|
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
"@rollup/plugin-commonjs": "^26.0.1",
|
|
19
19
|
"@rollup/plugin-json": "^6.1.0",
|
|
20
20
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
|
21
|
-
"@hemia/common": "^0.0.
|
|
21
|
+
"@hemia/common": "^0.0.16",
|
|
22
22
|
"@hemia/app-context": "^0.0.6",
|
|
23
23
|
"@hemia/trace-manager": "^0.0.9",
|
|
24
|
-
"@hemia/auth-sdk": "^0.0.
|
|
24
|
+
"@hemia/auth-sdk": "^0.0.16",
|
|
25
25
|
"@types/express": "^5.0.5",
|
|
26
26
|
"express": "^5.1.0",
|
|
27
27
|
"inversify": "^7.10.4",
|