@hemia/core 0.0.12 → 0.0.13
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, 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
|
/**
|
|
@@ -338,9 +338,9 @@ class AppFactory {
|
|
|
338
338
|
const defaultHeaders = {
|
|
339
339
|
'Access-Control-Allow-Origin': req.headers.origin || '*',
|
|
340
340
|
'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',
|
|
341
|
+
'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
342
|
'Access-Control-Allow-Credentials': 'true',
|
|
343
|
-
'Access-Control-Expose-Headers': 'Authorization, X-Correlation-Id, X-Trace-Id, x-session',
|
|
343
|
+
'Access-Control-Expose-Headers': 'Authorization, X-Correlation-Id, X-Trace-Id, x-session, X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After',
|
|
344
344
|
};
|
|
345
345
|
const headers = { ...defaultHeaders, ...(options.corsHeaders || {}) };
|
|
346
346
|
Object.entries(headers).forEach(([key, value]) => {
|
|
@@ -525,4 +525,112 @@ JWTGuard = __decorate([
|
|
|
525
525
|
__metadata("design:paramtypes", [AuthService])
|
|
526
526
|
], JWTGuard);
|
|
527
527
|
|
|
528
|
-
|
|
528
|
+
let PublicClientGuard = class PublicClientGuard {
|
|
529
|
+
constructor(oauthService) {
|
|
530
|
+
this.oauthService = oauthService;
|
|
531
|
+
}
|
|
532
|
+
async canActivate(context) {
|
|
533
|
+
const request = context.switchToHttp().getRequest();
|
|
534
|
+
const clientId = request.headers['x-client-id'] || request.body.client_id;
|
|
535
|
+
if (!clientId) {
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
const client = await this.oauthService.getByClientId(clientId);
|
|
539
|
+
if (!client || !client.isActive) {
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
request.oauthClient = client;
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
PublicClientGuard = __decorate([
|
|
547
|
+
injectable(),
|
|
548
|
+
__param(0, inject(OAuthService)),
|
|
549
|
+
__metadata("design:paramtypes", [OAuthService])
|
|
550
|
+
], PublicClientGuard);
|
|
551
|
+
|
|
552
|
+
var ThrottleGuard_1;
|
|
553
|
+
let ThrottleGuard = ThrottleGuard_1 = class ThrottleGuard {
|
|
554
|
+
constructor() {
|
|
555
|
+
if (!ThrottleGuard_1.cleanupInterval) {
|
|
556
|
+
ThrottleGuard_1.cleanupInterval = setInterval(() => {
|
|
557
|
+
this.cleanup();
|
|
558
|
+
}, 60000);
|
|
559
|
+
ThrottleGuard_1.cleanupInterval.unref();
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
async canActivate(context) {
|
|
563
|
+
const request = context.switchToHttp().getRequest();
|
|
564
|
+
const handler = context.getHandler();
|
|
565
|
+
const options = Reflect.getMetadata(METADATA_KEYS.THROTTLE, handler);
|
|
566
|
+
if (!options) {
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
569
|
+
if (options.skipIf && options.skipIf(context)) {
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
const ip = this.getClientIp(request);
|
|
573
|
+
const endpointKey = `${context.getClass().name}:${handler.name}`;
|
|
574
|
+
const key = `throttle:${endpointKey}:${ip}`;
|
|
575
|
+
const now = Date.now();
|
|
576
|
+
const ttlMs = options.ttl * 1000;
|
|
577
|
+
let record = ThrottleGuard_1.storage.get(key);
|
|
578
|
+
if (!record || record.expiresAt < now) {
|
|
579
|
+
record = {
|
|
580
|
+
hits: 0,
|
|
581
|
+
expiresAt: now + ttlMs
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
record.hits++;
|
|
585
|
+
ThrottleGuard_1.storage.set(key, record);
|
|
586
|
+
if (record.hits > options.limit) {
|
|
587
|
+
const response = context.switchToHttp().getResponse();
|
|
588
|
+
const retryAfter = Math.ceil((record.expiresAt - now) / 1000);
|
|
589
|
+
this.setHeaders(response, options.limit, 0, retryAfter);
|
|
590
|
+
throw new CustomHttpError('Too Many Requests', 429, 'rate_limit_exceeded');
|
|
591
|
+
}
|
|
592
|
+
const response = context.switchToHttp().getResponse();
|
|
593
|
+
const remaining = Math.max(0, options.limit - record.hits);
|
|
594
|
+
this.setHeaders(response, options.limit, remaining, options.ttl);
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Limpia las entradas expiradas para evitar fugas de memoria
|
|
599
|
+
*/
|
|
600
|
+
cleanup() {
|
|
601
|
+
const now = Date.now();
|
|
602
|
+
ThrottleGuard_1.storage.forEach((value, key) => {
|
|
603
|
+
if (value.expiresAt < now) {
|
|
604
|
+
ThrottleGuard_1.storage.delete(key);
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
getClientIp(request) {
|
|
609
|
+
const headers = request.headers;
|
|
610
|
+
if (headers['cf-connecting-ip'])
|
|
611
|
+
return headers['cf-connecting-ip'];
|
|
612
|
+
if (headers['x-forwarded-for']) {
|
|
613
|
+
return headers['x-forwarded-for'].split(',')[0].trim();
|
|
614
|
+
}
|
|
615
|
+
if (headers['x-real-ip'])
|
|
616
|
+
return headers['x-real-ip'];
|
|
617
|
+
return request.socket?.remoteAddress || request.ip || 'unknown';
|
|
618
|
+
}
|
|
619
|
+
setHeaders(res, limit, remaining, resetOrRetry) {
|
|
620
|
+
if (res && typeof res.setHeader === 'function') {
|
|
621
|
+
res.setHeader('X-RateLimit-Limit', limit);
|
|
622
|
+
res.setHeader('X-RateLimit-Remaining', remaining);
|
|
623
|
+
if (remaining === 0) {
|
|
624
|
+
res.setHeader('Retry-After', resetOrRetry);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
ThrottleGuard.storage = new Map();
|
|
630
|
+
ThrottleGuard.cleanupInterval = null;
|
|
631
|
+
ThrottleGuard = ThrottleGuard_1 = __decorate([
|
|
632
|
+
injectable(),
|
|
633
|
+
__metadata("design:paramtypes", [])
|
|
634
|
+
], ThrottleGuard);
|
|
635
|
+
|
|
636
|
+
export { ApiKeyGuard, AppFactory, AuthGuard, GuardsConsumer, HemiaExecutionContext, JWTGuard, PublicClientGuard, Reflector, ResponseSerializer, ThrottleGuard, registerRoutes };
|
package/dist/hemia-core.js
CHANGED
|
@@ -340,9 +340,9 @@ class AppFactory {
|
|
|
340
340
|
const defaultHeaders = {
|
|
341
341
|
'Access-Control-Allow-Origin': req.headers.origin || '*',
|
|
342
342
|
'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',
|
|
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',
|
|
344
344
|
'Access-Control-Allow-Credentials': 'true',
|
|
345
|
-
'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',
|
|
346
346
|
};
|
|
347
347
|
const headers = { ...defaultHeaders, ...(options.corsHeaders || {}) };
|
|
348
348
|
Object.entries(headers).forEach(([key, value]) => {
|
|
@@ -527,6 +527,114 @@ exports.JWTGuard = __decorate([
|
|
|
527
527
|
__metadata("design:paramtypes", [authSdk.AuthService])
|
|
528
528
|
], exports.JWTGuard);
|
|
529
529
|
|
|
530
|
+
exports.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
|
+
exports.PublicClientGuard = __decorate([
|
|
549
|
+
inversify.injectable(),
|
|
550
|
+
__param(0, inversify.inject(authSdk.OAuthService)),
|
|
551
|
+
__metadata("design:paramtypes", [authSdk.OAuthService])
|
|
552
|
+
], exports.PublicClientGuard);
|
|
553
|
+
|
|
554
|
+
var ThrottleGuard_1;
|
|
555
|
+
exports.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(common.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 common.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
|
+
exports.ThrottleGuard.storage = new Map();
|
|
632
|
+
exports.ThrottleGuard.cleanupInterval = null;
|
|
633
|
+
exports.ThrottleGuard = ThrottleGuard_1 = __decorate([
|
|
634
|
+
inversify.injectable(),
|
|
635
|
+
__metadata("design:paramtypes", [])
|
|
636
|
+
], exports.ThrottleGuard);
|
|
637
|
+
|
|
530
638
|
exports.AppFactory = AppFactory;
|
|
531
639
|
exports.GuardsConsumer = GuardsConsumer;
|
|
532
640
|
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.13",
|
|
4
4
|
"description": "Core utilities for Hemia projects",
|
|
5
5
|
"main": "dist/hemia-core.js",
|
|
6
6
|
"module": "dist/hemia-core.esm.js",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"@hemia/common": "^0.0.15",
|
|
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",
|