@discover-cloud/shared 1.0.10 → 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 +38 -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
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ENVIRONMENT HELPERS (@discover-cloud/shared)
|
|
4
|
+
* ───────────────────────────────────────────────
|
|
5
|
+
* Typed accessors for process.env values.
|
|
6
|
+
*
|
|
7
|
+
* Why not read process.env directly?
|
|
8
|
+
* - process.env values are always string | undefined. Reading them inline
|
|
9
|
+
* forces every callsite to handle undefined or cast — this pushes that
|
|
10
|
+
* contract to one place.
|
|
11
|
+
* - getEnv() fails fast at startup (before serving any traffic) if a
|
|
12
|
+
* required variable is absent, surfacing misconfiguration immediately
|
|
13
|
+
* rather than at runtime inside a request handler.
|
|
14
|
+
* - Centralised access makes it straightforward to add validation, type
|
|
15
|
+
* coercion, or secret-redaction logic later without touching callsites.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* // Required — throws at startup if missing
|
|
19
|
+
* const dbUrl = getEnv("DATABASE_URL");
|
|
20
|
+
* const jwtSecret = getEnv("JWT_SECRET");
|
|
21
|
+
*
|
|
22
|
+
* // Optional — returns undefined (or a typed default) when absent
|
|
23
|
+
* const logLevel = getEnvOptional("LOG_LEVEL") ?? "info";
|
|
24
|
+
* const port = Number(getEnvOptional("PORT") ?? "3000");
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.getEnv = getEnv;
|
|
28
|
+
exports.getEnvOptional = getEnvOptional;
|
|
29
|
+
/**
|
|
30
|
+
* getEnv
|
|
31
|
+
* Returns the value of a required environment variable.
|
|
32
|
+
* Throws at call time (typically during service startup) if the variable
|
|
33
|
+
* is absent or empty — this is intentional: missing required config should
|
|
34
|
+
* crash the process before it begins serving traffic.
|
|
35
|
+
*
|
|
36
|
+
* Empty string ("") is treated as missing because it is almost always an
|
|
37
|
+
* accidental misconfiguration (e.g. `SECRET=` with no value in a .env file).
|
|
38
|
+
*/
|
|
39
|
+
function getEnv(name) {
|
|
40
|
+
const value = process.env[name];
|
|
41
|
+
if (!value) {
|
|
42
|
+
throw new Error(`Missing required environment variable: ${name}. ` +
|
|
43
|
+
`Ensure it is set in your .env file or deployment environment.`);
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* getEnvOptional
|
|
49
|
+
* Returns the value of an optional environment variable, or undefined
|
|
50
|
+
* if it is absent or empty. Use with a nullish coalescing default:
|
|
51
|
+
*
|
|
52
|
+
* const logLevel = getEnvOptional("LOG_LEVEL") ?? "info";
|
|
53
|
+
*
|
|
54
|
+
* Returns undefined (not empty string) so callers can safely use `??`
|
|
55
|
+
* and `||` without needing to guard against empty strings separately.
|
|
56
|
+
*/
|
|
57
|
+
function getEnvOptional(name) {
|
|
58
|
+
const value = process.env[name];
|
|
59
|
+
// Normalise empty string to undefined — same convention as getEnv.
|
|
60
|
+
return value === "" ? undefined : value;
|
|
61
|
+
}
|
|
@@ -1,30 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* LOGGER INTERFACE (@discover-cloud/shared)
|
|
3
3
|
* ────────────────────────────────────────────
|
|
4
|
-
* A minimal logger contract shared code can depend on.
|
|
5
|
-
* Each service injects its own implementation (pino, winston,
|
|
4
|
+
* A minimal logger contract that shared code can depend on.
|
|
5
|
+
* Each service injects its own concrete implementation (pino, winston, etc.).
|
|
6
6
|
*
|
|
7
|
-
* Shared
|
|
8
|
-
*
|
|
7
|
+
* Shared classes (RequireAuthMiddleware, PermissionCacheService, etc.)
|
|
8
|
+
* accept ILogger via constructor injection — they never import a concrete
|
|
9
|
+
* logger directly, keeping the shared package dependency-free.
|
|
9
10
|
*
|
|
10
11
|
* ─── Usage in shared classes ────────────────────────────────────────
|
|
11
12
|
* class RequireAuthMiddleware {
|
|
12
13
|
* constructor(
|
|
13
14
|
* private readonly verifier: InternalJwtVerifier,
|
|
14
|
-
* private readonly logger:
|
|
15
|
+
* private readonly logger: ILogger = noopLogger,
|
|
15
16
|
* ) {}
|
|
16
17
|
* }
|
|
17
18
|
*
|
|
18
19
|
* ─── Wiring in each service ─────────────────────────────────────────
|
|
19
|
-
* // auth-service / composition root:
|
|
20
20
|
* import pino from "pino";
|
|
21
|
-
* const
|
|
21
|
+
* const logger = pino({ level: "info" });
|
|
22
|
+
* const requireAuth = new RequireAuthMiddleware(jwtVerifier, logger);
|
|
22
23
|
*
|
|
23
|
-
*
|
|
24
|
+
* // pino satisfies ILogger — its method signatures are compatible.
|
|
24
25
|
*
|
|
25
|
-
* ───
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* ─── Signature convention ───────────────────────────────────────────
|
|
27
|
+
* Follows pino's overloaded signature:
|
|
28
|
+
* logger.info({ userId }, "User logged in") — object first, then message
|
|
29
|
+
* logger.info("Simple message") — string only
|
|
30
|
+
*
|
|
31
|
+
* This matches pino natively and means a pino instance can be passed
|
|
32
|
+
* directly without any wrapping.
|
|
28
33
|
*/
|
|
29
34
|
export interface ILogger {
|
|
30
35
|
debug(obj: object, msg?: string): void;
|
|
@@ -38,14 +43,24 @@ export interface ILogger {
|
|
|
38
43
|
}
|
|
39
44
|
/**
|
|
40
45
|
* noopLogger
|
|
41
|
-
* Silent
|
|
42
|
-
*
|
|
46
|
+
* Silent no-op implementation — the safe default when no logger is injected.
|
|
47
|
+
* All log calls are discarded. Use in tests and library consumers that
|
|
48
|
+
* don't want log noise.
|
|
43
49
|
*/
|
|
44
50
|
export declare const noopLogger: ILogger;
|
|
45
51
|
/**
|
|
46
52
|
* consoleLogger
|
|
47
|
-
* Thin console wrapper
|
|
48
|
-
* that don't need structured
|
|
49
|
-
*
|
|
53
|
+
* Thin console wrapper that follows the pino (obj, msg?) calling convention.
|
|
54
|
+
* Useful for local development and simple services that don't need structured
|
|
55
|
+
* logging — pass this instead of wiring up pino.
|
|
56
|
+
*
|
|
57
|
+
* Output order mirrors pino: message first, then the context object on a
|
|
58
|
+
* separate argument so it appears as structured context in most terminals.
|
|
59
|
+
*
|
|
60
|
+
* consoleLogger.info({ userId: "abc" }, "User logged in")
|
|
61
|
+
* → console.info("User logged in", { userId: "abc" })
|
|
62
|
+
*
|
|
63
|
+
* consoleLogger.info("Simple message")
|
|
64
|
+
* → console.info("Simple message")
|
|
50
65
|
*/
|
|
51
66
|
export declare const consoleLogger: ILogger;
|
|
@@ -2,37 +2,43 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* LOGGER INTERFACE (@discover-cloud/shared)
|
|
4
4
|
* ────────────────────────────────────────────
|
|
5
|
-
* A minimal logger contract shared code can depend on.
|
|
6
|
-
* Each service injects its own implementation (pino, winston,
|
|
5
|
+
* A minimal logger contract that shared code can depend on.
|
|
6
|
+
* Each service injects its own concrete implementation (pino, winston, etc.).
|
|
7
7
|
*
|
|
8
|
-
* Shared
|
|
9
|
-
*
|
|
8
|
+
* Shared classes (RequireAuthMiddleware, PermissionCacheService, etc.)
|
|
9
|
+
* accept ILogger via constructor injection — they never import a concrete
|
|
10
|
+
* logger directly, keeping the shared package dependency-free.
|
|
10
11
|
*
|
|
11
12
|
* ─── Usage in shared classes ────────────────────────────────────────
|
|
12
13
|
* class RequireAuthMiddleware {
|
|
13
14
|
* constructor(
|
|
14
15
|
* private readonly verifier: InternalJwtVerifier,
|
|
15
|
-
* private readonly logger:
|
|
16
|
+
* private readonly logger: ILogger = noopLogger,
|
|
16
17
|
* ) {}
|
|
17
18
|
* }
|
|
18
19
|
*
|
|
19
20
|
* ─── Wiring in each service ─────────────────────────────────────────
|
|
20
|
-
* // auth-service / composition root:
|
|
21
21
|
* import pino from "pino";
|
|
22
|
-
* const
|
|
22
|
+
* const logger = pino({ level: "info" });
|
|
23
|
+
* const requireAuth = new RequireAuthMiddleware(jwtVerifier, logger);
|
|
23
24
|
*
|
|
24
|
-
*
|
|
25
|
+
* // pino satisfies ILogger — its method signatures are compatible.
|
|
25
26
|
*
|
|
26
|
-
* ───
|
|
27
|
-
*
|
|
28
|
-
*
|
|
27
|
+
* ─── Signature convention ───────────────────────────────────────────
|
|
28
|
+
* Follows pino's overloaded signature:
|
|
29
|
+
* logger.info({ userId }, "User logged in") — object first, then message
|
|
30
|
+
* logger.info("Simple message") — string only
|
|
31
|
+
*
|
|
32
|
+
* This matches pino natively and means a pino instance can be passed
|
|
33
|
+
* directly without any wrapping.
|
|
29
34
|
*/
|
|
30
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
36
|
exports.consoleLogger = exports.noopLogger = void 0;
|
|
32
37
|
/**
|
|
33
38
|
* noopLogger
|
|
34
|
-
* Silent
|
|
35
|
-
*
|
|
39
|
+
* Silent no-op implementation — the safe default when no logger is injected.
|
|
40
|
+
* All log calls are discarded. Use in tests and library consumers that
|
|
41
|
+
* don't want log noise.
|
|
36
42
|
*/
|
|
37
43
|
exports.noopLogger = {
|
|
38
44
|
debug: () => { },
|
|
@@ -42,21 +48,50 @@ exports.noopLogger = {
|
|
|
42
48
|
};
|
|
43
49
|
/**
|
|
44
50
|
* consoleLogger
|
|
45
|
-
* Thin console wrapper
|
|
46
|
-
* that don't need structured
|
|
47
|
-
*
|
|
51
|
+
* Thin console wrapper that follows the pino (obj, msg?) calling convention.
|
|
52
|
+
* Useful for local development and simple services that don't need structured
|
|
53
|
+
* logging — pass this instead of wiring up pino.
|
|
54
|
+
*
|
|
55
|
+
* Output order mirrors pino: message first, then the context object on a
|
|
56
|
+
* separate argument so it appears as structured context in most terminals.
|
|
57
|
+
*
|
|
58
|
+
* consoleLogger.info({ userId: "abc" }, "User logged in")
|
|
59
|
+
* → console.info("User logged in", { userId: "abc" })
|
|
60
|
+
*
|
|
61
|
+
* consoleLogger.info("Simple message")
|
|
62
|
+
* → console.info("Simple message")
|
|
48
63
|
*/
|
|
49
64
|
exports.consoleLogger = {
|
|
50
65
|
debug: (objOrMsg, msg) => {
|
|
51
|
-
|
|
66
|
+
if (typeof objOrMsg === "string") {
|
|
67
|
+
console.debug(objOrMsg);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
console.debug(msg ?? "", objOrMsg);
|
|
71
|
+
}
|
|
52
72
|
},
|
|
53
73
|
info: (objOrMsg, msg) => {
|
|
54
|
-
|
|
74
|
+
if (typeof objOrMsg === "string") {
|
|
75
|
+
console.info(objOrMsg);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
console.info(msg ?? "", objOrMsg);
|
|
79
|
+
}
|
|
55
80
|
},
|
|
56
81
|
warn: (objOrMsg, msg) => {
|
|
57
|
-
|
|
82
|
+
if (typeof objOrMsg === "string") {
|
|
83
|
+
console.warn(objOrMsg);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
console.warn(msg ?? "", objOrMsg);
|
|
87
|
+
}
|
|
58
88
|
},
|
|
59
89
|
error: (objOrMsg, msg) => {
|
|
60
|
-
|
|
90
|
+
if (typeof objOrMsg === "string") {
|
|
91
|
+
console.error(objOrMsg);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
console.error(msg ?? "", objOrMsg);
|
|
95
|
+
}
|
|
61
96
|
},
|
|
62
97
|
};
|
|
@@ -1,12 +1,54 @@
|
|
|
1
1
|
import { Response, Request } from "express";
|
|
2
2
|
/**
|
|
3
|
-
* RESPONSE HELPERS
|
|
4
|
-
*
|
|
3
|
+
* RESPONSE HELPERS (@discover-cloud/shared)
|
|
4
|
+
* ────────────────────────────────────────────
|
|
5
5
|
* Typed wrappers around res.json() that enforce the ApiSuccessResponse
|
|
6
|
-
* and ApiErrorResponse envelope shapes.
|
|
6
|
+
* and ApiErrorResponse envelope shapes across all services.
|
|
7
7
|
*
|
|
8
|
-
* Both
|
|
9
|
-
*
|
|
8
|
+
* Both helpers accept req explicitly rather than reading res.req — this
|
|
9
|
+
* makes the dependency visible at the call site and avoids the res.req
|
|
10
|
+
* cast anti-pattern.
|
|
11
|
+
*
|
|
12
|
+
* requestId fallback:
|
|
13
|
+
* req.id is set by requestId middleware. The randomUUID() fallback should
|
|
14
|
+
* never fire in a correctly wired service — if it does, it means requestId
|
|
15
|
+
* middleware was not registered before this helper was called. The fallback
|
|
16
|
+
* keeps the response well-formed but the generated ID won't correlate with
|
|
17
|
+
* any upstream trace. Check middleware registration order if you see UUIDs
|
|
18
|
+
* in responses that don't match the x-request-id header.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* success
|
|
22
|
+
* Wraps data in an ApiSuccessResponse envelope and sends it.
|
|
23
|
+
*
|
|
24
|
+
* @param res - Express response object
|
|
25
|
+
* @param req - Express request object (for requestId + timestamp)
|
|
26
|
+
* @param data - The response payload; typed as T for type inference
|
|
27
|
+
* @param statusCode - HTTP status code (default 200)
|
|
28
|
+
*
|
|
29
|
+
* Usage:
|
|
30
|
+
* success<CloudAccountDto>(res, req, accountDto);
|
|
31
|
+
* success<PaginatedResponseDto<CloudAccountDto>>(res, req, paginatedResult, 200);
|
|
32
|
+
* success<MessageResponseDto>(res, req, { message: "Deleted" }, 200);
|
|
10
33
|
*/
|
|
11
34
|
export declare const success: <T>(res: Response, req: Request, data: T, statusCode?: number) => void;
|
|
35
|
+
/**
|
|
36
|
+
* failure
|
|
37
|
+
* Wraps an error in an ApiErrorResponse envelope and sends it.
|
|
38
|
+
*
|
|
39
|
+
* @param res - Express response object
|
|
40
|
+
* @param req - Express request object (for requestId + timestamp)
|
|
41
|
+
* @param message - Human-readable error description
|
|
42
|
+
* @param code - Machine-readable error code (maps to AppError.code)
|
|
43
|
+
* @param statusCode - HTTP status code (default 400)
|
|
44
|
+
* @param details - Optional structured context (e.g. Zod flatten() output).
|
|
45
|
+
* Never put secrets, stack traces, or raw DB errors here.
|
|
46
|
+
*
|
|
47
|
+
* details is omitted from the response body when not provided — this avoids
|
|
48
|
+
* "details": null noise in responses and keeps the shape clean for clients.
|
|
49
|
+
*
|
|
50
|
+
* Usage:
|
|
51
|
+
* failure(res, req, "Account not found", "NOT_FOUND", 404);
|
|
52
|
+
* failure(res, req, "Validation failed", "VALIDATION_ERROR", 400, err.flatten());
|
|
53
|
+
*/
|
|
12
54
|
export declare const failure: (res: Response, req: Request, message: string, code: string, statusCode?: number, details?: unknown) => void;
|
|
@@ -3,33 +3,76 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.failure = exports.success = void 0;
|
|
4
4
|
const crypto_1 = require("crypto");
|
|
5
5
|
/**
|
|
6
|
-
* RESPONSE HELPERS
|
|
7
|
-
*
|
|
6
|
+
* RESPONSE HELPERS (@discover-cloud/shared)
|
|
7
|
+
* ────────────────────────────────────────────
|
|
8
8
|
* Typed wrappers around res.json() that enforce the ApiSuccessResponse
|
|
9
|
-
* and ApiErrorResponse envelope shapes.
|
|
9
|
+
* and ApiErrorResponse envelope shapes across all services.
|
|
10
10
|
*
|
|
11
|
-
* Both
|
|
12
|
-
*
|
|
11
|
+
* Both helpers accept req explicitly rather than reading res.req — this
|
|
12
|
+
* makes the dependency visible at the call site and avoids the res.req
|
|
13
|
+
* cast anti-pattern.
|
|
14
|
+
*
|
|
15
|
+
* requestId fallback:
|
|
16
|
+
* req.id is set by requestId middleware. The randomUUID() fallback should
|
|
17
|
+
* never fire in a correctly wired service — if it does, it means requestId
|
|
18
|
+
* middleware was not registered before this helper was called. The fallback
|
|
19
|
+
* keeps the response well-formed but the generated ID won't correlate with
|
|
20
|
+
* any upstream trace. Check middleware registration order if you see UUIDs
|
|
21
|
+
* in responses that don't match the x-request-id header.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* success
|
|
25
|
+
* Wraps data in an ApiSuccessResponse envelope and sends it.
|
|
26
|
+
*
|
|
27
|
+
* @param res - Express response object
|
|
28
|
+
* @param req - Express request object (for requestId + timestamp)
|
|
29
|
+
* @param data - The response payload; typed as T for type inference
|
|
30
|
+
* @param statusCode - HTTP status code (default 200)
|
|
31
|
+
*
|
|
32
|
+
* Usage:
|
|
33
|
+
* success<CloudAccountDto>(res, req, accountDto);
|
|
34
|
+
* success<PaginatedResponseDto<CloudAccountDto>>(res, req, paginatedResult, 200);
|
|
35
|
+
* success<MessageResponseDto>(res, req, { message: "Deleted" }, 200);
|
|
13
36
|
*/
|
|
14
37
|
const success = (res, req, data, statusCode = 200) => {
|
|
15
38
|
const response = {
|
|
16
39
|
success: true,
|
|
17
40
|
data,
|
|
18
41
|
meta: {
|
|
19
|
-
requestId: req.id ?? (0, crypto_1.randomUUID)(),
|
|
42
|
+
requestId: req.id ?? (0, crypto_1.randomUUID)(),
|
|
20
43
|
timestamp: new Date().toISOString(),
|
|
21
44
|
},
|
|
22
45
|
};
|
|
23
46
|
res.status(statusCode).json(response);
|
|
24
47
|
};
|
|
25
48
|
exports.success = success;
|
|
49
|
+
/**
|
|
50
|
+
* failure
|
|
51
|
+
* Wraps an error in an ApiErrorResponse envelope and sends it.
|
|
52
|
+
*
|
|
53
|
+
* @param res - Express response object
|
|
54
|
+
* @param req - Express request object (for requestId + timestamp)
|
|
55
|
+
* @param message - Human-readable error description
|
|
56
|
+
* @param code - Machine-readable error code (maps to AppError.code)
|
|
57
|
+
* @param statusCode - HTTP status code (default 400)
|
|
58
|
+
* @param details - Optional structured context (e.g. Zod flatten() output).
|
|
59
|
+
* Never put secrets, stack traces, or raw DB errors here.
|
|
60
|
+
*
|
|
61
|
+
* details is omitted from the response body when not provided — this avoids
|
|
62
|
+
* "details": null noise in responses and keeps the shape clean for clients.
|
|
63
|
+
*
|
|
64
|
+
* Usage:
|
|
65
|
+
* failure(res, req, "Account not found", "NOT_FOUND", 404);
|
|
66
|
+
* failure(res, req, "Validation failed", "VALIDATION_ERROR", 400, err.flatten());
|
|
67
|
+
*/
|
|
26
68
|
const failure = (res, req, message, code, statusCode = 400, details) => {
|
|
27
69
|
const response = {
|
|
28
70
|
success: false,
|
|
29
71
|
error: {
|
|
30
72
|
code,
|
|
31
73
|
message,
|
|
32
|
-
//
|
|
74
|
+
// Spread details only when present — omitting the key entirely is
|
|
75
|
+
// cleaner than sending "details": undefined (which JSON.stringify drops anyway).
|
|
33
76
|
...(details !== undefined ? { details } : {}),
|
|
34
77
|
},
|
|
35
78
|
meta: {
|