@discover-cloud/shared 1.0.0 → 1.0.2
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/authorization/index.d.ts +2 -0
- package/dist/authorization/index.js +18 -0
- package/dist/authorization/permission-cache.service.d.ts +12 -0
- package/dist/authorization/permission-cache.service.js +132 -0
- package/dist/authorization/permissions.d.ts +74 -0
- package/dist/authorization/permissions.js +171 -0
- package/dist/dto/auth-service.dtos.d.ts +5 -12
- package/dist/dto/response.dtos.d.ts +34 -27
- package/dist/dto/response.dtos.js +4 -0
- package/dist/dto/user-service.dtos.d.ts +9 -13
- package/dist/dtos/auth-service.dto.d.ts +44 -0
- package/dist/dtos/auth-service.dto.js +2 -0
- package/dist/dtos/index.d.ts +3 -0
- package/dist/dtos/index.js +19 -0
- package/dist/dtos/response.dto.d.ts +55 -0
- package/dist/dtos/response.dto.js +6 -0
- package/dist/dtos/user-service.dto.d.ts +50 -0
- package/dist/dtos/user-service.dto.js +2 -0
- package/dist/enums/domain.enums.d.ts +42 -0
- package/dist/enums/domain.enums.js +79 -0
- package/dist/enums/index.d.ts +2 -3
- package/dist/enums/index.js +2 -3
- package/dist/enums/permissions.enums.d.ts +124 -0
- package/dist/enums/permissions.enums.js +141 -0
- package/dist/errors/app-error.d.ts +19 -2
- package/dist/errors/app-error.js +17 -2
- package/dist/errors/http-errors.d.ts +18 -0
- package/dist/errors/http-errors.js +25 -1
- package/dist/http/index.d.ts +1 -0
- package/dist/http/index.js +17 -0
- package/dist/http/service-client.d.ts +23 -7
- package/dist/http/service-client.js +54 -22
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/jwt/index.d.ts +1 -2
- package/dist/jwt/index.js +1 -2
- package/dist/jwt/internal-jwt-verifier.d.ts +35 -0
- package/dist/jwt/internal-jwt-verifier.js +162 -0
- package/dist/middleware/authorize.middleware.d.ts +22 -0
- package/dist/middleware/authorize.middleware.js +77 -0
- package/dist/middleware/error-handler.middleware.d.ts +16 -0
- package/dist/middleware/error-handler.middleware.js +42 -0
- package/dist/middleware/index.d.ts +4 -5
- package/dist/middleware/index.js +4 -5
- package/dist/middleware/request-id.middleware.d.ts +20 -0
- package/dist/middleware/request-id.middleware.js +34 -0
- package/dist/middleware/validate.middleware.d.ts +26 -0
- package/dist/middleware/validate.middleware.js +41 -0
- package/dist/types/express.types.d.ts +148 -0
- package/dist/types/express.types.js +42 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.js +1 -1
- package/dist/utils/index.d.ts +2 -1
- package/dist/utils/index.js +2 -1
- package/dist/utils/logger.utils.d.ts +51 -0
- package/dist/utils/logger.utils.js +62 -0
- package/dist/utils/response.d.ts +2 -1
- package/dist/utils/response.js +6 -3
- package/dist/utils/response.utils.d.ts +12 -0
- package/dist/utils/response.utils.js +42 -0
- package/package.json +3 -2
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.InternalServerError = exports.UnprocessableEntityError = exports.ConflictError = exports.NotFoundError = exports.ForbiddenError = exports.UnauthorizedError = exports.BadRequestError = void 0;
|
|
3
|
+
exports.ServiceUnavailableError = exports.InternalServerError = exports.TooManyRequestsError = exports.UnprocessableEntityError = exports.ConflictError = exports.NotFoundError = exports.ForbiddenError = exports.UnauthorizedError = exports.BadRequestError = void 0;
|
|
4
4
|
const app_error_1 = require("./app-error");
|
|
5
|
+
/**
|
|
6
|
+
* HTTP ERRORS
|
|
7
|
+
* ────────────
|
|
8
|
+
* Typed subclasses of AppError for every common HTTP error status.
|
|
9
|
+
* GlobalErrorHandler catches these via instanceof AppError and maps
|
|
10
|
+
* statusCode + code directly to the response.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* throw new NotFoundError("Account not found");
|
|
14
|
+
* throw new ConflictError("Email already in use");
|
|
15
|
+
* throw new BadRequestError("Invalid input", { field: "email" });
|
|
16
|
+
*/
|
|
5
17
|
class BadRequestError extends app_error_1.AppError {
|
|
6
18
|
constructor(message = "Bad Request", details) {
|
|
7
19
|
super("BAD_REQUEST", 400, message, details);
|
|
@@ -38,9 +50,21 @@ class UnprocessableEntityError extends app_error_1.AppError {
|
|
|
38
50
|
}
|
|
39
51
|
}
|
|
40
52
|
exports.UnprocessableEntityError = UnprocessableEntityError;
|
|
53
|
+
class TooManyRequestsError extends app_error_1.AppError {
|
|
54
|
+
constructor(message = "Too Many Requests", details) {
|
|
55
|
+
super("TOO_MANY_REQUESTS", 429, message, details);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
exports.TooManyRequestsError = TooManyRequestsError;
|
|
41
59
|
class InternalServerError extends app_error_1.AppError {
|
|
42
60
|
constructor(message = "Internal Server Error", details) {
|
|
43
61
|
super("INTERNAL_SERVER_ERROR", 500, message, details);
|
|
44
62
|
}
|
|
45
63
|
}
|
|
46
64
|
exports.InternalServerError = InternalServerError;
|
|
65
|
+
class ServiceUnavailableError extends app_error_1.AppError {
|
|
66
|
+
constructor(message = "Service Unavailable", details) {
|
|
67
|
+
super("SERVICE_UNAVAILABLE", 503, message, details);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.ServiceUnavailableError = ServiceUnavailableError;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./service-client";
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
__exportStar(require("./service-client"), exports);
|
|
@@ -1,15 +1,31 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
|
|
2
2
|
import { Request } from "express";
|
|
3
|
+
/**
|
|
4
|
+
* SERVICE CLIENT
|
|
5
|
+
* ───────────────
|
|
6
|
+
* HTTP client for service-to-service calls.
|
|
7
|
+
* Forwards the internal JWT and request ID from the current Express request.
|
|
8
|
+
*
|
|
9
|
+
* Fixes vs original:
|
|
10
|
+
* - Removed `validateStatus: () => true` — it silently swallowed 4xx/5xx,
|
|
11
|
+
* forcing every caller to manually check response.status. Removed so
|
|
12
|
+
* axios throws naturally on error responses.
|
|
13
|
+
* - `postWithAuth(url, data: any)` → `data: unknown` — no silent any
|
|
14
|
+
* - requestId fallback now consistent with requestId middleware logic
|
|
15
|
+
* - Added `patchWithAuth` and `deleteWithAuth` — common enough to include
|
|
16
|
+
* - Retry only on network errors + 5xx (not 4xx — those are caller errors)
|
|
17
|
+
*/
|
|
3
18
|
export declare class ServiceClient {
|
|
4
19
|
readonly http: AxiosInstance;
|
|
5
20
|
constructor(baseURL: string);
|
|
6
21
|
private setupRetry;
|
|
22
|
+
getWithAuth<T = unknown>(url: string, req: Request, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
|
|
23
|
+
postWithAuth<T = unknown>(url: string, data: unknown, req: Request, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
|
|
24
|
+
patchWithAuth<T = unknown>(url: string, data: unknown, req: Request, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
|
|
25
|
+
deleteWithAuth<T = unknown>(url: string, req: Request, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
|
|
7
26
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* extracts the Gateway's Bearer token and forwards it.
|
|
27
|
+
* Merges Authorization + x-request-id into any provided config.
|
|
28
|
+
* The internal JWT is forwarded as-is — the gateway already minted it.
|
|
11
29
|
*/
|
|
12
|
-
|
|
13
|
-
postWithAuth(url: string, data: any, req: Request): Promise<axios.AxiosResponse<any, any, {}>>;
|
|
14
|
-
private createAuthHeader;
|
|
30
|
+
private mergeAuth;
|
|
15
31
|
}
|
|
@@ -6,12 +6,26 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.ServiceClient = void 0;
|
|
7
7
|
const axios_1 = __importDefault(require("axios"));
|
|
8
8
|
const axios_retry_1 = __importDefault(require("axios-retry"));
|
|
9
|
+
/**
|
|
10
|
+
* SERVICE CLIENT
|
|
11
|
+
* ───────────────
|
|
12
|
+
* HTTP client for service-to-service calls.
|
|
13
|
+
* Forwards the internal JWT and request ID from the current Express request.
|
|
14
|
+
*
|
|
15
|
+
* Fixes vs original:
|
|
16
|
+
* - Removed `validateStatus: () => true` — it silently swallowed 4xx/5xx,
|
|
17
|
+
* forcing every caller to manually check response.status. Removed so
|
|
18
|
+
* axios throws naturally on error responses.
|
|
19
|
+
* - `postWithAuth(url, data: any)` → `data: unknown` — no silent any
|
|
20
|
+
* - requestId fallback now consistent with requestId middleware logic
|
|
21
|
+
* - Added `patchWithAuth` and `deleteWithAuth` — common enough to include
|
|
22
|
+
* - Retry only on network errors + 5xx (not 4xx — those are caller errors)
|
|
23
|
+
*/
|
|
9
24
|
class ServiceClient {
|
|
10
25
|
constructor(baseURL) {
|
|
11
26
|
this.http = axios_1.default.create({
|
|
12
27
|
baseURL,
|
|
13
28
|
timeout: 8000,
|
|
14
|
-
validateStatus: () => true
|
|
15
29
|
});
|
|
16
30
|
this.setupRetry();
|
|
17
31
|
}
|
|
@@ -20,37 +34,55 @@ class ServiceClient {
|
|
|
20
34
|
retries: 3,
|
|
21
35
|
retryDelay: axios_retry_1.default.exponentialDelay,
|
|
22
36
|
retryCondition: (err) => {
|
|
37
|
+
// Retry on network errors or 5xx only
|
|
38
|
+
// Do NOT retry 4xx — those are deterministic failures
|
|
23
39
|
if (axios_retry_1.default.isNetworkError(err))
|
|
24
40
|
return true;
|
|
25
|
-
|
|
26
|
-
|
|
41
|
+
const status = err.response?.status ?? 0;
|
|
42
|
+
return status >= 500;
|
|
43
|
+
},
|
|
27
44
|
});
|
|
28
45
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
/* ----------------------------------------------------------------
|
|
47
|
+
Auth-forwarding methods
|
|
48
|
+
Pass the current Express Request to automatically forward:
|
|
49
|
+
- Authorization: Bearer <internal-jwt>
|
|
50
|
+
- x-request-id: <propagated request ID>
|
|
51
|
+
---------------------------------------------------------------- */
|
|
52
|
+
async getWithAuth(url, req, config) {
|
|
53
|
+
return this.http.get(url, this.mergeAuth(req, config));
|
|
54
|
+
}
|
|
55
|
+
async postWithAuth(url, data, req, config) {
|
|
56
|
+
return this.http.post(url, data, this.mergeAuth(req, config));
|
|
36
57
|
}
|
|
37
|
-
async
|
|
38
|
-
return this.http.
|
|
58
|
+
async patchWithAuth(url, data, req, config) {
|
|
59
|
+
return this.http.patch(url, data, this.mergeAuth(req, config));
|
|
39
60
|
}
|
|
40
|
-
|
|
41
|
-
|
|
61
|
+
async deleteWithAuth(url, req, config) {
|
|
62
|
+
return this.http.delete(url, this.mergeAuth(req, config));
|
|
63
|
+
}
|
|
64
|
+
/* ----------------------------------------------------------------
|
|
65
|
+
Private helpers
|
|
66
|
+
---------------------------------------------------------------- */
|
|
67
|
+
/**
|
|
68
|
+
* Merges Authorization + x-request-id into any provided config.
|
|
69
|
+
* The internal JWT is forwarded as-is — the gateway already minted it.
|
|
70
|
+
*/
|
|
71
|
+
mergeAuth(req, config) {
|
|
42
72
|
const authHeader = req.headers.authorization;
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
?
|
|
48
|
-
|
|
73
|
+
// req.id is set by requestId middleware — always a string at this point.
|
|
74
|
+
// Fall back to x-request-id header if somehow req.id isn't set yet.
|
|
75
|
+
const upstream = req.headers["x-request-id"];
|
|
76
|
+
const requestId = req.id
|
|
77
|
+
?? (Array.isArray(upstream) ? upstream[0] : upstream)
|
|
78
|
+
?? `fallback-${Date.now()}`;
|
|
49
79
|
return {
|
|
80
|
+
...config,
|
|
50
81
|
headers: {
|
|
82
|
+
...config?.headers,
|
|
51
83
|
...(authHeader ? { Authorization: authHeader } : {}),
|
|
52
|
-
"x-request-id":
|
|
53
|
-
}
|
|
84
|
+
"x-request-id": requestId,
|
|
85
|
+
},
|
|
54
86
|
};
|
|
55
87
|
}
|
|
56
88
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export * from "./enums";
|
|
2
2
|
export * from "./errors";
|
|
3
|
-
export * from "./
|
|
4
|
-
export * from "./
|
|
3
|
+
export * from "./dtos";
|
|
4
|
+
export * from "./authorization";
|
|
5
5
|
export * from "./utils";
|
|
6
6
|
export * from "./middleware";
|
|
7
7
|
export * from "./types";
|
|
8
8
|
export * from "./jwt";
|
|
9
|
+
export * from "./http";
|
package/dist/index.js
CHANGED
|
@@ -16,9 +16,10 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./enums"), exports);
|
|
18
18
|
__exportStar(require("./errors"), exports);
|
|
19
|
-
__exportStar(require("./
|
|
20
|
-
__exportStar(require("./
|
|
19
|
+
__exportStar(require("./dtos"), exports);
|
|
20
|
+
__exportStar(require("./authorization"), exports);
|
|
21
21
|
__exportStar(require("./utils"), exports);
|
|
22
22
|
__exportStar(require("./middleware"), exports);
|
|
23
23
|
__exportStar(require("./types"), exports);
|
|
24
24
|
__exportStar(require("./jwt"), exports);
|
|
25
|
+
__exportStar(require("./http"), exports);
|
package/dist/jwt/index.d.ts
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
export * from "./jwt-verifier";
|
|
2
|
-
export * from "./service-client";
|
|
1
|
+
export * from "./internal-jwt-verifier";
|
package/dist/jwt/index.js
CHANGED
|
@@ -14,5 +14,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
__exportStar(require("./jwt-verifier"), exports);
|
|
18
|
-
__exportStar(require("./service-client"), exports);
|
|
17
|
+
__exportStar(require("./internal-jwt-verifier"), exports);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { AccountRole, GlobalPermission } from "../enums";
|
|
2
|
+
import { InternalJwtPayload, AccessContext } from "../types";
|
|
3
|
+
import { PermissionCacheService } from "../authorization";
|
|
4
|
+
export declare class InternalJwtVerifier {
|
|
5
|
+
private readonly permissionCache;
|
|
6
|
+
private readonly jwks;
|
|
7
|
+
private readonly issuer;
|
|
8
|
+
private readonly audience;
|
|
9
|
+
constructor(permissionCache: PermissionCacheService);
|
|
10
|
+
verifyInternal(token: string): Promise<InternalJwtPayload>;
|
|
11
|
+
buildAccessContext(token: string): Promise<{
|
|
12
|
+
payload: InternalJwtPayload;
|
|
13
|
+
context: AccessContext;
|
|
14
|
+
}>;
|
|
15
|
+
/**
|
|
16
|
+
* Returns true if the context has the given permission.
|
|
17
|
+
* Machine contexts always return false — they carry no user permissions.
|
|
18
|
+
*/
|
|
19
|
+
static hasPermission(ctx: AccessContext, permission: GlobalPermission): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Throws JWTClaimValidationFailed if the permission is missing.
|
|
22
|
+
* Use in middleware for a one-liner gate:
|
|
23
|
+
* InternalJwtVerifier.requirePermission(ctx, GlobalPermission.MANAGE_ACCOUNTS);
|
|
24
|
+
*/
|
|
25
|
+
static requirePermission(ctx: AccessContext, permission: GlobalPermission): void;
|
|
26
|
+
/**
|
|
27
|
+
* Returns true if the context carries the given role.
|
|
28
|
+
* Machine contexts always return false.
|
|
29
|
+
*/
|
|
30
|
+
static hasRole(ctx: AccessContext, role: AccountRole): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Throws JWTClaimValidationFailed if the role doesn't match.
|
|
33
|
+
*/
|
|
34
|
+
static requireRole(ctx: AccessContext, role: AccountRole): void;
|
|
35
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
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 __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.InternalJwtVerifier = void 0;
|
|
37
|
+
const jose = __importStar(require("jose"));
|
|
38
|
+
/**
|
|
39
|
+
* INTERNAL JWT VERIFIER (@discover-cloud/shared)
|
|
40
|
+
* ──────────────────────────────────────────────────
|
|
41
|
+
* Verifies the internal JWT forwarded by the API Gateway, resolves
|
|
42
|
+
* permissions from Redis, and builds the AccessContext for req.accessContext.
|
|
43
|
+
*
|
|
44
|
+
* Key source: Gateway JWKS
|
|
45
|
+
* GET http://api-gateway:3000/.well-known/jwks.json
|
|
46
|
+
*
|
|
47
|
+
* Flow per request:
|
|
48
|
+
* 1. Verify JWT signature, iss, aud, exp, nbf, typ (jose.jwtVerify)
|
|
49
|
+
* 2. Guard typ === "internal" (token confusion protection)
|
|
50
|
+
* 3. For human tokens: resolve perms from Redis (derive from role on miss)
|
|
51
|
+
* 4. Build and return AccessContext
|
|
52
|
+
*/
|
|
53
|
+
const CLOCK_TOLERANCE_SECONDS = 30;
|
|
54
|
+
const JWKS_CACHE_MAX_AGE_MS = 10 * 60 * 1000; // 10 minutes
|
|
55
|
+
const JWKS_FETCH_TIMEOUT_MS = 5000;
|
|
56
|
+
class InternalJwtVerifier {
|
|
57
|
+
constructor(permissionCache) {
|
|
58
|
+
this.permissionCache = permissionCache;
|
|
59
|
+
const gatewayJwksUri = process.env.GATEWAY_JWKS_URI ??
|
|
60
|
+
"http://api-gateway:3000/.well-known/jwks.json";
|
|
61
|
+
this.issuer = process.env.INTERNAL_JWT_ISSUER ?? "discover-cloud:api-gateway";
|
|
62
|
+
this.audience = process.env.INTERNAL_JWT_AUDIENCE ?? "discover-cloud:internal";
|
|
63
|
+
this.jwks = jose.createRemoteJWKSet(new URL(gatewayJwksUri), {
|
|
64
|
+
cacheMaxAge: JWKS_CACHE_MAX_AGE_MS,
|
|
65
|
+
timeoutDuration: JWKS_FETCH_TIMEOUT_MS,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/* ----------------------------------------------------------------
|
|
69
|
+
verifyInternal
|
|
70
|
+
Full cryptographic verification + typ guard + clock skew tolerance.
|
|
71
|
+
Returns the raw verified payload.
|
|
72
|
+
|
|
73
|
+
Use when you need raw JWT claims (e.g. jti for blacklisting).
|
|
74
|
+
For everything else, use buildAccessContext().
|
|
75
|
+
---------------------------------------------------------------- */
|
|
76
|
+
async verifyInternal(token) {
|
|
77
|
+
// jose.jwtVerify accepts string directly — the Uint8Array overload is
|
|
78
|
+
// for raw JWS. The type error comes from jose's union overloads; casting
|
|
79
|
+
// to the correct overload signature resolves it.
|
|
80
|
+
const { payload } = await jose.jwtVerify(token, this.jwks, {
|
|
81
|
+
issuer: this.issuer,
|
|
82
|
+
audience: this.audience,
|
|
83
|
+
algorithms: ["RS256"],
|
|
84
|
+
clockTolerance: CLOCK_TOLERANCE_SECONDS,
|
|
85
|
+
});
|
|
86
|
+
// typ guard — rejects external tokens accidentally forwarded downstream
|
|
87
|
+
if (payload.typ !== "internal") {
|
|
88
|
+
throw new jose.errors.JWTClaimValidationFailed(`Token type mismatch: expected "internal", got "${String(payload.typ)}". ` +
|
|
89
|
+
`Ensure clients are not forwarding external tokens past the gateway.`, payload, "typ", "check_failed");
|
|
90
|
+
}
|
|
91
|
+
return payload;
|
|
92
|
+
}
|
|
93
|
+
/* ----------------------------------------------------------------
|
|
94
|
+
buildAccessContext
|
|
95
|
+
Primary method — called by RequireAuthMiddleware on every request.
|
|
96
|
+
Verifies the token, resolves permissions, returns payload + context.
|
|
97
|
+
|
|
98
|
+
Usage in auth middleware:
|
|
99
|
+
const token = extractBearer(req);
|
|
100
|
+
const { payload, context } = await verifier.buildAccessContext(token);
|
|
101
|
+
req.internalAuth = payload;
|
|
102
|
+
req.accessContext = context;
|
|
103
|
+
---------------------------------------------------------------- */
|
|
104
|
+
async buildAccessContext(token) {
|
|
105
|
+
const payload = await this.verifyInternal(token);
|
|
106
|
+
// Machine token — no user context, no permission cache lookup
|
|
107
|
+
if (payload.isMachine === true) {
|
|
108
|
+
const machinePayload = payload;
|
|
109
|
+
const context = {
|
|
110
|
+
kind: "machine",
|
|
111
|
+
serviceId: machinePayload.serviceId,
|
|
112
|
+
};
|
|
113
|
+
return { payload, context };
|
|
114
|
+
}
|
|
115
|
+
// Human token — resolve permissions from Redis (derive from role on miss)
|
|
116
|
+
const humanPayload = payload;
|
|
117
|
+
const perms = await this.permissionCache.resolve(humanPayload.accountId, humanPayload.accountRole);
|
|
118
|
+
const context = {
|
|
119
|
+
kind: "human",
|
|
120
|
+
accountId: humanPayload.accountId,
|
|
121
|
+
accountRole: humanPayload.accountRole,
|
|
122
|
+
perms, // populated from cache — never from the JWT
|
|
123
|
+
};
|
|
124
|
+
return { payload, context };
|
|
125
|
+
}
|
|
126
|
+
/* ----------------------------------------------------------------
|
|
127
|
+
Static helpers — use on req.accessContext after auth middleware runs
|
|
128
|
+
---------------------------------------------------------------- */
|
|
129
|
+
/**
|
|
130
|
+
* Returns true if the context has the given permission.
|
|
131
|
+
* Machine contexts always return false — they carry no user permissions.
|
|
132
|
+
*/
|
|
133
|
+
static hasPermission(ctx, permission) {
|
|
134
|
+
return ctx.kind === "human" && ctx.perms.includes(permission);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Throws JWTClaimValidationFailed if the permission is missing.
|
|
138
|
+
* Use in middleware for a one-liner gate:
|
|
139
|
+
* InternalJwtVerifier.requirePermission(ctx, GlobalPermission.MANAGE_ACCOUNTS);
|
|
140
|
+
*/
|
|
141
|
+
static requirePermission(ctx, permission) {
|
|
142
|
+
if (!InternalJwtVerifier.hasPermission(ctx, permission)) {
|
|
143
|
+
throw new jose.errors.JWTClaimValidationFailed(`Missing required permission: ${permission}`, {}, "perms", "check_failed");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Returns true if the context carries the given role.
|
|
148
|
+
* Machine contexts always return false.
|
|
149
|
+
*/
|
|
150
|
+
static hasRole(ctx, role) {
|
|
151
|
+
return ctx.kind === "human" && ctx.accountRole === role;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Throws JWTClaimValidationFailed if the role doesn't match.
|
|
155
|
+
*/
|
|
156
|
+
static requireRole(ctx, role) {
|
|
157
|
+
if (!InternalJwtVerifier.hasRole(ctx, role)) {
|
|
158
|
+
throw new jose.errors.JWTClaimValidationFailed(`Missing required role: ${role}`, {}, "role", "check_failed");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
exports.InternalJwtVerifier = InternalJwtVerifier;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from "express";
|
|
2
|
+
import { GlobalPermission } from "../enums";
|
|
3
|
+
/**
|
|
4
|
+
* AUTHORIZE MIDDLEWARE
|
|
5
|
+
* ─────────────────────
|
|
6
|
+
* Permission-gates a route. Must run AFTER require-auth (depends on req.accessContext).
|
|
7
|
+
* Uses InternalJwtVerifier.hasPermission — permissions come from the Redis
|
|
8
|
+
* permission cache loaded by RequireAuthMiddleware, not from the JWT itself.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* router.delete("/accounts/:id",
|
|
12
|
+
* requireAuth,
|
|
13
|
+
* authorize(GlobalPermission.DELETE_ACCOUNT),
|
|
14
|
+
* controller.delete
|
|
15
|
+
* );
|
|
16
|
+
*/
|
|
17
|
+
export declare const authorize: (permission: GlobalPermission) => (req: Request, res: Response, next: NextFunction) => void;
|
|
18
|
+
/**
|
|
19
|
+
* authorizeAny
|
|
20
|
+
* Passes if the context has AT LEAST ONE of the provided permissions.
|
|
21
|
+
*/
|
|
22
|
+
export declare const authorizeAny: (...permissions: GlobalPermission[]) => (req: Request, res: Response, next: NextFunction) => void;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.authorizeAny = exports.authorize = void 0;
|
|
4
|
+
const internal_jwt_verifier_1 = require("../jwt/internal-jwt-verifier");
|
|
5
|
+
/**
|
|
6
|
+
* AUTHORIZE MIDDLEWARE
|
|
7
|
+
* ─────────────────────
|
|
8
|
+
* Permission-gates a route. Must run AFTER require-auth (depends on req.accessContext).
|
|
9
|
+
* Uses InternalJwtVerifier.hasPermission — permissions come from the Redis
|
|
10
|
+
* permission cache loaded by RequireAuthMiddleware, not from the JWT itself.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* router.delete("/accounts/:id",
|
|
14
|
+
* requireAuth,
|
|
15
|
+
* authorize(GlobalPermission.DELETE_ACCOUNT),
|
|
16
|
+
* controller.delete
|
|
17
|
+
* );
|
|
18
|
+
*/
|
|
19
|
+
const authorize = (permission) => {
|
|
20
|
+
return (req, res, next) => {
|
|
21
|
+
// 1. Ensure RequireAuthMiddleware has already run
|
|
22
|
+
if (!req.accessContext) {
|
|
23
|
+
res.status(401).json({
|
|
24
|
+
success: false,
|
|
25
|
+
error: {
|
|
26
|
+
message: "Unauthorized: No access context",
|
|
27
|
+
code: "UNAUTHORIZED",
|
|
28
|
+
requestId: req.id,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// 2. Check permission against accessContext.perms (loaded from Redis)
|
|
34
|
+
if (!internal_jwt_verifier_1.InternalJwtVerifier.hasPermission(req.accessContext, permission)) {
|
|
35
|
+
res.status(403).json({
|
|
36
|
+
success: false,
|
|
37
|
+
error: {
|
|
38
|
+
message: `Forbidden: Missing permission ${permission}`,
|
|
39
|
+
code: "FORBIDDEN",
|
|
40
|
+
requestId: req.id,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
next();
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
exports.authorize = authorize;
|
|
49
|
+
/**
|
|
50
|
+
* authorizeAny
|
|
51
|
+
* Passes if the context has AT LEAST ONE of the provided permissions.
|
|
52
|
+
*/
|
|
53
|
+
const authorizeAny = (...permissions) => {
|
|
54
|
+
return (req, res, next) => {
|
|
55
|
+
if (!req.accessContext) {
|
|
56
|
+
res.status(401).json({
|
|
57
|
+
success: false,
|
|
58
|
+
error: { message: "Unauthorized: No access context", code: "UNAUTHORIZED", requestId: req.id },
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const hasAny = permissions.some((p) => internal_jwt_verifier_1.InternalJwtVerifier.hasPermission(req.accessContext, p));
|
|
63
|
+
if (!hasAny) {
|
|
64
|
+
res.status(403).json({
|
|
65
|
+
success: false,
|
|
66
|
+
error: {
|
|
67
|
+
message: `Forbidden: Missing one of [${permissions.join(", ")}]`,
|
|
68
|
+
code: "FORBIDDEN",
|
|
69
|
+
requestId: req.id,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
next();
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
exports.authorizeAny = authorizeAny;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from "express";
|
|
2
|
+
/**
|
|
3
|
+
* GLOBAL ERROR HANDLER
|
|
4
|
+
* ─────────────────────
|
|
5
|
+
* Centralized error handler for all Express services.
|
|
6
|
+
*
|
|
7
|
+
* Handles in order:
|
|
8
|
+
* 1. ZodError → 400 with flattened validation details
|
|
9
|
+
* 2. AppError → mapped statusCode + code
|
|
10
|
+
* 3. Unknown → 500 Internal Server Error (internals never leaked)
|
|
11
|
+
*
|
|
12
|
+
* Every response includes requestId for traceability.
|
|
13
|
+
*/
|
|
14
|
+
export declare class GlobalErrorHandler {
|
|
15
|
+
static handle(err: unknown, req: Request, res: Response, _next: NextFunction): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GlobalErrorHandler = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const errors_1 = require("../errors");
|
|
6
|
+
const utils_1 = require("../utils");
|
|
7
|
+
/**
|
|
8
|
+
* GLOBAL ERROR HANDLER
|
|
9
|
+
* ─────────────────────
|
|
10
|
+
* Centralized error handler for all Express services.
|
|
11
|
+
*
|
|
12
|
+
* Handles in order:
|
|
13
|
+
* 1. ZodError → 400 with flattened validation details
|
|
14
|
+
* 2. AppError → mapped statusCode + code
|
|
15
|
+
* 3. Unknown → 500 Internal Server Error (internals never leaked)
|
|
16
|
+
*
|
|
17
|
+
* Every response includes requestId for traceability.
|
|
18
|
+
*/
|
|
19
|
+
class GlobalErrorHandler {
|
|
20
|
+
static handle(err, req, res, _next) {
|
|
21
|
+
if (process.env.NODE_ENV !== "production") {
|
|
22
|
+
console.error(`[req ${req.id}]`, err);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
console.error(`[req ${req.id}]`, err instanceof Error ? err.message : err);
|
|
26
|
+
}
|
|
27
|
+
// 1. Zod validation errors
|
|
28
|
+
if (err instanceof zod_1.ZodError) {
|
|
29
|
+
(0, utils_1.failure)(res, req, "Validation failed", "VALIDATION_ERROR", 400, err.flatten() // default flatten — preserves fieldErrors + formErrors + codes
|
|
30
|
+
);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// 2. Known AppError subclasses — instanceof, never duck-typing
|
|
34
|
+
if (err instanceof errors_1.AppError) {
|
|
35
|
+
(0, utils_1.failure)(res, req, err.message, err.code, err.statusCode, err.details);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
// 3. Unexpected errors — never leak internals to the client
|
|
39
|
+
(0, utils_1.failure)(res, req, "Internal Server Error", "INTERNAL_SERVER_ERROR", 500);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.GlobalErrorHandler = GlobalErrorHandler;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
export * from "./error-handler";
|
|
2
|
-
export * from "./validate";
|
|
3
|
-
export * from "./request-id";
|
|
4
|
-
export * from "./
|
|
5
|
-
export * from "./authorize";
|
|
1
|
+
export * from "./error-handler.middleware";
|
|
2
|
+
export * from "./validate.middleware";
|
|
3
|
+
export * from "./request-id.middleware";
|
|
4
|
+
export * from "./authorize.middleware";
|
package/dist/middleware/index.js
CHANGED
|
@@ -14,8 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
__exportStar(require("./error-handler"), exports);
|
|
18
|
-
__exportStar(require("./validate"), exports);
|
|
19
|
-
__exportStar(require("./request-id"), exports);
|
|
20
|
-
__exportStar(require("./
|
|
21
|
-
__exportStar(require("./authorize"), exports);
|
|
17
|
+
__exportStar(require("./error-handler.middleware"), exports);
|
|
18
|
+
__exportStar(require("./validate.middleware"), exports);
|
|
19
|
+
__exportStar(require("./request-id.middleware"), exports);
|
|
20
|
+
__exportStar(require("./authorize.middleware"), exports);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from "express";
|
|
2
|
+
/**
|
|
3
|
+
* REQUEST ID MIDDLEWARE
|
|
4
|
+
* ──────────────────────
|
|
5
|
+
* Attaches a unique request ID to req.id for distributed tracing.
|
|
6
|
+
*
|
|
7
|
+
* Priority:
|
|
8
|
+
* 1. x-request-id header forwarded by the API Gateway (or load balancer)
|
|
9
|
+
* 2. Generate a new UUID if no upstream header is present
|
|
10
|
+
*
|
|
11
|
+
* Fix vs original:
|
|
12
|
+
* The original always generated a new UUID, breaking distributed tracing.
|
|
13
|
+
* If the gateway mints a requestId and forwards it via x-request-id,
|
|
14
|
+
* the downstream service must carry that same ID — otherwise logs across
|
|
15
|
+
* services cannot be correlated.
|
|
16
|
+
*
|
|
17
|
+
* Also sets x-request-id on the response so clients and proxies can
|
|
18
|
+
* correlate their logs with the service's logs.
|
|
19
|
+
*/
|
|
20
|
+
export declare const requestId: (req: Request, res: Response, next: NextFunction) => void;
|