@discover-cloud/shared 1.0.0 → 1.0.1
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/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 +2 -1
- package/dist/index.js +2 -1
- 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 +23 -0
- package/dist/middleware/error-handler.middleware.js +52 -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/response.d.ts +2 -1
- package/dist/utils/response.js +6 -3
- package/package.json +3 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export * from "./enums";
|
|
2
2
|
export * from "./errors";
|
|
3
3
|
export * from "./dto";
|
|
4
|
-
export * from "./
|
|
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
|
@@ -17,8 +17,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
17
17
|
__exportStar(require("./enums"), exports);
|
|
18
18
|
__exportStar(require("./errors"), exports);
|
|
19
19
|
__exportStar(require("./dto"), exports);
|
|
20
|
-
__exportStar(require("./
|
|
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,23 @@
|
|
|
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 (BadRequestError, NotFoundError, etc.)
|
|
10
|
+
* 3. Unknown → 500 Internal Server Error
|
|
11
|
+
*
|
|
12
|
+
* Every response includes requestId for traceability.
|
|
13
|
+
*
|
|
14
|
+
* Fix vs original:
|
|
15
|
+
* - Replaced unsafe duck-type cast `(err as { statusCode? })` with
|
|
16
|
+
* proper `instanceof AppError` check. Duck-typing any error with a
|
|
17
|
+
* statusCode property as an AppError is a bug vector.
|
|
18
|
+
* - req.id included in every error response
|
|
19
|
+
* - Stack trace logged only in non-production environments
|
|
20
|
+
*/
|
|
21
|
+
export declare class GlobalErrorHandler {
|
|
22
|
+
static handle(err: unknown, req: Request, res: Response, _next: NextFunction): void;
|
|
23
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GlobalErrorHandler = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const app_error_1 = require("../errors/app-error");
|
|
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 (BadRequestError, NotFoundError, etc.)
|
|
15
|
+
* 3. Unknown → 500 Internal Server Error
|
|
16
|
+
*
|
|
17
|
+
* Every response includes requestId for traceability.
|
|
18
|
+
*
|
|
19
|
+
* Fix vs original:
|
|
20
|
+
* - Replaced unsafe duck-type cast `(err as { statusCode? })` with
|
|
21
|
+
* proper `instanceof AppError` check. Duck-typing any error with a
|
|
22
|
+
* statusCode property as an AppError is a bug vector.
|
|
23
|
+
* - req.id included in every error response
|
|
24
|
+
* - Stack trace logged only in non-production environments
|
|
25
|
+
*/
|
|
26
|
+
class GlobalErrorHandler {
|
|
27
|
+
static handle(err, req, res, _next) {
|
|
28
|
+
const requestId = req.id;
|
|
29
|
+
// Always log — replace with your pino/winston logger in production
|
|
30
|
+
if (process.env.NODE_ENV !== "production") {
|
|
31
|
+
console.error(`[req ${requestId}]`, err);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
// In production, log without the stack trace in the response
|
|
35
|
+
console.error(`[req ${requestId}]`, err instanceof Error ? err.message : err);
|
|
36
|
+
}
|
|
37
|
+
// 1. Zod validation errors
|
|
38
|
+
if (err instanceof zod_1.ZodError) {
|
|
39
|
+
(0, utils_1.failure)(res, "Validation failed", "VALIDATION_ERROR", 400, err.flatten((issue) => issue.message));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// 2. Known AppError subclasses (BadRequestError, NotFoundError, etc.)
|
|
43
|
+
// instanceof check — safe, no duck-typing
|
|
44
|
+
if (err instanceof app_error_1.AppError) {
|
|
45
|
+
(0, utils_1.failure)(res, err.message, err.code, err.statusCode, err.details);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// 3. Unhandled / unexpected errors — never leak internals
|
|
49
|
+
(0, utils_1.failure)(res, "Internal Server Error", "INTERNAL_SERVER_ERROR", 500);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
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;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requestId = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
/**
|
|
6
|
+
* REQUEST ID MIDDLEWARE
|
|
7
|
+
* ──────────────────────
|
|
8
|
+
* Attaches a unique request ID to req.id for distributed tracing.
|
|
9
|
+
*
|
|
10
|
+
* Priority:
|
|
11
|
+
* 1. x-request-id header forwarded by the API Gateway (or load balancer)
|
|
12
|
+
* 2. Generate a new UUID if no upstream header is present
|
|
13
|
+
*
|
|
14
|
+
* Fix vs original:
|
|
15
|
+
* The original always generated a new UUID, breaking distributed tracing.
|
|
16
|
+
* If the gateway mints a requestId and forwards it via x-request-id,
|
|
17
|
+
* the downstream service must carry that same ID — otherwise logs across
|
|
18
|
+
* services cannot be correlated.
|
|
19
|
+
*
|
|
20
|
+
* Also sets x-request-id on the response so clients and proxies can
|
|
21
|
+
* correlate their logs with the service's logs.
|
|
22
|
+
*/
|
|
23
|
+
const requestId = (req, res, next) => {
|
|
24
|
+
const upstream = req.headers["x-request-id"];
|
|
25
|
+
// Header can technically be a string array (multi-value) — take the first
|
|
26
|
+
const id = Array.isArray(upstream)
|
|
27
|
+
? upstream[0]
|
|
28
|
+
: upstream ?? (0, crypto_1.randomUUID)();
|
|
29
|
+
req.id = id;
|
|
30
|
+
// Echo back on the response for client-side correlation
|
|
31
|
+
res.setHeader("x-request-id", id);
|
|
32
|
+
next();
|
|
33
|
+
};
|
|
34
|
+
exports.requestId = requestId;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from "express";
|
|
2
|
+
import { ZodType } from "zod";
|
|
3
|
+
/**
|
|
4
|
+
* VALIDATOR MIDDLEWARE
|
|
5
|
+
* ─────────────────────
|
|
6
|
+
* Validates and parses request input using a Zod schema.
|
|
7
|
+
* On success, attaches the parsed data to req.validated.
|
|
8
|
+
* On failure, passes a ZodError to GlobalErrorHandler → 400 response.
|
|
9
|
+
*
|
|
10
|
+
* Changes vs original:
|
|
11
|
+
* - Added "headers" as a valid source (useful for API key / custom header validation)
|
|
12
|
+
* - req.validated typed as unknown — consumers must narrow via z.infer at route level
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* router.post("/orders",
|
|
16
|
+
* requireAuth,
|
|
17
|
+
* Validator.validate(CreateOrderSchema, "body"),
|
|
18
|
+
* controller.create
|
|
19
|
+
* );
|
|
20
|
+
*
|
|
21
|
+
* // In controller:
|
|
22
|
+
* const body = req.validated as z.infer<typeof CreateOrderSchema>;
|
|
23
|
+
*/
|
|
24
|
+
export declare class Validator {
|
|
25
|
+
static validate<T>(schema: ZodType<T>, source?: "body" | "params" | "query" | "headers"): (req: Request, _res: Response, next: NextFunction) => void;
|
|
26
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Validator = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* VALIDATOR MIDDLEWARE
|
|
6
|
+
* ─────────────────────
|
|
7
|
+
* Validates and parses request input using a Zod schema.
|
|
8
|
+
* On success, attaches the parsed data to req.validated.
|
|
9
|
+
* On failure, passes a ZodError to GlobalErrorHandler → 400 response.
|
|
10
|
+
*
|
|
11
|
+
* Changes vs original:
|
|
12
|
+
* - Added "headers" as a valid source (useful for API key / custom header validation)
|
|
13
|
+
* - req.validated typed as unknown — consumers must narrow via z.infer at route level
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* router.post("/orders",
|
|
17
|
+
* requireAuth,
|
|
18
|
+
* Validator.validate(CreateOrderSchema, "body"),
|
|
19
|
+
* controller.create
|
|
20
|
+
* );
|
|
21
|
+
*
|
|
22
|
+
* // In controller:
|
|
23
|
+
* const body = req.validated as z.infer<typeof CreateOrderSchema>;
|
|
24
|
+
*/
|
|
25
|
+
class Validator {
|
|
26
|
+
static validate(schema, source = "body") {
|
|
27
|
+
return (req, _res, next) => {
|
|
28
|
+
const result = schema.safeParse(req[source]);
|
|
29
|
+
if (!result.success) {
|
|
30
|
+
// ZodError propagates to GlobalErrorHandler → 400 with flatten()
|
|
31
|
+
next(result.error);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// req.validated typed as unknown in express.d.ts
|
|
35
|
+
// Narrow to your schema type at the route/controller level
|
|
36
|
+
req.validated = result.data;
|
|
37
|
+
next();
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
exports.Validator = Validator;
|