@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.
Files changed (61) hide show
  1. package/dist/authorization/index.d.ts +2 -0
  2. package/dist/authorization/index.js +18 -0
  3. package/dist/authorization/permission-cache.service.d.ts +12 -0
  4. package/dist/authorization/permission-cache.service.js +132 -0
  5. package/dist/authorization/permissions.d.ts +74 -0
  6. package/dist/authorization/permissions.js +171 -0
  7. package/dist/dto/auth-service.dtos.d.ts +5 -12
  8. package/dist/dto/response.dtos.d.ts +34 -27
  9. package/dist/dto/response.dtos.js +4 -0
  10. package/dist/dto/user-service.dtos.d.ts +9 -13
  11. package/dist/dtos/auth-service.dto.d.ts +44 -0
  12. package/dist/dtos/auth-service.dto.js +2 -0
  13. package/dist/dtos/index.d.ts +3 -0
  14. package/dist/dtos/index.js +19 -0
  15. package/dist/dtos/response.dto.d.ts +55 -0
  16. package/dist/dtos/response.dto.js +6 -0
  17. package/dist/dtos/user-service.dto.d.ts +50 -0
  18. package/dist/dtos/user-service.dto.js +2 -0
  19. package/dist/enums/domain.enums.d.ts +42 -0
  20. package/dist/enums/domain.enums.js +79 -0
  21. package/dist/enums/index.d.ts +2 -3
  22. package/dist/enums/index.js +2 -3
  23. package/dist/enums/permissions.enums.d.ts +124 -0
  24. package/dist/enums/permissions.enums.js +141 -0
  25. package/dist/errors/app-error.d.ts +19 -2
  26. package/dist/errors/app-error.js +17 -2
  27. package/dist/errors/http-errors.d.ts +18 -0
  28. package/dist/errors/http-errors.js +25 -1
  29. package/dist/http/index.d.ts +1 -0
  30. package/dist/http/index.js +17 -0
  31. package/dist/http/service-client.d.ts +23 -7
  32. package/dist/http/service-client.js +54 -22
  33. package/dist/index.d.ts +3 -2
  34. package/dist/index.js +3 -2
  35. package/dist/jwt/index.d.ts +1 -2
  36. package/dist/jwt/index.js +1 -2
  37. package/dist/jwt/internal-jwt-verifier.d.ts +35 -0
  38. package/dist/jwt/internal-jwt-verifier.js +162 -0
  39. package/dist/middleware/authorize.middleware.d.ts +22 -0
  40. package/dist/middleware/authorize.middleware.js +77 -0
  41. package/dist/middleware/error-handler.middleware.d.ts +16 -0
  42. package/dist/middleware/error-handler.middleware.js +42 -0
  43. package/dist/middleware/index.d.ts +4 -5
  44. package/dist/middleware/index.js +4 -5
  45. package/dist/middleware/request-id.middleware.d.ts +20 -0
  46. package/dist/middleware/request-id.middleware.js +34 -0
  47. package/dist/middleware/validate.middleware.d.ts +26 -0
  48. package/dist/middleware/validate.middleware.js +41 -0
  49. package/dist/types/express.types.d.ts +148 -0
  50. package/dist/types/express.types.js +42 -0
  51. package/dist/types/index.d.ts +1 -1
  52. package/dist/types/index.js +1 -1
  53. package/dist/utils/index.d.ts +2 -1
  54. package/dist/utils/index.js +2 -1
  55. package/dist/utils/logger.utils.d.ts +51 -0
  56. package/dist/utils/logger.utils.js +62 -0
  57. package/dist/utils/response.d.ts +2 -1
  58. package/dist/utils/response.js +6 -3
  59. package/dist/utils/response.utils.d.ts +12 -0
  60. package/dist/utils/response.utils.js +42 -0
  61. 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 axios, { AxiosInstance } from "axios";
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
- * The new magic method.
9
- * Pass the current Express Request object, and it automatically
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
- getWithAuth(url: string, req: Request): Promise<axios.AxiosResponse<any, any, {}>>;
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
- return (err.response?.status ?? 0) >= 500;
26
- }
41
+ const status = err.response?.status ?? 0;
42
+ return status >= 500;
43
+ },
27
44
  });
28
45
  }
29
- /**
30
- * The new magic method.
31
- * Pass the current Express Request object, and it automatically
32
- * extracts the Gateway's Bearer token and forwards it.
33
- */
34
- async getWithAuth(url, req) {
35
- return this.http.get(url, this.createAuthHeader(req));
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 postWithAuth(url, data, req) {
38
- return this.http.post(url, data, this.createAuthHeader(req));
58
+ async patchWithAuth(url, data, req, config) {
59
+ return this.http.patch(url, data, this.mergeAuth(req, config));
39
60
  }
40
- // --- Helper to extract and forward the token ---
41
- createAuthHeader(req) {
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
- // Express headers can technically be string arrays.
44
- // We grab the first item if it's an array, or fallback to a generated ID.
45
- const rawReqId = req.headers["x-request-id"];
46
- const reqId = Array.isArray(rawReqId)
47
- ? rawReqId[0]
48
- : rawReqId || `req_${Math.random().toString(36).slice(2)}`;
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": reqId
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 "./dto";
4
- export * from "./security";
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("./dto"), exports);
20
- __exportStar(require("./security"), exports);
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);
@@ -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 "./require-auth";
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";
@@ -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("./require-auth"), exports);
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;