@discover-cloud/shared 1.0.11 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authorization/permission-cache.service.d.ts +14 -1
- package/dist/authorization/permission-cache.service.js +84 -16
- package/dist/authorization/permissions.d.ts +19 -15
- package/dist/authorization/permissions.js +24 -21
- package/dist/dtos/auth-service.dto.d.ts +13 -0
- package/dist/dtos/cloud-service.dto.d.ts +25 -10
- package/dist/dtos/insights-service.dto.d.ts +24 -8
- package/dist/dtos/response.dto.d.ts +80 -10
- package/dist/dtos/response.dto.js +23 -4
- package/dist/dtos/user-service.dto.d.ts +7 -0
- package/dist/enums/domain.enums.d.ts +45 -14
- package/dist/enums/domain.enums.js +40 -6
- package/dist/enums/permissions.enums.d.ts +12 -52
- package/dist/enums/permissions.enums.js +55 -59
- package/dist/errors/app-error.d.ts +17 -14
- package/dist/errors/app-error.js +19 -15
- package/dist/errors/http-errors.d.ts +12 -5
- package/dist/errors/http-errors.js +27 -5
- package/dist/http/service-client.d.ts +37 -12
- package/dist/http/service-client.js +43 -17
- package/dist/jwt/internal-jwt-verifier.d.ts +9 -3
- package/dist/jwt/internal-jwt-verifier.js +49 -26
- package/dist/middleware/authorize.middleware.d.ts +38 -6
- package/dist/middleware/authorize.middleware.js +62 -37
- package/dist/middleware/error-handler.middleware.d.ts +21 -7
- package/dist/middleware/error-handler.middleware.js +42 -14
- package/dist/middleware/request-id.middleware.d.ts +12 -10
- package/dist/middleware/request-id.middleware.js +15 -15
- package/dist/middleware/validate.middleware.d.ts +22 -13
- package/dist/middleware/validate.middleware.js +25 -16
- package/dist/middleware/validated-merge.middleware.js +1 -2
- package/dist/types/express.types.d.ts +37 -32
- package/dist/types/express.types.js +13 -11
- package/dist/utils/date.utils.d.ts +18 -4
- package/dist/utils/date.utils.js +18 -4
- package/dist/utils/env.d.ts +46 -0
- package/dist/utils/env.js +61 -0
- package/dist/utils/logger.utils.d.ts +31 -16
- package/dist/utils/logger.utils.js +55 -20
- package/dist/utils/response.utils.d.ts +47 -5
- package/dist/utils/response.utils.js +50 -7
- package/package.json +1 -1
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
import { Request, Response, NextFunction } from "express";
|
|
2
2
|
/**
|
|
3
|
-
* GLOBAL ERROR HANDLER
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* GLOBAL ERROR HANDLER (@discover-cloud/shared)
|
|
4
|
+
* ────────────────────────────────────────────────
|
|
5
|
+
* Centralised Express error handler. Register as the last middleware in
|
|
6
|
+
* every service's app.ts:
|
|
7
|
+
*
|
|
8
|
+
* app.use(GlobalErrorHandler.handle);
|
|
6
9
|
*
|
|
7
10
|
* Handles in order:
|
|
8
|
-
* 1. ZodError → 400 with flattened
|
|
9
|
-
* 2. AppError → mapped statusCode + code
|
|
10
|
-
* 3. Unknown → 500
|
|
11
|
+
* 1. ZodError → 400 VALIDATION_ERROR with flattened field errors
|
|
12
|
+
* 2. AppError → mapped statusCode + code (details forwarded to client)
|
|
13
|
+
* 3. Unknown → 500 INTERNAL_SERVER_ERROR (internals never leaked)
|
|
14
|
+
*
|
|
15
|
+
* Logging strategy:
|
|
16
|
+
* Development — full error object printed for stack traces during development.
|
|
17
|
+
* Production — message only for AppError/Error (no stack, no details in logs).
|
|
18
|
+
* Unknown errors log the raw value at warn level.
|
|
19
|
+
* In production, a structured logger (Pino) replaces console.*
|
|
20
|
+
* in each service — these console calls are captured by stdout.
|
|
11
21
|
*
|
|
12
|
-
*
|
|
22
|
+
* Security:
|
|
23
|
+
* Unknown (unexpected) errors never expose internals to the client.
|
|
24
|
+
* AppError.details IS forwarded — callers that set details on an AppError
|
|
25
|
+
* are intentionally choosing to surface that context (e.g. validation hints).
|
|
26
|
+
* Never put secrets, stack traces, or raw DB errors in AppError.details.
|
|
13
27
|
*/
|
|
14
28
|
export declare class GlobalErrorHandler {
|
|
15
29
|
static handle(err: unknown, req: Request, res: Response, _next: NextFunction): void;
|
|
@@ -5,37 +5,65 @@ const zod_1 = require("zod");
|
|
|
5
5
|
const errors_1 = require("../errors");
|
|
6
6
|
const utils_1 = require("../utils");
|
|
7
7
|
/**
|
|
8
|
-
* GLOBAL ERROR HANDLER
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* GLOBAL ERROR HANDLER (@discover-cloud/shared)
|
|
9
|
+
* ────────────────────────────────────────────────
|
|
10
|
+
* Centralised Express error handler. Register as the last middleware in
|
|
11
|
+
* every service's app.ts:
|
|
12
|
+
*
|
|
13
|
+
* app.use(GlobalErrorHandler.handle);
|
|
11
14
|
*
|
|
12
15
|
* Handles in order:
|
|
13
|
-
* 1. ZodError → 400 with flattened
|
|
14
|
-
* 2. AppError → mapped statusCode + code
|
|
15
|
-
* 3. Unknown → 500
|
|
16
|
+
* 1. ZodError → 400 VALIDATION_ERROR with flattened field errors
|
|
17
|
+
* 2. AppError → mapped statusCode + code (details forwarded to client)
|
|
18
|
+
* 3. Unknown → 500 INTERNAL_SERVER_ERROR (internals never leaked)
|
|
19
|
+
*
|
|
20
|
+
* Logging strategy:
|
|
21
|
+
* Development — full error object printed for stack traces during development.
|
|
22
|
+
* Production — message only for AppError/Error (no stack, no details in logs).
|
|
23
|
+
* Unknown errors log the raw value at warn level.
|
|
24
|
+
* In production, a structured logger (Pino) replaces console.*
|
|
25
|
+
* in each service — these console calls are captured by stdout.
|
|
16
26
|
*
|
|
17
|
-
*
|
|
27
|
+
* Security:
|
|
28
|
+
* Unknown (unexpected) errors never expose internals to the client.
|
|
29
|
+
* AppError.details IS forwarded — callers that set details on an AppError
|
|
30
|
+
* are intentionally choosing to surface that context (e.g. validation hints).
|
|
31
|
+
* Never put secrets, stack traces, or raw DB errors in AppError.details.
|
|
18
32
|
*/
|
|
19
33
|
class GlobalErrorHandler {
|
|
20
34
|
static handle(err, req, res, _next) {
|
|
21
|
-
|
|
35
|
+
// ── Logging ──────────────────────────────────────────────────────────
|
|
36
|
+
if (process.env["NODE_ENV"] !== "production") {
|
|
37
|
+
// Full object in development — stack traces + details are useful locally.
|
|
22
38
|
console.error(`[req ${req.id}]`, err);
|
|
23
39
|
}
|
|
40
|
+
else if (err instanceof Error) {
|
|
41
|
+
// Production: message only — no stack, no details, no internal paths.
|
|
42
|
+
console.error(`[req ${req.id}] ${err.name}: ${err.message}`);
|
|
43
|
+
}
|
|
24
44
|
else {
|
|
25
|
-
|
|
45
|
+
// Non-Error throw (string, object, etc.) — log at warn; this should not happen.
|
|
46
|
+
console.warn(`[req ${req.id}] Non-Error thrown:`, typeof err);
|
|
26
47
|
}
|
|
27
|
-
// 1. Zod validation errors
|
|
48
|
+
// ── 1. Zod validation errors ─────────────────────────────────────────
|
|
28
49
|
if (err instanceof zod_1.ZodError) {
|
|
29
|
-
(
|
|
30
|
-
)
|
|
50
|
+
// flatten() groups errors into fieldErrors (keyed by path) and formErrors
|
|
51
|
+
// (top-level). This shape is stable across Zod versions and is what
|
|
52
|
+
// clients should key on for field-level error display.
|
|
53
|
+
(0, utils_1.failure)(res, req, "Validation failed", "VALIDATION_ERROR", 400, err.flatten());
|
|
31
54
|
return;
|
|
32
55
|
}
|
|
33
|
-
// 2. Known AppError subclasses
|
|
56
|
+
// ── 2. Known AppError subclasses ─────────────────────────────────────
|
|
57
|
+
// instanceof check — no duck-typing, no accidental matches.
|
|
58
|
+
// AppError.details is caller-controlled context (e.g. validation hints)
|
|
59
|
+
// and is intentionally forwarded to the client.
|
|
34
60
|
if (err instanceof errors_1.AppError) {
|
|
35
61
|
(0, utils_1.failure)(res, req, err.message, err.code, err.statusCode, err.details);
|
|
36
62
|
return;
|
|
37
63
|
}
|
|
38
|
-
// 3. Unexpected errors — never leak internals
|
|
64
|
+
// ── 3. Unexpected errors — never leak internals ───────────────────────
|
|
65
|
+
// Generic 500 with no details. The actual error is logged above (production:
|
|
66
|
+
// message only; development: full object). Nothing internal reaches the client.
|
|
39
67
|
(0, utils_1.failure)(res, req, "Internal Server Error", "INTERNAL_SERVER_ERROR", 500);
|
|
40
68
|
}
|
|
41
69
|
}
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import { Request, Response, NextFunction } from "express";
|
|
2
2
|
/**
|
|
3
|
-
* REQUEST ID MIDDLEWARE
|
|
4
|
-
*
|
|
3
|
+
* REQUEST ID MIDDLEWARE (@discover-cloud/shared)
|
|
4
|
+
* ─────────────────────────────────────────────────
|
|
5
5
|
* Attaches a unique request ID to req.id for distributed tracing.
|
|
6
|
+
* Register early in the middleware stack — before any logger or handler
|
|
7
|
+
* that needs to include the request ID in its output.
|
|
6
8
|
*
|
|
7
9
|
* Priority:
|
|
8
10
|
* 1. x-request-id header forwarded by the API Gateway (or load balancer)
|
|
9
|
-
* 2.
|
|
11
|
+
* 2. Newly generated UUID v4 if no upstream header is present
|
|
10
12
|
*
|
|
11
|
-
*
|
|
12
|
-
* The
|
|
13
|
-
* If
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* Why forward the upstream header?
|
|
14
|
+
* The gateway mints a requestId and stamps it on every internal request.
|
|
15
|
+
* If downstream services generate their own IDs, the same logical request
|
|
16
|
+
* carries different IDs in each service's logs, breaking cross-service
|
|
17
|
+
* log correlation. Forwarding the upstream ID keeps the trace consistent.
|
|
16
18
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
+
* The resolved ID is also echoed back on the response via x-request-id
|
|
20
|
+
* so clients and proxies can correlate their own logs with the service's.
|
|
19
21
|
*/
|
|
20
22
|
export declare const requestId: (req: Request, res: Response, next: NextFunction) => void;
|
|
@@ -3,31 +3,31 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.requestId = void 0;
|
|
4
4
|
const crypto_1 = require("crypto");
|
|
5
5
|
/**
|
|
6
|
-
* REQUEST ID MIDDLEWARE
|
|
7
|
-
*
|
|
6
|
+
* REQUEST ID MIDDLEWARE (@discover-cloud/shared)
|
|
7
|
+
* ─────────────────────────────────────────────────
|
|
8
8
|
* Attaches a unique request ID to req.id for distributed tracing.
|
|
9
|
+
* Register early in the middleware stack — before any logger or handler
|
|
10
|
+
* that needs to include the request ID in its output.
|
|
9
11
|
*
|
|
10
12
|
* Priority:
|
|
11
13
|
* 1. x-request-id header forwarded by the API Gateway (or load balancer)
|
|
12
|
-
* 2.
|
|
14
|
+
* 2. Newly generated UUID v4 if no upstream header is present
|
|
13
15
|
*
|
|
14
|
-
*
|
|
15
|
-
* The
|
|
16
|
-
* If
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* Why forward the upstream header?
|
|
17
|
+
* The gateway mints a requestId and stamps it on every internal request.
|
|
18
|
+
* If downstream services generate their own IDs, the same logical request
|
|
19
|
+
* carries different IDs in each service's logs, breaking cross-service
|
|
20
|
+
* log correlation. Forwarding the upstream ID keeps the trace consistent.
|
|
19
21
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
+
* The resolved ID is also echoed back on the response via x-request-id
|
|
23
|
+
* so clients and proxies can correlate their own logs with the service's.
|
|
22
24
|
*/
|
|
23
25
|
const requestId = (req, res, next) => {
|
|
24
26
|
const upstream = req.headers["x-request-id"];
|
|
25
|
-
// Header can
|
|
26
|
-
const id = Array.isArray(upstream)
|
|
27
|
-
? upstream[0]
|
|
28
|
-
: upstream ?? (0, crypto_1.randomUUID)();
|
|
27
|
+
// Header can be a string array (multi-value header) — take the first value.
|
|
28
|
+
const id = Array.isArray(upstream) ? upstream[0] : (upstream ?? (0, crypto_1.randomUUID)());
|
|
29
29
|
req.id = id;
|
|
30
|
-
// Echo back on the response for client-side correlation
|
|
30
|
+
// Echo back on the response for client-side and proxy-side correlation.
|
|
31
31
|
res.setHeader("x-request-id", id);
|
|
32
32
|
next();
|
|
33
33
|
};
|
|
@@ -1,25 +1,34 @@
|
|
|
1
1
|
import { Request, Response, NextFunction } from "express";
|
|
2
2
|
import { ZodType } from "zod";
|
|
3
3
|
/**
|
|
4
|
-
* VALIDATOR MIDDLEWARE
|
|
5
|
-
*
|
|
6
|
-
* Validates and parses request input
|
|
7
|
-
* On success, attaches the parsed data to req.validated.
|
|
8
|
-
* On failure, passes
|
|
4
|
+
* VALIDATOR MIDDLEWARE (@discover-cloud/shared)
|
|
5
|
+
* ────────────────────────────────────────────────
|
|
6
|
+
* Validates and parses request input against a Zod schema.
|
|
7
|
+
* On success, attaches the parsed (coerced + stripped) data to req.validated.
|
|
8
|
+
* On failure, passes the ZodError to next() → GlobalErrorHandler → 400.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Source options:
|
|
11
|
+
* "body" — req.body (POST/PUT/PATCH payloads)
|
|
12
|
+
* "params" — req.params (URL path parameters)
|
|
13
|
+
* "query" — req.query (URL query string)
|
|
14
|
+
* "headers" — req.headers (custom header validation, e.g. API keys)
|
|
15
|
+
*
|
|
16
|
+
* Narrowing req.validated in a controller:
|
|
17
|
+
* req.validated is typed as unknown in express.d.ts — consumers must
|
|
18
|
+
* narrow it to the schema's inferred type at the call site:
|
|
19
|
+
*
|
|
20
|
+
* const body = req.validated as z.infer<typeof CreateOrderSchema>;
|
|
21
|
+
*
|
|
22
|
+
* This is intentional: the middleware is schema-agnostic and cannot
|
|
23
|
+
* carry the generic type through Express's untyped req object.
|
|
13
24
|
*
|
|
14
25
|
* Usage:
|
|
15
|
-
* router.post(
|
|
26
|
+
* router.post(
|
|
27
|
+
* "/orders",
|
|
16
28
|
* requireAuth,
|
|
17
29
|
* Validator.validate(CreateOrderSchema, "body"),
|
|
18
|
-
* controller.create
|
|
30
|
+
* controller.create,
|
|
19
31
|
* );
|
|
20
|
-
*
|
|
21
|
-
* // In controller:
|
|
22
|
-
* const body = req.validated as z.infer<typeof CreateOrderSchema>;
|
|
23
32
|
*/
|
|
24
33
|
export declare class Validator {
|
|
25
34
|
static validate<T>(schema: ZodType<T>, source?: "body" | "params" | "query" | "headers"): (req: Request, _res: Response, next: NextFunction) => void;
|
|
@@ -2,37 +2,46 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Validator = void 0;
|
|
4
4
|
/**
|
|
5
|
-
* VALIDATOR MIDDLEWARE
|
|
6
|
-
*
|
|
7
|
-
* Validates and parses request input
|
|
8
|
-
* On success, attaches the parsed data to req.validated.
|
|
9
|
-
* On failure, passes
|
|
5
|
+
* VALIDATOR MIDDLEWARE (@discover-cloud/shared)
|
|
6
|
+
* ────────────────────────────────────────────────
|
|
7
|
+
* Validates and parses request input against a Zod schema.
|
|
8
|
+
* On success, attaches the parsed (coerced + stripped) data to req.validated.
|
|
9
|
+
* On failure, passes the ZodError to next() → GlobalErrorHandler → 400.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* Source options:
|
|
12
|
+
* "body" — req.body (POST/PUT/PATCH payloads)
|
|
13
|
+
* "params" — req.params (URL path parameters)
|
|
14
|
+
* "query" — req.query (URL query string)
|
|
15
|
+
* "headers" — req.headers (custom header validation, e.g. API keys)
|
|
16
|
+
*
|
|
17
|
+
* Narrowing req.validated in a controller:
|
|
18
|
+
* req.validated is typed as unknown in express.d.ts — consumers must
|
|
19
|
+
* narrow it to the schema's inferred type at the call site:
|
|
20
|
+
*
|
|
21
|
+
* const body = req.validated as z.infer<typeof CreateOrderSchema>;
|
|
22
|
+
*
|
|
23
|
+
* This is intentional: the middleware is schema-agnostic and cannot
|
|
24
|
+
* carry the generic type through Express's untyped req object.
|
|
14
25
|
*
|
|
15
26
|
* Usage:
|
|
16
|
-
* router.post(
|
|
27
|
+
* router.post(
|
|
28
|
+
* "/orders",
|
|
17
29
|
* requireAuth,
|
|
18
30
|
* Validator.validate(CreateOrderSchema, "body"),
|
|
19
|
-
* controller.create
|
|
31
|
+
* controller.create,
|
|
20
32
|
* );
|
|
21
|
-
*
|
|
22
|
-
* // In controller:
|
|
23
|
-
* const body = req.validated as z.infer<typeof CreateOrderSchema>;
|
|
24
33
|
*/
|
|
25
34
|
class Validator {
|
|
26
35
|
static validate(schema, source = "body") {
|
|
27
36
|
return (req, _res, next) => {
|
|
28
37
|
const result = schema.safeParse(req[source]);
|
|
29
38
|
if (!result.success) {
|
|
30
|
-
// ZodError
|
|
39
|
+
// Pass the ZodError to GlobalErrorHandler — it flattens and returns 400.
|
|
31
40
|
next(result.error);
|
|
32
41
|
return;
|
|
33
42
|
}
|
|
34
|
-
//
|
|
35
|
-
//
|
|
43
|
+
// Attach the parsed data (coerced + unknown keys stripped by Zod).
|
|
44
|
+
// req.validated is typed as unknown — narrow to your schema type in the controller.
|
|
36
45
|
req.validated = result.data;
|
|
37
46
|
next();
|
|
38
47
|
};
|
|
@@ -19,8 +19,7 @@ exports.mergeValidated = exports.captureValidated = void 0;
|
|
|
19
19
|
* enough. Casting to unknown first tells TypeScript we know what we're doing.
|
|
20
20
|
*/
|
|
21
21
|
const captureValidated = (key, tempKey) => (req, _res, next) => {
|
|
22
|
-
req[tempKey] =
|
|
23
|
-
req.validated[key];
|
|
22
|
+
req[tempKey] = req.validated[key];
|
|
24
23
|
next();
|
|
25
24
|
};
|
|
26
25
|
exports.captureValidated = captureValidated;
|
|
@@ -1,42 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* EXPRESS TYPE AUGMENTATION + JWT PAYLOAD TYPES
|
|
3
|
-
*
|
|
2
|
+
* EXPRESS TYPE AUGMENTATION + JWT PAYLOAD TYPES (@discover-cloud/shared)
|
|
3
|
+
* ──────────────────────────────────────────────────────────────────────────
|
|
4
4
|
* Single source of truth for everything attached to Express's Request object
|
|
5
5
|
* and all internal JWT payload shapes.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* Import in each service's entrypoint to activate the augmentation:
|
|
8
8
|
* import "@discover-cloud/shared/types/express";
|
|
9
9
|
*
|
|
10
|
-
* ─── Permission flow
|
|
11
|
-
* JWT carries:
|
|
12
|
-
*
|
|
13
|
-
* AccessContext has:
|
|
10
|
+
* ─── Permission flow ────────────────────────────────────────────────────
|
|
11
|
+
* JWT carries: accountId + accountRole (lean payload, no perms)
|
|
12
|
+
* Redis derives: GlobalPermission[] (role map on cache miss)
|
|
13
|
+
* AccessContext has: perms (resolved by auth middleware)
|
|
14
14
|
*
|
|
15
|
-
* perms never touch the JWT — they live in the permission cache and
|
|
16
|
-
* resolved into req.accessContext by RequireAuthMiddleware
|
|
15
|
+
* perms never touch the JWT — they live in the Redis permission cache and
|
|
16
|
+
* are resolved into req.accessContext by RequireAuthMiddleware per request.
|
|
17
|
+
* ────────────────────────────────────────────────────────────────────────
|
|
17
18
|
*/
|
|
18
19
|
import { JWTPayload } from "jose";
|
|
19
|
-
import { AccountRole } from "../enums";
|
|
20
|
-
import { GlobalPermission } from "../enums";
|
|
20
|
+
import { AccountRole, GlobalPermission } from "../enums";
|
|
21
21
|
interface BaseInternalJwtPayload extends JWTPayload {
|
|
22
22
|
/**
|
|
23
|
-
* JWT ID —
|
|
23
|
+
* JWT ID — required.
|
|
24
24
|
* Used for the Redis kill-switch (session revocation / account suspension).
|
|
25
|
-
* Overrides JWTPayload's optional jti with required.
|
|
25
|
+
* Overrides JWTPayload's optional jti with a required string.
|
|
26
26
|
*/
|
|
27
27
|
jti: string;
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Token type discriminator — guards against token confusion attacks.
|
|
30
|
+
* InternalJwtVerifier rejects any token where typ !== "internal",
|
|
31
|
+
* even if it passes signature verification.
|
|
32
|
+
*/
|
|
29
33
|
typ: "internal";
|
|
30
34
|
/**
|
|
31
35
|
* The entity that last minted or forwarded this token.
|
|
32
|
-
* "api-gateway"
|
|
33
|
-
* "
|
|
36
|
+
* "api-gateway" → direct user-initiated request
|
|
37
|
+
* "notification-service" → service-to-service call on behalf of a user
|
|
34
38
|
* Used for audit logging and conditional authorization.
|
|
35
39
|
*/
|
|
36
40
|
caller: string;
|
|
37
41
|
/**
|
|
38
42
|
* Propagated request ID for distributed tracing.
|
|
39
43
|
* Ties together logs across the entire service call chain.
|
|
44
|
+
* Set by the gateway from the incoming x-request-id header.
|
|
40
45
|
*/
|
|
41
46
|
requestId?: string;
|
|
42
47
|
iss: string;
|
|
@@ -46,11 +51,10 @@ interface BaseInternalJwtPayload extends JWTPayload {
|
|
|
46
51
|
nbf: number;
|
|
47
52
|
}
|
|
48
53
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* from globalRolePermissions on a cold cache miss — without a DB call.
|
|
54
|
+
* HumanInternalJwtPayload
|
|
55
|
+
* Issued on behalf of an authenticated user after external JWT verification.
|
|
56
|
+
* accountRole is carried so the permission cache can derive perms from
|
|
57
|
+
* globalRolePermissions on a cold cache miss — without a DB call.
|
|
54
58
|
*/
|
|
55
59
|
export interface HumanInternalJwtPayload extends BaseInternalJwtPayload {
|
|
56
60
|
isMachine: false;
|
|
@@ -59,14 +63,15 @@ export interface HumanInternalJwtPayload extends BaseInternalJwtPayload {
|
|
|
59
63
|
externalJti?: string;
|
|
60
64
|
}
|
|
61
65
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
66
|
+
* MachineInternalJwtPayload
|
|
67
|
+
* Issued for service-to-service calls with no user context.
|
|
68
|
+
* Intentionally carries no user fields — their absence is enforced by the type.
|
|
64
69
|
*/
|
|
65
70
|
export interface MachineInternalJwtPayload extends BaseInternalJwtPayload {
|
|
66
71
|
isMachine: true;
|
|
67
72
|
serviceId: string;
|
|
68
73
|
}
|
|
69
|
-
/**
|
|
74
|
+
/** Full internal JWT payload union — use in verifiers and auth middleware */
|
|
70
75
|
export type InternalJwtPayload = HumanInternalJwtPayload | MachineInternalJwtPayload;
|
|
71
76
|
export interface HumanAccessContext {
|
|
72
77
|
kind: "human";
|
|
@@ -74,7 +79,7 @@ export interface HumanAccessContext {
|
|
|
74
79
|
accountRole: AccountRole;
|
|
75
80
|
/**
|
|
76
81
|
* Resolved from Redis permission cache — NOT from the JWT.
|
|
77
|
-
* Always present by the time req.accessContext is
|
|
82
|
+
* Always present by the time req.accessContext is populated.
|
|
78
83
|
* Invalidated on role change, suspension, and account deletion.
|
|
79
84
|
*/
|
|
80
85
|
perms: GlobalPermission[];
|
|
@@ -113,13 +118,13 @@ declare module "express-serve-static-core" {
|
|
|
113
118
|
interface Request {
|
|
114
119
|
/**
|
|
115
120
|
* Unique request ID — set by requestId middleware.
|
|
116
|
-
* Respects upstream x-request-id
|
|
121
|
+
* Respects upstream x-request-id forwarded by the gateway / load balancer.
|
|
117
122
|
* Always present after requestId middleware runs.
|
|
118
123
|
*/
|
|
119
124
|
id: string;
|
|
120
125
|
/**
|
|
121
|
-
* Validated + parsed request body/query/
|
|
122
|
-
* Typed as unknown — narrow at the route level:
|
|
126
|
+
* Validated + parsed request input (body / params / query / headers).
|
|
127
|
+
* Typed as unknown — narrow at the route level using z.infer:
|
|
123
128
|
*
|
|
124
129
|
* const body = req.validated as z.infer<typeof CreateOrderSchema>;
|
|
125
130
|
*
|
|
@@ -130,13 +135,13 @@ declare module "express-serve-static-core" {
|
|
|
130
135
|
* Raw verified internal JWT payload.
|
|
131
136
|
* Set by RequireAuthMiddleware after InternalJwtVerifier.verifyInternal().
|
|
132
137
|
*
|
|
133
|
-
* Prefer req.accessContext in route handlers.
|
|
134
|
-
* Access
|
|
138
|
+
* Prefer req.accessContext in route handlers and controllers.
|
|
139
|
+
* Access internalAuth only when you need raw JWT claims (e.g. jti).
|
|
135
140
|
*/
|
|
136
141
|
internalAuth?: InternalJwtPayload;
|
|
137
142
|
/**
|
|
138
|
-
* Clean access context — built from internalAuth + permission cache.
|
|
139
|
-
* This is what route handlers, controllers, and domain services
|
|
143
|
+
* Clean access context — built from internalAuth + Redis permission cache.
|
|
144
|
+
* This is what route handlers, controllers, and domain services consume.
|
|
140
145
|
*
|
|
141
146
|
* if (isHumanContext(req.accessContext)) {
|
|
142
147
|
* const { accountId, accountRole, perms } = req.accessContext;
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* EXPRESS TYPE AUGMENTATION + JWT PAYLOAD TYPES
|
|
4
|
-
*
|
|
3
|
+
* EXPRESS TYPE AUGMENTATION + JWT PAYLOAD TYPES (@discover-cloud/shared)
|
|
4
|
+
* ──────────────────────────────────────────────────────────────────────────
|
|
5
5
|
* Single source of truth for everything attached to Express's Request object
|
|
6
6
|
* and all internal JWT payload shapes.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* Import in each service's entrypoint to activate the augmentation:
|
|
9
9
|
* import "@discover-cloud/shared/types/express";
|
|
10
10
|
*
|
|
11
|
-
* ─── Permission flow
|
|
12
|
-
* JWT carries:
|
|
13
|
-
*
|
|
14
|
-
* AccessContext has:
|
|
11
|
+
* ─── Permission flow ────────────────────────────────────────────────────
|
|
12
|
+
* JWT carries: accountId + accountRole (lean payload, no perms)
|
|
13
|
+
* Redis derives: GlobalPermission[] (role map on cache miss)
|
|
14
|
+
* AccessContext has: perms (resolved by auth middleware)
|
|
15
15
|
*
|
|
16
|
-
* perms never touch the JWT — they live in the permission cache and
|
|
17
|
-
* resolved into req.accessContext by RequireAuthMiddleware
|
|
16
|
+
* perms never touch the JWT — they live in the Redis permission cache and
|
|
17
|
+
* are resolved into req.accessContext by RequireAuthMiddleware per request.
|
|
18
|
+
* ────────────────────────────────────────────────────────────────────────
|
|
18
19
|
*/
|
|
19
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
21
|
exports.isHumanPayload = isHumanPayload;
|
|
@@ -24,8 +25,9 @@ exports.isMachineContext = isMachineContext;
|
|
|
24
25
|
/* ====================================================================
|
|
25
26
|
4. TYPE GUARDS
|
|
26
27
|
|
|
27
|
-
Use these over raw property checks — TypeScript narrows
|
|
28
|
-
through
|
|
28
|
+
Use these over raw property checks — TypeScript narrows the union
|
|
29
|
+
correctly through predicate functions on both InternalJwtPayload
|
|
30
|
+
and AccessContext.
|
|
29
31
|
==================================================================== */
|
|
30
32
|
function isHumanPayload(payload) {
|
|
31
33
|
return payload.isMachine === false;
|
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DATE SERIALIZATION UTILS
|
|
3
|
-
*
|
|
2
|
+
* DATE SERIALIZATION UTILS (@discover-cloud/shared)
|
|
3
|
+
* ─────────────────────────────────────────────────────
|
|
4
4
|
* Converts Date objects to ISO 8601 strings for HTTP responses.
|
|
5
5
|
* Use these in toDto() mappers — never call .toISOString() inline.
|
|
6
6
|
*
|
|
7
|
-
* Why
|
|
8
|
-
*
|
|
7
|
+
* Why centralise this?
|
|
8
|
+
* DTOs cross HTTP boundaries where JSON has no Date type. Domain models
|
|
9
|
+
* keep Date objects internally; these helpers handle the conversion at
|
|
10
|
+
* the boundary in a single consistent place. If the serialisation format
|
|
11
|
+
* ever needs to change (e.g. add milliseconds truncation, force UTC
|
|
12
|
+
* suffix), there is one place to update.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* toIso
|
|
16
|
+
* Converts a Date to an ISO 8601 string.
|
|
17
|
+
* Use for non-nullable timestamp fields in DTOs.
|
|
9
18
|
*/
|
|
10
19
|
export declare const toIso: (date: Date) => string;
|
|
20
|
+
/**
|
|
21
|
+
* toIsoOrNull
|
|
22
|
+
* Converts a Date to an ISO 8601 string, or returns null if the value
|
|
23
|
+
* is absent. Use for nullable timestamp fields (e.g. revokedAt, deletedAt).
|
|
24
|
+
*/
|
|
11
25
|
export declare const toIsoOrNull: (date: Date | null | undefined) => string | null;
|
package/dist/utils/date.utils.js
CHANGED
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* DATE SERIALIZATION UTILS
|
|
4
|
-
*
|
|
3
|
+
* DATE SERIALIZATION UTILS (@discover-cloud/shared)
|
|
4
|
+
* ─────────────────────────────────────────────────────
|
|
5
5
|
* Converts Date objects to ISO 8601 strings for HTTP responses.
|
|
6
6
|
* Use these in toDto() mappers — never call .toISOString() inline.
|
|
7
7
|
*
|
|
8
|
-
* Why
|
|
9
|
-
*
|
|
8
|
+
* Why centralise this?
|
|
9
|
+
* DTOs cross HTTP boundaries where JSON has no Date type. Domain models
|
|
10
|
+
* keep Date objects internally; these helpers handle the conversion at
|
|
11
|
+
* the boundary in a single consistent place. If the serialisation format
|
|
12
|
+
* ever needs to change (e.g. add milliseconds truncation, force UTC
|
|
13
|
+
* suffix), there is one place to update.
|
|
10
14
|
*/
|
|
11
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
16
|
exports.toIsoOrNull = exports.toIso = void 0;
|
|
17
|
+
/**
|
|
18
|
+
* toIso
|
|
19
|
+
* Converts a Date to an ISO 8601 string.
|
|
20
|
+
* Use for non-nullable timestamp fields in DTOs.
|
|
21
|
+
*/
|
|
13
22
|
const toIso = (date) => date.toISOString();
|
|
14
23
|
exports.toIso = toIso;
|
|
24
|
+
/**
|
|
25
|
+
* toIsoOrNull
|
|
26
|
+
* Converts a Date to an ISO 8601 string, or returns null if the value
|
|
27
|
+
* is absent. Use for nullable timestamp fields (e.g. revokedAt, deletedAt).
|
|
28
|
+
*/
|
|
15
29
|
const toIsoOrNull = (date) => date ? date.toISOString() : null;
|
|
16
30
|
exports.toIsoOrNull = toIsoOrNull;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ENVIRONMENT HELPERS (@discover-cloud/shared)
|
|
3
|
+
* ───────────────────────────────────────────────
|
|
4
|
+
* Typed accessors for process.env values.
|
|
5
|
+
*
|
|
6
|
+
* Why not read process.env directly?
|
|
7
|
+
* - process.env values are always string | undefined. Reading them inline
|
|
8
|
+
* forces every callsite to handle undefined or cast — this pushes that
|
|
9
|
+
* contract to one place.
|
|
10
|
+
* - getEnv() fails fast at startup (before serving any traffic) if a
|
|
11
|
+
* required variable is absent, surfacing misconfiguration immediately
|
|
12
|
+
* rather than at runtime inside a request handler.
|
|
13
|
+
* - Centralised access makes it straightforward to add validation, type
|
|
14
|
+
* coercion, or secret-redaction logic later without touching callsites.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* // Required — throws at startup if missing
|
|
18
|
+
* const dbUrl = getEnv("DATABASE_URL");
|
|
19
|
+
* const jwtSecret = getEnv("JWT_SECRET");
|
|
20
|
+
*
|
|
21
|
+
* // Optional — returns undefined (or a typed default) when absent
|
|
22
|
+
* const logLevel = getEnvOptional("LOG_LEVEL") ?? "info";
|
|
23
|
+
* const port = Number(getEnvOptional("PORT") ?? "3000");
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* getEnv
|
|
27
|
+
* Returns the value of a required environment variable.
|
|
28
|
+
* Throws at call time (typically during service startup) if the variable
|
|
29
|
+
* is absent or empty — this is intentional: missing required config should
|
|
30
|
+
* crash the process before it begins serving traffic.
|
|
31
|
+
*
|
|
32
|
+
* Empty string ("") is treated as missing because it is almost always an
|
|
33
|
+
* accidental misconfiguration (e.g. `SECRET=` with no value in a .env file).
|
|
34
|
+
*/
|
|
35
|
+
export declare function getEnv(name: string): string;
|
|
36
|
+
/**
|
|
37
|
+
* getEnvOptional
|
|
38
|
+
* Returns the value of an optional environment variable, or undefined
|
|
39
|
+
* if it is absent or empty. Use with a nullish coalescing default:
|
|
40
|
+
*
|
|
41
|
+
* const logLevel = getEnvOptional("LOG_LEVEL") ?? "info";
|
|
42
|
+
*
|
|
43
|
+
* Returns undefined (not empty string) so callers can safely use `??`
|
|
44
|
+
* and `||` without needing to guard against empty strings separately.
|
|
45
|
+
*/
|
|
46
|
+
export declare function getEnvOptional(name: string): string | undefined;
|