permissions-contractx 2.2.0 → 2.3.0

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 (29) hide show
  1. package/README.md +93 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +1 -0
  5. package/dist/inter-service-auth/index.d.ts +8 -0
  6. package/dist/inter-service-auth/index.d.ts.map +1 -0
  7. package/dist/inter-service-auth/index.js +31 -0
  8. package/dist/inter-service-auth/inter-service-auth.config.interface.d.ts +10 -0
  9. package/dist/inter-service-auth/inter-service-auth.config.interface.d.ts.map +1 -0
  10. package/dist/inter-service-auth/inter-service-auth.config.interface.js +2 -0
  11. package/dist/inter-service-auth/inter-service-auth.middleware.d.ts +11 -0
  12. package/dist/inter-service-auth/inter-service-auth.middleware.d.ts.map +1 -0
  13. package/dist/inter-service-auth/inter-service-auth.middleware.js +75 -0
  14. package/dist/inter-service-auth/inter-service-auth.module.d.ts +11 -0
  15. package/dist/inter-service-auth/inter-service-auth.module.d.ts.map +1 -0
  16. package/dist/inter-service-auth/inter-service-auth.module.js +44 -0
  17. package/dist/inter-service-auth/inter-service-auth.service.d.ts +23 -0
  18. package/dist/inter-service-auth/inter-service-auth.service.d.ts.map +1 -0
  19. package/dist/inter-service-auth/inter-service-auth.service.js +202 -0
  20. package/dist/inter-service-auth/inter-service-auth.types.d.ts +57 -0
  21. package/dist/inter-service-auth/inter-service-auth.types.d.ts.map +1 -0
  22. package/dist/inter-service-auth/inter-service-auth.types.js +15 -0
  23. package/dist/inter-service-auth/service-auth.decorators.d.ts +12 -0
  24. package/dist/inter-service-auth/service-auth.decorators.d.ts.map +1 -0
  25. package/dist/inter-service-auth/service-auth.decorators.js +22 -0
  26. package/dist/inter-service-auth/service-auth.guard.d.ts +17 -0
  27. package/dist/inter-service-auth/service-auth.guard.d.ts.map +1 -0
  28. package/dist/inter-service-auth/service-auth.guard.js +121 -0
  29. package/package.json +1 -1
package/README.md CHANGED
@@ -84,6 +84,99 @@ import { PermissionWrites } from 'permissions-contractx';
84
84
  search() { /* ... */ }
85
85
  ```
86
86
 
87
+ ## Inter-Service Authentication (HMAC)
88
+
89
+ Además del modelo de autorización basado en JWT de usuario, el paquete incluye el sistema de autenticación **machine-to-machine (M2M)** que usan los microservicios de ContractX para llamarse entre sí de forma segura.
90
+
91
+ Cada llamada saliente lleva headers `X-Service-*` con una firma HMAC-SHA256 del par `apiKey + secretKey` del servicio emisor. El receptor verifica la firma, el timestamp (ventana de expiración + drift de reloj) y el nonce (anti-replay). Ningún JWT de usuario interviene en estas llamadas.
92
+
93
+ Para detalles de implementación, arquitectura y deuda conocida, ver [`src/inter-service-auth/README.md`](./src/inter-service-auth/README.md).
94
+
95
+ ### Configurar el módulo
96
+
97
+ ```typescript
98
+ import { InterServiceAuthModule, SERVICE_NAMES } from 'permissions-contractx';
99
+ import { ConfigModule, ConfigService } from '@nestjs/config';
100
+
101
+ @Module({
102
+ imports: [
103
+ InterServiceAuthModule.forRootAsync({
104
+ imports: [ConfigModule],
105
+ useFactory: (cfg: ConfigService) => ({
106
+ services: {
107
+ [SERVICE_NAMES.AUTH]: {
108
+ serviceId: SERVICE_NAMES.AUTH,
109
+ apiKey: cfg.get('AUTH_API_KEY'),
110
+ secretKey: cfg.get('AUTH_SECRET_KEY'),
111
+ },
112
+ [SERVICE_NAMES.CONTRACTS]: {
113
+ serviceId: SERVICE_NAMES.CONTRACTS,
114
+ apiKey: cfg.get('CONTRACTS_API_KEY'),
115
+ secretKey: cfg.get('CONTRACTS_SECRET_KEY'),
116
+ },
117
+ },
118
+ signatureExpireMinutes: 5,
119
+ allowedTimeDriftSeconds: 30,
120
+ enableLogging: cfg.get('SERVICE_AUTH_LOGGING') === 'true',
121
+ }),
122
+ inject: [ConfigService],
123
+ }),
124
+ ],
125
+ })
126
+ export class AppModule {}
127
+ ```
128
+
129
+ El middleware (`InterServiceAuthMiddleware`) debe registrarse en el módulo consumidor:
130
+
131
+ ```typescript
132
+ import { InterServiceAuthMiddleware } from 'permissions-contractx';
133
+
134
+ export class AppModule implements NestModule {
135
+ configure(consumer: MiddlewareConsumer) {
136
+ consumer.apply(InterServiceAuthMiddleware).forRoutes('*');
137
+ }
138
+ }
139
+ ```
140
+
141
+ ### Proteger un endpoint (solo M2M)
142
+
143
+ ```typescript
144
+ import {
145
+ ServiceAuthGuard,
146
+ RequireServiceAuth,
147
+ RequireServicePermissions,
148
+ } from 'permissions-contractx';
149
+
150
+ @Controller('internal')
151
+ @RequireServiceAuth() // rechaza cualquier request no-M2M
152
+ @UseGuards(ServiceAuthGuard)
153
+ export class InternalController {
154
+ @Get('contracts')
155
+ @RequireServicePermissions('contracts.read')
156
+ listContracts() { /* ... */ }
157
+ }
158
+ ```
159
+
160
+ ### Emitir una llamada M2M saliente
161
+
162
+ ```typescript
163
+ constructor(private readonly authSvc: InterServiceAuthService) {}
164
+
165
+ async fetchFromContracts(schema: string) {
166
+ const headers = this.authSvc.generateAuthHeaders(SERVICE_NAMES.CONTRACTS, schema);
167
+ return this.httpService.get('/internal/contracts', { headers }).toPromise();
168
+ }
169
+ ```
170
+
171
+ ### Decoradores disponibles
172
+
173
+ | Decorador | Propósito |
174
+ |---|---|
175
+ | `@RequireServiceAuth()` | Endpoint exclusivo M2M; rechaza JWT de usuario |
176
+ | `@AllowServiceAuth(false)` | Bloquea M2M en un endpoint específico (gana sobre clase) |
177
+ | `@RequireServicePermissions(...p)` | El servicio llamante debe tener todos los permisos listados |
178
+ | `@RequireServiceRoles(...r)` | El servicio llamante debe tener al menos uno de los roles listados |
179
+
87
180
  ## Modelo de cuatro estados (migración)
88
181
 
89
182
  El `AuthorizationGuard` permite migrar cada endpoint de forma gradual mediante la variable de entorno de enforcement:
package/dist/index.d.ts CHANGED
@@ -4,5 +4,6 @@ export * from './decorators';
4
4
  export * from './services';
5
5
  export * from './interfaces';
6
6
  export * from './constants';
7
+ export * from './inter-service-auth';
7
8
  export type { JwtPayload, AuthenticatedRequest, PermissionsModuleOptions, JwtAuthConfig } from './interfaces';
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,YAAY,EAAE,UAAU,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,sBAAsB,CAAC;AACrC,YAAY,EAAE,UAAU,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js CHANGED
@@ -20,3 +20,4 @@ __exportStar(require("./decorators"), exports);
20
20
  __exportStar(require("./services"), exports);
21
21
  __exportStar(require("./interfaces"), exports);
22
22
  __exportStar(require("./constants"), exports);
23
+ __exportStar(require("./inter-service-auth"), exports);
@@ -0,0 +1,8 @@
1
+ export * from './inter-service-auth.types';
2
+ export * from './inter-service-auth.config.interface';
3
+ export { InterServiceAuthService, INTER_SERVICE_AUTH_CONFIG } from './inter-service-auth.service';
4
+ export * from './inter-service-auth.module';
5
+ export * from './inter-service-auth.middleware';
6
+ export { ServiceAuthGuard, SERVICE_PERMISSIONS_KEY, SERVICE_ROLES_KEY, ALLOW_SERVICE_AUTH_KEY, REQUIRE_SERVICE_AUTH_KEY, } from './service-auth.guard';
7
+ export * from './service-auth.decorators';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/inter-service-auth/index.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uCAAuC,CAAC;AACtD,OAAO,EAAE,uBAAuB,EAAE,yBAAyB,EAAE,MAAM,8BAA8B,CAAC;AAClG,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iCAAiC,CAAC;AAChD,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,iBAAiB,EACjB,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,sBAAsB,CAAC;AAC9B,cAAc,2BAA2B,CAAC"}
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.REQUIRE_SERVICE_AUTH_KEY = exports.ALLOW_SERVICE_AUTH_KEY = exports.SERVICE_ROLES_KEY = exports.SERVICE_PERMISSIONS_KEY = exports.ServiceAuthGuard = exports.INTER_SERVICE_AUTH_CONFIG = exports.InterServiceAuthService = void 0;
18
+ __exportStar(require("./inter-service-auth.types"), exports);
19
+ __exportStar(require("./inter-service-auth.config.interface"), exports);
20
+ var inter_service_auth_service_1 = require("./inter-service-auth.service");
21
+ Object.defineProperty(exports, "InterServiceAuthService", { enumerable: true, get: function () { return inter_service_auth_service_1.InterServiceAuthService; } });
22
+ Object.defineProperty(exports, "INTER_SERVICE_AUTH_CONFIG", { enumerable: true, get: function () { return inter_service_auth_service_1.INTER_SERVICE_AUTH_CONFIG; } });
23
+ __exportStar(require("./inter-service-auth.module"), exports);
24
+ __exportStar(require("./inter-service-auth.middleware"), exports);
25
+ var service_auth_guard_1 = require("./service-auth.guard");
26
+ Object.defineProperty(exports, "ServiceAuthGuard", { enumerable: true, get: function () { return service_auth_guard_1.ServiceAuthGuard; } });
27
+ Object.defineProperty(exports, "SERVICE_PERMISSIONS_KEY", { enumerable: true, get: function () { return service_auth_guard_1.SERVICE_PERMISSIONS_KEY; } });
28
+ Object.defineProperty(exports, "SERVICE_ROLES_KEY", { enumerable: true, get: function () { return service_auth_guard_1.SERVICE_ROLES_KEY; } });
29
+ Object.defineProperty(exports, "ALLOW_SERVICE_AUTH_KEY", { enumerable: true, get: function () { return service_auth_guard_1.ALLOW_SERVICE_AUTH_KEY; } });
30
+ Object.defineProperty(exports, "REQUIRE_SERVICE_AUTH_KEY", { enumerable: true, get: function () { return service_auth_guard_1.REQUIRE_SERVICE_AUTH_KEY; } });
31
+ __exportStar(require("./service-auth.decorators"), exports);
@@ -0,0 +1,10 @@
1
+ import { IServiceCredentials } from './inter-service-auth.types';
2
+ export interface IServiceAuthConfig {
3
+ services: Record<string, IServiceCredentials>;
4
+ /** Minutes before a signed request expires (looking backward). Default: 5 */
5
+ signatureExpireMinutes?: number;
6
+ /** Seconds of future clock skew tolerated (looking forward). Default: 30 */
7
+ allowedTimeDriftSeconds?: number;
8
+ enableLogging?: boolean;
9
+ }
10
+ //# sourceMappingURL=inter-service-auth.config.interface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inter-service-auth.config.interface.d.ts","sourceRoot":"","sources":["../../src/inter-service-auth/inter-service-auth.config.interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEjE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IAC9C,6EAA6E;IAC7E,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,4EAA4E;IAC5E,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB"}
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,11 @@
1
+ import { NestMiddleware } from '@nestjs/common';
2
+ import { Request, Response, NextFunction } from 'express';
3
+ import { InterServiceAuthService } from './inter-service-auth.service';
4
+ export declare class InterServiceAuthMiddleware implements NestMiddleware {
5
+ private readonly interServiceAuthService;
6
+ private readonly logger;
7
+ constructor(interServiceAuthService: InterServiceAuthService);
8
+ use(req: Request, res: Response, next: NextFunction): void | Response<any, Record<string, any>>;
9
+ private normalizeHeaders;
10
+ }
11
+ //# sourceMappingURL=inter-service-auth.middleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inter-service-auth.middleware.d.ts","sourceRoot":"","sources":["../../src/inter-service-auth/inter-service-auth.middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,cAAc,EAAiC,MAAM,gBAAgB,CAAC;AAC3F,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE1D,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAEvE,qBACa,0BAA2B,YAAW,cAAc;IAGnD,OAAO,CAAC,QAAQ,CAAC,uBAAuB;IAFpD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4C;gBAEtC,uBAAuB,EAAE,uBAAuB;IAE7E,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY;IA4CnD,OAAO,CAAC,gBAAgB;CAczB"}
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.InterServiceAuthMiddleware = void 0;
13
+ const common_1 = require("@nestjs/common");
14
+ const inter_service_auth_service_1 = require("./inter-service-auth.service");
15
+ let InterServiceAuthMiddleware = class InterServiceAuthMiddleware {
16
+ constructor(interServiceAuthService) {
17
+ this.interServiceAuthService = interServiceAuthService;
18
+ this.logger = new common_1.Logger('InterServiceAuthMiddleware');
19
+ }
20
+ use(req, res, next) {
21
+ try {
22
+ const isServiceAuth = req.headers['x-service-auth'] === 'true';
23
+ if (!isServiceAuth) {
24
+ // Not a service-to-service request — pass through for normal auth guards
25
+ req.isServiceRequest = false;
26
+ return next();
27
+ }
28
+ const headers = this.normalizeHeaders(req.headers);
29
+ const servicePayload = this.interServiceAuthService.validateServiceAuth(headers);
30
+ req.serviceAuth = servicePayload;
31
+ req.isServiceRequest = true;
32
+ this.logger.debug(`Service-to-service call authenticated: ${servicePayload.serviceId} -> ${req.path}`);
33
+ next();
34
+ }
35
+ catch (error) {
36
+ const message = error instanceof Error ? error.message : String(error);
37
+ const stack = error instanceof Error ? error.stack : undefined;
38
+ this.logger.error(`Service authentication failed: ${message}`, stack);
39
+ if (error instanceof common_1.UnauthorizedException) {
40
+ return res.status(401).json({
41
+ statusCode: 401,
42
+ message: 'Service authentication failed',
43
+ error: 'Unauthorized',
44
+ timestamp: new Date().toISOString(),
45
+ path: req.url,
46
+ });
47
+ }
48
+ return res.status(500).json({
49
+ statusCode: 500,
50
+ message: 'Internal server error during service authentication',
51
+ error: 'Internal Server Error',
52
+ timestamp: new Date().toISOString(),
53
+ path: req.url,
54
+ });
55
+ }
56
+ }
57
+ normalizeHeaders(headers) {
58
+ const normalized = {};
59
+ Object.keys(headers).forEach((key) => {
60
+ const value = headers[key];
61
+ if (typeof value === 'string') {
62
+ normalized[key.toLowerCase()] = value;
63
+ }
64
+ else if (Array.isArray(value) && value.length > 0) {
65
+ normalized[key.toLowerCase()] = value[0];
66
+ }
67
+ });
68
+ return normalized;
69
+ }
70
+ };
71
+ exports.InterServiceAuthMiddleware = InterServiceAuthMiddleware;
72
+ exports.InterServiceAuthMiddleware = InterServiceAuthMiddleware = __decorate([
73
+ (0, common_1.Injectable)(),
74
+ __metadata("design:paramtypes", [inter_service_auth_service_1.InterServiceAuthService])
75
+ ], InterServiceAuthMiddleware);
@@ -0,0 +1,11 @@
1
+ import { DynamicModule } from '@nestjs/common';
2
+ import { IServiceAuthConfig } from './inter-service-auth.config.interface';
3
+ export declare class InterServiceAuthModule {
4
+ static forRoot(config: IServiceAuthConfig): DynamicModule;
5
+ static forRootAsync(options: {
6
+ imports?: any[];
7
+ useFactory: (...args: any[]) => IServiceAuthConfig | Promise<IServiceAuthConfig>;
8
+ inject?: any[];
9
+ }): DynamicModule;
10
+ }
11
+ //# sourceMappingURL=inter-service-auth.module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inter-service-auth.module.d.ts","sourceRoot":"","sources":["../../src/inter-service-auth/inter-service-auth.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAkB,MAAM,gBAAgB,CAAC;AAE/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,uCAAuC,CAAC;AAE3E,qBAEa,sBAAsB;IACjC,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,kBAAkB,GAAG,aAAa;IAWzD,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE;QAC3B,OAAO,CAAC,EAAE,GAAG,EAAE,CAAC;QAChB,UAAU,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;QACjF,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC;KAChB,GAAG,aAAa;CAelB"}
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var InterServiceAuthModule_1;
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.InterServiceAuthModule = void 0;
11
+ const common_1 = require("@nestjs/common");
12
+ const inter_service_auth_service_1 = require("./inter-service-auth.service");
13
+ let InterServiceAuthModule = InterServiceAuthModule_1 = class InterServiceAuthModule {
14
+ static forRoot(config) {
15
+ return {
16
+ module: InterServiceAuthModule_1,
17
+ providers: [
18
+ { provide: inter_service_auth_service_1.INTER_SERVICE_AUTH_CONFIG, useValue: config },
19
+ inter_service_auth_service_1.InterServiceAuthService,
20
+ ],
21
+ exports: [inter_service_auth_service_1.InterServiceAuthService],
22
+ };
23
+ }
24
+ static forRootAsync(options) {
25
+ return {
26
+ module: InterServiceAuthModule_1,
27
+ imports: options.imports || [],
28
+ providers: [
29
+ {
30
+ provide: inter_service_auth_service_1.INTER_SERVICE_AUTH_CONFIG,
31
+ useFactory: options.useFactory,
32
+ inject: options.inject || [],
33
+ },
34
+ inter_service_auth_service_1.InterServiceAuthService,
35
+ ],
36
+ exports: [inter_service_auth_service_1.InterServiceAuthService],
37
+ };
38
+ }
39
+ };
40
+ exports.InterServiceAuthModule = InterServiceAuthModule;
41
+ exports.InterServiceAuthModule = InterServiceAuthModule = InterServiceAuthModule_1 = __decorate([
42
+ (0, common_1.Global)(),
43
+ (0, common_1.Module)({})
44
+ ], InterServiceAuthModule);
@@ -0,0 +1,23 @@
1
+ import { OnModuleDestroy } from '@nestjs/common';
2
+ import { IServiceAuthConfig } from './inter-service-auth.config.interface';
3
+ import { IServiceAuthPayload, IServiceCredentials } from './inter-service-auth.types';
4
+ export declare const INTER_SERVICE_AUTH_CONFIG = "INTER_SERVICE_AUTH_CONFIG";
5
+ export declare class InterServiceAuthService implements OnModuleDestroy {
6
+ private readonly config;
7
+ private readonly logger;
8
+ private readonly seenNonces;
9
+ private readonly intervalId;
10
+ constructor(config: IServiceAuthConfig);
11
+ onModuleDestroy(): void;
12
+ generateAuthHeaders(serviceId: string, targetSchema?: string): Record<string, string>;
13
+ validateServiceAuth(headers: Record<string, string>): IServiceAuthPayload;
14
+ getServiceConfig(serviceId: string): IServiceCredentials | undefined;
15
+ getConfiguredServices(): string[];
16
+ hasPermission(serviceId: string, permission: string): boolean;
17
+ hasRole(serviceId: string, role: string): boolean;
18
+ private purgeExpiredNonces;
19
+ private generateSignature;
20
+ private verifySignature;
21
+ private generateNonce;
22
+ }
23
+ //# sourceMappingURL=inter-service-auth.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inter-service-auth.service.d.ts","sourceRoot":"","sources":["../../src/inter-service-auth/inter-service-auth.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqD,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEpG,OAAO,EAAE,kBAAkB,EAAE,MAAM,uCAAuC,CAAC;AAC3E,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEtF,eAAO,MAAM,yBAAyB,8BAA8B,CAAC;AAErE,qBACa,uBAAwB,YAAW,eAAe;IASd,OAAO,CAAC,QAAQ,CAAC,MAAM;IARtE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4C;IAKnE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkC;IAC7D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAiC;gBAEI,MAAM,EAAE,kBAAkB;IAK1F,eAAe,IAAI,IAAI;IAIvB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,GAAE,MAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IA6BzF,mBAAmB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,mBAAmB;IA2EzE,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS;IAIpE,qBAAqB,IAAI,MAAM,EAAE;IAIjC,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO;IAU7D,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO;IAUjD,OAAO,CAAC,kBAAkB;IAS1B,OAAO,CAAC,iBAAiB;IAKzB,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,aAAa;CAGtB"}
@@ -0,0 +1,202 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
19
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
20
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
21
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
22
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
23
+ };
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ var __metadata = (this && this.__metadata) || function (k, v) {
42
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
43
+ };
44
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
45
+ return function (target, key) { decorator(target, key, paramIndex); }
46
+ };
47
+ var InterServiceAuthService_1;
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.InterServiceAuthService = exports.INTER_SERVICE_AUTH_CONFIG = void 0;
50
+ const common_1 = require("@nestjs/common");
51
+ const crypto = __importStar(require("crypto"));
52
+ exports.INTER_SERVICE_AUTH_CONFIG = 'INTER_SERVICE_AUTH_CONFIG';
53
+ let InterServiceAuthService = InterServiceAuthService_1 = class InterServiceAuthService {
54
+ constructor(config) {
55
+ this.config = config;
56
+ this.logger = new common_1.Logger(InterServiceAuthService_1.name);
57
+ // Replay protection: tracks nonces seen within their validity window.
58
+ // Key: `${serviceId}:${nonce}`, value: expiresAt timestamp (ms).
59
+ // DEBT: in-memory only — assumes single-node deployment. For horizontal scaling, migrate to Redis.
60
+ this.seenNonces = new Map();
61
+ // Purge expired nonces every 60 s to prevent unbounded memory growth
62
+ this.intervalId = setInterval(() => this.purgeExpiredNonces(), 60_000);
63
+ }
64
+ onModuleDestroy() {
65
+ clearInterval(this.intervalId);
66
+ }
67
+ generateAuthHeaders(serviceId, targetSchema = '') {
68
+ const service = this.config.services[serviceId];
69
+ if (!service) {
70
+ throw new common_1.UnauthorizedException(`Service ${serviceId} not configured`);
71
+ }
72
+ const timestamp = Date.now();
73
+ const nonce = this.generateNonce();
74
+ const payload = {
75
+ serviceId: service.serviceId,
76
+ permissions: service.permissions ?? ['inter-service.communicate'],
77
+ roles: service.roles ?? ['service'],
78
+ timestamp,
79
+ nonce,
80
+ };
81
+ const signature = this.generateSignature(payload, service.secretKey, targetSchema);
82
+ return {
83
+ 'X-Service-ID': service.serviceId,
84
+ 'X-Service-API-Key': service.apiKey,
85
+ 'X-Service-Timestamp': timestamp.toString(),
86
+ 'X-Service-Nonce': nonce,
87
+ 'X-Service-Signature': signature,
88
+ 'X-Service-Auth': 'true',
89
+ };
90
+ }
91
+ validateServiceAuth(headers) {
92
+ const serviceId = headers['x-service-id'];
93
+ const apiKey = headers['x-service-api-key'];
94
+ const timestamp = parseInt(headers['x-service-timestamp'] || '0');
95
+ const nonce = headers['x-service-nonce'];
96
+ const signature = headers['x-service-signature'];
97
+ const isServiceAuth = headers['x-service-auth'] === 'true';
98
+ if (!isServiceAuth) {
99
+ throw new common_1.UnauthorizedException('Not a service-to-service request');
100
+ }
101
+ if (!serviceId || !apiKey || !timestamp || !nonce || !signature) {
102
+ throw new common_1.UnauthorizedException('Missing service authentication headers');
103
+ }
104
+ const service = Object.values(this.config.services).find((s) => s.serviceId === serviceId && s.apiKey === apiKey);
105
+ if (!service) {
106
+ this.logger.warn(`Unknown service attempted authentication: ${serviceId}`);
107
+ throw new common_1.UnauthorizedException('Invalid service credentials');
108
+ }
109
+ // signatureExpireMinutes: window looking backward — how long a signature remains valid.
110
+ // allowedTimeDriftSeconds: window looking forward — tolerance for sender clock skew.
111
+ // These are distinct concepts. Using Math.abs() with OR collapses both into min(expiry, drift),
112
+ // making the effective window only 30 s in both directions instead of 5 min back / 30 s forward.
113
+ const now = Date.now();
114
+ const maxAge = (this.config.signatureExpireMinutes ?? 5) * 60 * 1000;
115
+ const timeDrift = (this.config.allowedTimeDriftSeconds ?? 30) * 1000;
116
+ const ageMs = now - timestamp; // positive = timestamp is in the past; negative = timestamp is in the future
117
+ if (ageMs > maxAge) {
118
+ this.logger.warn(`Service auth signature expired: ${serviceId}`);
119
+ throw new common_1.UnauthorizedException('Service authentication signature expired');
120
+ }
121
+ if (ageMs < -timeDrift) {
122
+ this.logger.warn(`Service auth timestamp too far in the future: ${serviceId}`);
123
+ throw new common_1.UnauthorizedException('Service authentication timestamp invalid');
124
+ }
125
+ const payload = {
126
+ serviceId,
127
+ permissions: service.permissions ?? ['inter-service.communicate'],
128
+ roles: service.roles ?? ['service'],
129
+ timestamp,
130
+ nonce,
131
+ };
132
+ const targetSchema = headers['x-target-schema'] || '';
133
+ const expectedSignature = this.generateSignature(payload, service.secretKey, targetSchema);
134
+ if (!this.verifySignature(signature, expectedSignature)) {
135
+ this.logger.warn(`Invalid signature for service: ${serviceId}`);
136
+ throw new common_1.UnauthorizedException('Invalid service signature');
137
+ }
138
+ // Replay protection: reject any nonce already seen within its validity window.
139
+ // DEBT: in-memory only — assumes single-node deployment. For horizontal scaling, migrate to Redis.
140
+ const nonceKey = `${serviceId}:${nonce}`;
141
+ if (this.seenNonces.has(nonceKey)) {
142
+ this.logger.warn(`Replayed nonce detected for service: ${serviceId}`);
143
+ throw new common_1.UnauthorizedException('Nonce already used');
144
+ }
145
+ this.seenNonces.set(nonceKey, timestamp + maxAge);
146
+ if (this.config.enableLogging) {
147
+ this.logger.log(`Service authenticated successfully: ${serviceId}`);
148
+ }
149
+ return payload;
150
+ }
151
+ getServiceConfig(serviceId) {
152
+ return this.config.services[serviceId];
153
+ }
154
+ getConfiguredServices() {
155
+ return Object.keys(this.config.services);
156
+ }
157
+ hasPermission(serviceId, permission) {
158
+ const service = this.config.services[serviceId];
159
+ if (!service)
160
+ return false;
161
+ return ((service.permissions ?? []).includes(permission) ||
162
+ (service.permissions ?? []).includes('*') ||
163
+ (service.permissions ?? []).includes('inter-service.*'));
164
+ }
165
+ hasRole(serviceId, role) {
166
+ const service = this.config.services[serviceId];
167
+ if (!service)
168
+ return false;
169
+ const roles = service.roles ?? ['service'];
170
+ // Every configured service implicitly carries the base 'service' role,
171
+ // regardless of explicit configuration. This enables coarse-grained checks
172
+ // like @RequireServiceRoles('service') to pass for any valid credential.
173
+ return roles.includes(role) || role === 'service';
174
+ }
175
+ purgeExpiredNonces() {
176
+ const now = Date.now();
177
+ for (const [key, expiresAt] of this.seenNonces) {
178
+ if (expiresAt < now) {
179
+ this.seenNonces.delete(key);
180
+ }
181
+ }
182
+ }
183
+ generateSignature(payload, secretKey, targetSchema = '') {
184
+ const data = `${payload.serviceId}:${payload.timestamp}:${payload.nonce}:${targetSchema}`;
185
+ return crypto.createHmac('sha256', secretKey).update(data).digest('hex');
186
+ }
187
+ verifySignature(provided, expected) {
188
+ if (provided.length !== expected.length) {
189
+ return false;
190
+ }
191
+ return crypto.timingSafeEqual(Buffer.from(provided, 'hex'), Buffer.from(expected, 'hex'));
192
+ }
193
+ generateNonce() {
194
+ return crypto.randomBytes(16).toString('hex');
195
+ }
196
+ };
197
+ exports.InterServiceAuthService = InterServiceAuthService;
198
+ exports.InterServiceAuthService = InterServiceAuthService = InterServiceAuthService_1 = __decorate([
199
+ (0, common_1.Injectable)(),
200
+ __param(0, (0, common_1.Inject)(exports.INTER_SERVICE_AUTH_CONFIG)),
201
+ __metadata("design:paramtypes", [Object])
202
+ ], InterServiceAuthService);
@@ -0,0 +1,57 @@
1
+ import { Request } from 'express';
2
+ export interface IServiceCredentials {
3
+ serviceId: string;
4
+ apiKey: string;
5
+ secretKey: string;
6
+ /** Default: ['inter-service.communicate'] */
7
+ permissions?: string[];
8
+ /** Default: ['service'] */
9
+ roles?: string[];
10
+ }
11
+ export interface IServiceAuthPayload {
12
+ serviceId: string;
13
+ permissions: string[];
14
+ roles: string[];
15
+ timestamp: number;
16
+ nonce: string;
17
+ }
18
+ export interface IServiceAuthRequest {
19
+ serviceId: string;
20
+ timestamp: number;
21
+ nonce: string;
22
+ signature: string;
23
+ }
24
+ export declare const SERVICE_NAMES: {
25
+ readonly AUTH: "contractx-authorizations";
26
+ readonly CONTRACTS: "contractx-contracts";
27
+ readonly PROVIDERS: "contractx-providers";
28
+ readonly DELIVERIES: "contractx-deliveries";
29
+ readonly INVOICES: "contractx-invoices";
30
+ readonly WORKFLOWS: "contractx-workflows";
31
+ readonly SLAS: "contractx-SLAs";
32
+ readonly MEETINGS: "contractx-meetings";
33
+ readonly NOTIFICATIONS: "contractx-notifications";
34
+ readonly AUDIT: "contractx-audit";
35
+ };
36
+ export type ServiceName = (typeof SERVICE_NAMES)[keyof typeof SERVICE_NAMES];
37
+ declare module 'express-serve-static-core' {
38
+ interface Request {
39
+ serviceAuth?: IServiceAuthPayload;
40
+ isServiceRequest?: boolean;
41
+ }
42
+ }
43
+ /**
44
+ * Narrowed Request type for handlers that sit behind ServiceAuthGuard.
45
+ * Cast to this type when you need non-optional access to serviceAuth:
46
+ *
47
+ * @Get()
48
+ * @UseGuards(ServiceAuthGuard)
49
+ * myEndpoint(@Req() req: AuthenticatedServiceRequest) {
50
+ * const { serviceId } = req.serviceAuth; // guaranteed non-null
51
+ * }
52
+ */
53
+ export type AuthenticatedServiceRequest = Request & {
54
+ serviceAuth: IServiceAuthPayload;
55
+ isServiceRequest: true;
56
+ };
57
+ //# sourceMappingURL=inter-service-auth.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inter-service-auth.types.d.ts","sourceRoot":"","sources":["../../src/inter-service-auth/inter-service-auth.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAElC,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,6CAA6C;IAC7C,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,aAAa;;;;;;;;;;;CAWhB,CAAC;AAEX,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,OAAO,aAAa,CAAC,CAAC;AAI7E,OAAO,QAAQ,2BAA2B,CAAC;IACzC,UAAU,OAAO;QACf,WAAW,CAAC,EAAE,mBAAmB,CAAC;QAClC,gBAAgB,CAAC,EAAE,OAAO,CAAC;KAC5B;CACF;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,2BAA2B,GAAG,OAAO,GAAG;IAClD,WAAW,EAAE,mBAAmB,CAAC;IACjC,gBAAgB,EAAE,IAAI,CAAC;CACxB,CAAC"}
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SERVICE_NAMES = void 0;
4
+ exports.SERVICE_NAMES = {
5
+ AUTH: 'contractx-authorizations',
6
+ CONTRACTS: 'contractx-contracts',
7
+ PROVIDERS: 'contractx-providers',
8
+ DELIVERIES: 'contractx-deliveries',
9
+ INVOICES: 'contractx-invoices',
10
+ WORKFLOWS: 'contractx-workflows',
11
+ SLAS: 'contractx-SLAs',
12
+ MEETINGS: 'contractx-meetings',
13
+ NOTIFICATIONS: 'contractx-notifications',
14
+ AUDIT: 'contractx-audit',
15
+ };
@@ -0,0 +1,12 @@
1
+ /** Requires the caller to have all listed permissions in their service credentials. */
2
+ export declare const RequireServicePermissions: (...permissions: string[]) => import("@nestjs/common").CustomDecorator<string>;
3
+ /** Requires the caller to have at least one of the listed roles. */
4
+ export declare const RequireServiceRoles: (...roles: string[]) => import("@nestjs/common").CustomDecorator<string>;
5
+ /**
6
+ * Controls whether M2M requests are accepted on this endpoint.
7
+ * @AllowServiceAuth(false) blocks M2M even when @RequireServiceAuth() is set at class level.
8
+ */
9
+ export declare const AllowServiceAuth: (allow?: boolean) => import("@nestjs/common").CustomDecorator<string>;
10
+ /** Marks this endpoint as M2M-only. Rejects any request without valid service credentials. */
11
+ export declare const RequireServiceAuth: () => import("@nestjs/common").CustomDecorator<string>;
12
+ //# sourceMappingURL=service-auth.decorators.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-auth.decorators.d.ts","sourceRoot":"","sources":["../../src/inter-service-auth/service-auth.decorators.ts"],"names":[],"mappings":"AAQA,uFAAuF;AACvF,eAAO,MAAM,yBAAyB,GAAI,GAAG,aAAa,MAAM,EAAE,qDACf,CAAC;AAEpD,oEAAoE;AACpE,eAAO,MAAM,mBAAmB,GAAI,GAAG,OAAO,MAAM,EAAE,qDAA0C,CAAC;AAEjG;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAAI,QAAO,OAAc,qDAA+C,CAAC;AAEtG,8FAA8F;AAC9F,eAAO,MAAM,kBAAkB,wDAAoD,CAAC"}
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RequireServiceAuth = exports.AllowServiceAuth = exports.RequireServiceRoles = exports.RequireServicePermissions = void 0;
4
+ const common_1 = require("@nestjs/common");
5
+ const service_auth_guard_1 = require("./service-auth.guard");
6
+ /** Requires the caller to have all listed permissions in their service credentials. */
7
+ const RequireServicePermissions = (...permissions) => (0, common_1.SetMetadata)(service_auth_guard_1.SERVICE_PERMISSIONS_KEY, permissions);
8
+ exports.RequireServicePermissions = RequireServicePermissions;
9
+ /** Requires the caller to have at least one of the listed roles. */
10
+ const RequireServiceRoles = (...roles) => (0, common_1.SetMetadata)(service_auth_guard_1.SERVICE_ROLES_KEY, roles);
11
+ exports.RequireServiceRoles = RequireServiceRoles;
12
+ /**
13
+ * Controls whether M2M requests are accepted on this endpoint.
14
+ * @AllowServiceAuth(false) blocks M2M even when @RequireServiceAuth() is set at class level.
15
+ */
16
+ const AllowServiceAuth = (allow = true) => (0, common_1.SetMetadata)(service_auth_guard_1.ALLOW_SERVICE_AUTH_KEY, allow);
17
+ exports.AllowServiceAuth = AllowServiceAuth;
18
+ /** Marks this endpoint as M2M-only. Rejects any request without valid service credentials. */
19
+ const RequireServiceAuth = () => (0, common_1.SetMetadata)(service_auth_guard_1.REQUIRE_SERVICE_AUTH_KEY, true);
20
+ exports.RequireServiceAuth = RequireServiceAuth;
21
+ // TODO: @CurrentService() param decorator postponed — implement cleanly with
22
+ // createParamDecorator if a handler needs the calling serviceId.
@@ -0,0 +1,17 @@
1
+ import { CanActivate, ExecutionContext } from '@nestjs/common';
2
+ import { Reflector } from '@nestjs/core';
3
+ import { InterServiceAuthService } from './inter-service-auth.service';
4
+ export declare const SERVICE_PERMISSIONS_KEY = "service_permissions";
5
+ export declare const SERVICE_ROLES_KEY = "service_roles";
6
+ export declare const ALLOW_SERVICE_AUTH_KEY = "allow_service_auth";
7
+ export declare const REQUIRE_SERVICE_AUTH_KEY = "require_service_auth";
8
+ export declare class ServiceAuthGuard implements CanActivate {
9
+ private readonly reflector;
10
+ private readonly interServiceAuthService;
11
+ private readonly logger;
12
+ constructor(reflector: Reflector, interServiceAuthService: InterServiceAuthService);
13
+ canActivate(context: ExecutionContext): boolean;
14
+ private checkPermission;
15
+ private checkRole;
16
+ }
17
+ //# sourceMappingURL=service-auth.guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-auth.guard.d.ts","sourceRoot":"","sources":["../../src/inter-service-auth/service-auth.guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,WAAW,EACX,gBAAgB,EAIjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAGvE,eAAO,MAAM,uBAAuB,wBAAwB,CAAC;AAC7D,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AACjD,eAAO,MAAM,sBAAsB,uBAAuB,CAAC;AAC3D,eAAO,MAAM,wBAAwB,yBAAyB,CAAC;AAK/D,qBACa,gBAAiB,YAAW,WAAW;IAIhD,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,uBAAuB;IAJ1C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkC;gBAGtC,SAAS,EAAE,SAAS,EACpB,uBAAuB,EAAE,uBAAuB;IAGnE,WAAW,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO;IA6E/C,OAAO,CAAC,eAAe;IAyBvB,OAAO,CAAC,SAAS;CAYlB"}
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.ServiceAuthGuard = exports.REQUIRE_SERVICE_AUTH_KEY = exports.ALLOW_SERVICE_AUTH_KEY = exports.SERVICE_ROLES_KEY = exports.SERVICE_PERMISSIONS_KEY = void 0;
13
+ const common_1 = require("@nestjs/common");
14
+ const core_1 = require("@nestjs/core");
15
+ const inter_service_auth_service_1 = require("./inter-service-auth.service");
16
+ // Exported so decorators and consumers can reference keys without magic strings
17
+ exports.SERVICE_PERMISSIONS_KEY = 'service_permissions';
18
+ exports.SERVICE_ROLES_KEY = 'service_roles';
19
+ exports.ALLOW_SERVICE_AUTH_KEY = 'allow_service_auth';
20
+ exports.REQUIRE_SERVICE_AUTH_KEY = 'require_service_auth';
21
+ // TODO: @CurrentService() param decorator postponed — implement cleanly with
22
+ // createParamDecorator if a handler needs the calling serviceId.
23
+ let ServiceAuthGuard = class ServiceAuthGuard {
24
+ constructor(reflector, interServiceAuthService) {
25
+ this.reflector = reflector;
26
+ this.interServiceAuthService = interServiceAuthService;
27
+ this.logger = new common_1.Logger('ServiceAuthGuard');
28
+ }
29
+ canActivate(context) {
30
+ const request = context.switchToHttp().getRequest();
31
+ // Step 1: if @RequireServiceAuth() is set, reject non-M2M requests immediately
32
+ const requireServiceAuth = this.reflector.getAllAndOverride(exports.REQUIRE_SERVICE_AUTH_KEY, [
33
+ context.getHandler(),
34
+ context.getClass(),
35
+ ]);
36
+ if (requireServiceAuth === true) {
37
+ if (!request.isServiceRequest || !request.serviceAuth) {
38
+ this.logger.warn(`Service authentication required but not provided for ${request.method} ${request.path}`);
39
+ throw new common_1.UnauthorizedException('Service-to-service authentication required');
40
+ }
41
+ }
42
+ else {
43
+ // No service auth requirement — let other guards handle non-M2M requests
44
+ if (!request.isServiceRequest || !request.serviceAuth) {
45
+ return true;
46
+ }
47
+ }
48
+ // Step 2: @AllowServiceAuth(false) explicitly blocks M2M on this endpoint.
49
+ // This wins even when @RequireServiceAuth() is set at class level.
50
+ const allowServiceAuth = this.reflector.getAllAndOverride(exports.ALLOW_SERVICE_AUTH_KEY, [
51
+ context.getHandler(),
52
+ context.getClass(),
53
+ ]);
54
+ if (allowServiceAuth === false) {
55
+ throw new common_1.ForbiddenException('Service authentication not allowed for this endpoint');
56
+ }
57
+ const serviceAuth = request.serviceAuth;
58
+ // Step 3: permission check (handler metadata takes precedence over class via getAllAndOverride)
59
+ const requiredPermissions = this.reflector.getAllAndOverride(exports.SERVICE_PERMISSIONS_KEY, [
60
+ context.getHandler(),
61
+ context.getClass(),
62
+ ]) || [];
63
+ if (requiredPermissions.length > 0) {
64
+ const hasPermission = requiredPermissions.some((permission) => this.checkPermission(serviceAuth.serviceId, serviceAuth.permissions, permission));
65
+ if (!hasPermission) {
66
+ this.logger.warn(`Service ${serviceAuth.serviceId} lacks required permissions: ${requiredPermissions.join(', ')}`);
67
+ throw new common_1.ForbiddenException('Insufficient permissions for service operation');
68
+ }
69
+ }
70
+ // Step 4: role check
71
+ const requiredRoles = this.reflector.getAllAndOverride(exports.SERVICE_ROLES_KEY, [
72
+ context.getHandler(),
73
+ context.getClass(),
74
+ ]) || [];
75
+ if (requiredRoles.length > 0) {
76
+ const hasRole = requiredRoles.some((role) => this.checkRole(serviceAuth.serviceId, serviceAuth.roles, role));
77
+ if (!hasRole) {
78
+ this.logger.warn(`Service ${serviceAuth.serviceId} lacks required roles: ${requiredRoles.join(', ')}`);
79
+ throw new common_1.ForbiddenException('Insufficient role for service operation');
80
+ }
81
+ }
82
+ this.logger.debug(`Service ${serviceAuth.serviceId} authorized for ${request.method} ${request.path}`);
83
+ return true;
84
+ }
85
+ checkPermission(serviceId, servicePermissions, requiredPermission) {
86
+ if (servicePermissions.includes('*') || servicePermissions.includes('inter-service.*')) {
87
+ return true;
88
+ }
89
+ if (servicePermissions.includes(requiredPermission)) {
90
+ return true;
91
+ }
92
+ // Prefix wildcard: 'contracts.*' covers 'contracts.read', 'contracts.write', etc.
93
+ const hasWildcardPermission = servicePermissions.some((permission) => {
94
+ if (permission.endsWith('.*')) {
95
+ const basePermission = permission.slice(0, -2);
96
+ return requiredPermission.startsWith(basePermission);
97
+ }
98
+ return false;
99
+ });
100
+ if (hasWildcardPermission) {
101
+ return true;
102
+ }
103
+ return this.interServiceAuthService.hasPermission(serviceId, requiredPermission);
104
+ }
105
+ checkRole(serviceId, serviceRoles, requiredRole) {
106
+ if (serviceRoles.includes(requiredRole)) {
107
+ return true;
108
+ }
109
+ // Any service with the base 'service' role passes coarse-grained role checks
110
+ if (serviceRoles.includes('service')) {
111
+ return true;
112
+ }
113
+ return this.interServiceAuthService.hasRole(serviceId, requiredRole);
114
+ }
115
+ };
116
+ exports.ServiceAuthGuard = ServiceAuthGuard;
117
+ exports.ServiceAuthGuard = ServiceAuthGuard = __decorate([
118
+ (0, common_1.Injectable)(),
119
+ __metadata("design:paramtypes", [core_1.Reflector,
120
+ inter_service_auth_service_1.InterServiceAuthService])
121
+ ], ServiceAuthGuard);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "permissions-contractx",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Enterprise-grade authentication and authorization package for NestJS microservices with role-based and permission-based access control",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",