@nage-api/core 1.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +141 -0
- package/dist/bootstrap/bootstrap.d.ts +48 -0
- package/dist/bootstrap/bootstrap.js +255 -0
- package/dist/bootstrap/drain.d.ts +48 -0
- package/dist/bootstrap/drain.js +113 -0
- package/dist/bootstrap/lifecycle.d.ts +30 -0
- package/dist/bootstrap/lifecycle.js +64 -0
- package/dist/bootstrap/process-guards.d.ts +42 -0
- package/dist/bootstrap/process-guards.js +103 -0
- package/dist/bootstrap/query-parser.d.ts +34 -0
- package/dist/bootstrap/query-parser.js +37 -0
- package/dist/bootstrap/shutdown.d.ts +55 -0
- package/dist/bootstrap/shutdown.js +182 -0
- package/dist/constants.d.ts +32 -0
- package/dist/constants.js +48 -0
- package/dist/context/active-context.d.ts +23 -0
- package/dist/context/active-context.js +34 -0
- package/dist/context/request-context.middleware.d.ts +31 -0
- package/dist/context/request-context.middleware.js +95 -0
- package/dist/context/request-context.service.d.ts +29 -0
- package/dist/context/request-context.service.js +67 -0
- package/dist/decorators/owner.decorator.d.ts +18 -0
- package/dist/decorators/owner.decorator.js +31 -0
- package/dist/decorators/public.decorator.d.ts +13 -0
- package/dist/decorators/public.decorator.js +23 -0
- package/dist/decorators/version.decorators.d.ts +34 -0
- package/dist/decorators/version.decorators.js +40 -0
- package/dist/errors/catalog.d.ts +149 -0
- package/dist/errors/catalog.js +289 -0
- package/dist/errors/index.d.ts +3 -0
- package/dist/errors/index.js +22 -0
- package/dist/errors/nage.error.d.ts +43 -0
- package/dist/errors/nage.error.js +45 -0
- package/dist/guards/api-version.guard.d.ts +20 -0
- package/dist/guards/api-version.guard.js +73 -0
- package/dist/http/all-exceptions.filter.d.ts +25 -0
- package/dist/http/all-exceptions.filter.js +256 -0
- package/dist/http/envelope.d.ts +25 -0
- package/dist/http/envelope.js +44 -0
- package/dist/http/no-envelope.decorator.d.ts +11 -0
- package/dist/http/no-envelope.decorator.js +16 -0
- package/dist/http/request-timeout.decorators.d.ts +23 -0
- package/dist/http/request-timeout.decorators.js +29 -0
- package/dist/http/request-timeout.interceptor.d.ts +28 -0
- package/dist/http/request-timeout.interceptor.js +75 -0
- package/dist/http/response.interceptor.d.ts +19 -0
- package/dist/http/response.interceptor.js +73 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +135 -0
- package/dist/job/job.factory.d.ts +29 -0
- package/dist/job/job.factory.js +50 -0
- package/dist/logging/json.logger.d.ts +23 -0
- package/dist/logging/json.logger.js +136 -0
- package/dist/logging/nest-logger.adapter.d.ts +20 -0
- package/dist/logging/nest-logger.adapter.js +46 -0
- package/dist/module/core.module.d.ts +40 -0
- package/dist/module/core.module.js +112 -0
- package/dist/security/audit.d.ts +42 -0
- package/dist/security/audit.js +399 -0
- package/dist/security/index.d.ts +15 -0
- package/dist/security/index.js +50 -0
- package/dist/security/legacy-scan.d.ts +24 -0
- package/dist/security/legacy-scan.js +98 -0
- package/dist/security/random.d.ts +40 -0
- package/dist/security/random.js +87 -0
- package/dist/security/rate-limit.decorators.d.ts +24 -0
- package/dist/security/rate-limit.decorators.js +25 -0
- package/dist/security/rate-limit.guard.d.ts +44 -0
- package/dist/security/rate-limit.guard.js +130 -0
- package/dist/security/rate-limit.store.d.ts +30 -0
- package/dist/security/rate-limit.store.js +63 -0
- package/dist/security/redaction.d.ts +54 -0
- package/dist/security/redaction.js +146 -0
- package/dist/security/tls.d.ts +29 -0
- package/dist/security/tls.js +48 -0
- package/dist/tokens.d.ts +60 -0
- package/dist/tokens.js +89 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.js +8 -0
- package/package.json +77 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Enforces the version rules declared by `@ForVersion` and friends
|
|
4
|
+
* (PLAN.md §16.2).
|
|
5
|
+
*
|
|
6
|
+
* A route that does not apply to the requested version is reported as **not
|
|
7
|
+
* found**, not as a validation error: from the client's point of view that
|
|
8
|
+
* endpoint does not exist in the version it asked for.
|
|
9
|
+
*/
|
|
10
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
11
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
12
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
13
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
14
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
15
|
+
};
|
|
16
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
17
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
18
|
+
};
|
|
19
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
20
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
21
|
+
};
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.ApiVersionGuard = void 0;
|
|
24
|
+
const common_1 = require("@nestjs/common");
|
|
25
|
+
const core_1 = require("@nestjs/core");
|
|
26
|
+
const constants_js_1 = require("../constants.js");
|
|
27
|
+
const request_context_service_js_1 = require("../context/request-context.service.js");
|
|
28
|
+
const catalog_js_1 = require("../errors/catalog.js");
|
|
29
|
+
const version_decorators_js_1 = require("../decorators/version.decorators.js");
|
|
30
|
+
const tokens_js_1 = require("../tokens.js");
|
|
31
|
+
let ApiVersionGuard = class ApiVersionGuard {
|
|
32
|
+
reflector;
|
|
33
|
+
context;
|
|
34
|
+
config;
|
|
35
|
+
constructor(reflector, context, config) {
|
|
36
|
+
this.reflector = reflector;
|
|
37
|
+
this.context = context;
|
|
38
|
+
this.config = config;
|
|
39
|
+
}
|
|
40
|
+
canActivate(context) {
|
|
41
|
+
if (context.getType() !== 'http')
|
|
42
|
+
return true;
|
|
43
|
+
if (this.config.versioning?.enabled === false)
|
|
44
|
+
return true;
|
|
45
|
+
const requested = this.context.get()?.apiVersion ??
|
|
46
|
+
this.config.versioning?.defaultVersion ??
|
|
47
|
+
constants_js_1.DEFAULT_API_VERSION;
|
|
48
|
+
const minimum = this.config.versioning?.minVersion;
|
|
49
|
+
if (minimum !== undefined && requested < minimum) {
|
|
50
|
+
throw new catalog_js_1.NotFoundError({
|
|
51
|
+
meta: { reason: 'below-minimum-version', requested, minimum },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const rule = this.reflector.getAllAndOverride(constants_js_1.METADATA_KEYS.versionRule, [context.getHandler(), context.getClass()]);
|
|
55
|
+
// No rule means the route serves every version.
|
|
56
|
+
if (rule === undefined)
|
|
57
|
+
return true;
|
|
58
|
+
if (!(0, version_decorators_js_1.versionSatisfies)(rule, requested)) {
|
|
59
|
+
throw new catalog_js_1.NotFoundError({ meta: { reason: 'version-mismatch', requested, rule } });
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
exports.ApiVersionGuard = ApiVersionGuard;
|
|
65
|
+
exports.ApiVersionGuard = ApiVersionGuard = __decorate([
|
|
66
|
+
(0, common_1.Injectable)(),
|
|
67
|
+
__param(0, (0, common_1.Inject)(core_1.Reflector)),
|
|
68
|
+
__param(1, (0, common_1.Inject)(request_context_service_js_1.RequestContextService)),
|
|
69
|
+
__param(2, (0, common_1.Inject)(tokens_js_1.NAGE_CONFIG)),
|
|
70
|
+
__metadata("design:paramtypes", [core_1.Reflector,
|
|
71
|
+
request_context_service_js_1.RequestContextService, Object])
|
|
72
|
+
], ApiVersionGuard);
|
|
73
|
+
//# sourceMappingURL=api-version.guard.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single global exception filter (PLAN.md §17).
|
|
3
|
+
*
|
|
4
|
+
* Every thrown value — framework error, Nest `HttpException`, or an unexpected
|
|
5
|
+
* `TypeError` from a driver — leaves as the same error envelope. The client gets
|
|
6
|
+
* a stable code and a safe message; the stack, the SQL and the upstream body go
|
|
7
|
+
* to the log, keyed by the same request id the client received.
|
|
8
|
+
*/
|
|
9
|
+
import { type ArgumentsHost } from '@nestjs/common';
|
|
10
|
+
import type { ExceptionFilter } from '@nestjs/common';
|
|
11
|
+
import { HttpAdapterHost } from '@nestjs/core';
|
|
12
|
+
import type { LoggerPort } from '@nage-api/contracts';
|
|
13
|
+
import { RequestContextService } from '../context/request-context.service.js';
|
|
14
|
+
import { NageError } from '../errors/nage.error.js';
|
|
15
|
+
export declare class AllExceptionsFilter implements ExceptionFilter {
|
|
16
|
+
#private;
|
|
17
|
+
private readonly adapterHost;
|
|
18
|
+
private readonly context;
|
|
19
|
+
private readonly logger;
|
|
20
|
+
constructor(adapterHost: HttpAdapterHost, context: RequestContextService, logger: LoggerPort);
|
|
21
|
+
catch(exception: unknown, host: ArgumentsHost): void;
|
|
22
|
+
}
|
|
23
|
+
/** Re-exported so applications can narrow in their own filters. */
|
|
24
|
+
export { NageError };
|
|
25
|
+
//# sourceMappingURL=all-exceptions.filter.d.ts.map
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The single global exception filter (PLAN.md §17).
|
|
4
|
+
*
|
|
5
|
+
* Every thrown value — framework error, Nest `HttpException`, or an unexpected
|
|
6
|
+
* `TypeError` from a driver — leaves as the same error envelope. The client gets
|
|
7
|
+
* a stable code and a safe message; the stack, the SQL and the upstream body go
|
|
8
|
+
* to the log, keyed by the same request id the client received.
|
|
9
|
+
*/
|
|
10
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
11
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
12
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
13
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
14
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
15
|
+
};
|
|
16
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
17
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
18
|
+
};
|
|
19
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
20
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
21
|
+
};
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.NageError = exports.AllExceptionsFilter = void 0;
|
|
24
|
+
const common_1 = require("@nestjs/common");
|
|
25
|
+
const core_1 = require("@nestjs/core");
|
|
26
|
+
const constants_js_1 = require("../constants.js");
|
|
27
|
+
const request_context_service_js_1 = require("../context/request-context.service.js");
|
|
28
|
+
const request_context_middleware_js_1 = require("../context/request-context.middleware.js");
|
|
29
|
+
const catalog_js_1 = require("../errors/catalog.js");
|
|
30
|
+
const nage_error_js_1 = require("../errors/nage.error.js");
|
|
31
|
+
Object.defineProperty(exports, "NageError", { enumerable: true, get: function () { return nage_error_js_1.NageError; } });
|
|
32
|
+
const redaction_js_1 = require("../security/redaction.js");
|
|
33
|
+
const tokens_js_1 = require("../tokens.js");
|
|
34
|
+
const envelope_js_1 = require("./envelope.js");
|
|
35
|
+
/** Nest maps its own exceptions to statuses; we map statuses back to codes. */
|
|
36
|
+
const CODE_BY_STATUS = {
|
|
37
|
+
[common_1.HttpStatus.BAD_REQUEST]: 'INVALID_QUERY',
|
|
38
|
+
[common_1.HttpStatus.UNAUTHORIZED]: 'AUTH_REQUIRED',
|
|
39
|
+
[common_1.HttpStatus.FORBIDDEN]: 'FORBIDDEN',
|
|
40
|
+
[common_1.HttpStatus.NOT_FOUND]: 'RESOURCE_NOT_FOUND',
|
|
41
|
+
[common_1.HttpStatus.PRECONDITION_FAILED]: 'PRECONDITION_FAILED',
|
|
42
|
+
[common_1.HttpStatus.CONFLICT]: 'RESOURCE_CONFLICT',
|
|
43
|
+
[common_1.HttpStatus.GONE]: 'RESOURCE_GONE',
|
|
44
|
+
[common_1.HttpStatus.PAYLOAD_TOO_LARGE]: 'PAYLOAD_TOO_LARGE',
|
|
45
|
+
[common_1.HttpStatus.UNPROCESSABLE_ENTITY]: 'VALIDATION_FAILED',
|
|
46
|
+
[common_1.HttpStatus.TOO_MANY_REQUESTS]: 'RATE_LIMIT_EXCEEDED',
|
|
47
|
+
[common_1.HttpStatus.NOT_IMPLEMENTED]: 'UNSUPPORTED_OPERATION',
|
|
48
|
+
[common_1.HttpStatus.BAD_GATEWAY]: 'EXTERNAL_SERVICE_ERROR',
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* The 4xx status a body-parser error carries, or `undefined` for anything else.
|
|
52
|
+
*
|
|
53
|
+
* `raw-body` and `body-parser` set `type` (`entity.too.large`,
|
|
54
|
+
* `entity.parse.failed`, `encoding.unsupported`) and a 4xx `status`/`statusCode`
|
|
55
|
+
* on an ordinary `Error`. Only 4xx is honoured, and only with a recognised
|
|
56
|
+
* `type`: an arbitrary object that happens to have a numeric `status` must not be
|
|
57
|
+
* able to choose the response status, which is how an internal failure gets
|
|
58
|
+
* reported to a client as its own fault.
|
|
59
|
+
*/
|
|
60
|
+
const BODY_PARSER_ERROR_TYPES = new Set([
|
|
61
|
+
'entity.too.large',
|
|
62
|
+
'entity.parse.failed',
|
|
63
|
+
'entity.verify.failed',
|
|
64
|
+
'request.aborted',
|
|
65
|
+
'request.size.invalid',
|
|
66
|
+
'stream.encoding.set',
|
|
67
|
+
'parameters.too.many',
|
|
68
|
+
'charset.unsupported',
|
|
69
|
+
'encoding.unsupported',
|
|
70
|
+
]);
|
|
71
|
+
function bodyParserStatus(exception) {
|
|
72
|
+
if (!(exception instanceof Error))
|
|
73
|
+
return undefined;
|
|
74
|
+
const { type, status, statusCode } = exception;
|
|
75
|
+
if (typeof type !== 'string' || !BODY_PARSER_ERROR_TYPES.has(type))
|
|
76
|
+
return undefined;
|
|
77
|
+
const candidate = typeof status === 'number' ? status : statusCode;
|
|
78
|
+
if (typeof candidate !== 'number' || candidate < 400 || candidate > 499)
|
|
79
|
+
return undefined;
|
|
80
|
+
return candidate;
|
|
81
|
+
}
|
|
82
|
+
/** The inbound correlation id, if the caller sent one. */
|
|
83
|
+
function headerOf(request) {
|
|
84
|
+
const raw = request.headers?.[constants_js_1.REQUEST_ID_HEADER];
|
|
85
|
+
return Array.isArray(raw) ? raw[0] : raw;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* `ValidationPipe` throws a `BadRequestException` whose body carries an array of
|
|
89
|
+
* messages. Lift those into `details` so clients get field-level feedback in the
|
|
90
|
+
* documented shape instead of a wall of prose.
|
|
91
|
+
*/
|
|
92
|
+
function toValidationDetails(response) {
|
|
93
|
+
if (response === null || typeof response !== 'object')
|
|
94
|
+
return undefined;
|
|
95
|
+
const message = response.message;
|
|
96
|
+
if (!Array.isArray(message))
|
|
97
|
+
return undefined;
|
|
98
|
+
const details = message
|
|
99
|
+
.filter((entry) => typeof entry === 'string')
|
|
100
|
+
.map((entry) => ({ message: entry }));
|
|
101
|
+
return details.length > 0 ? details : undefined;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Flatten an exception's `cause` chain into log text.
|
|
105
|
+
*
|
|
106
|
+
* `Error.stack` does not include the cause on V8, so logging the stack alone
|
|
107
|
+
* drops the driver's own message: a `DatabaseError` wrapping a unique-constraint
|
|
108
|
+
* violation would be logged as "insert failed" with no mention of which
|
|
109
|
+
* constraint. §17 promises the operator gets everything, and this is the part
|
|
110
|
+
* that is easiest to lose. Depth is bounded because a chain can be cyclic.
|
|
111
|
+
*/
|
|
112
|
+
function causeChain(exception, depth = 0) {
|
|
113
|
+
if (depth > 4 || !(exception instanceof Error))
|
|
114
|
+
return [];
|
|
115
|
+
const cause = exception.cause;
|
|
116
|
+
if (cause === undefined)
|
|
117
|
+
return [];
|
|
118
|
+
// An `Error` cause is flattened to text, since its own stack would double the
|
|
119
|
+
// length of the line for no new information. Anything else is kept as it is, so
|
|
120
|
+
// that `redact` still walks it and blanks a credential-named field inside.
|
|
121
|
+
const described = cause instanceof Error ? `${cause.name}: ${cause.message}` : cause;
|
|
122
|
+
return [described, ...causeChain(cause, depth + 1)];
|
|
123
|
+
}
|
|
124
|
+
let AllExceptionsFilter = class AllExceptionsFilter {
|
|
125
|
+
adapterHost;
|
|
126
|
+
context;
|
|
127
|
+
logger;
|
|
128
|
+
constructor(adapterHost, context, logger) {
|
|
129
|
+
this.adapterHost = adapterHost;
|
|
130
|
+
this.context = context;
|
|
131
|
+
this.logger = logger;
|
|
132
|
+
}
|
|
133
|
+
catch(exception, host) {
|
|
134
|
+
if (host.getType() !== 'http')
|
|
135
|
+
throw exception;
|
|
136
|
+
const normalized = this.#normalize(exception);
|
|
137
|
+
const requestContext = this.context.get();
|
|
138
|
+
const http = host.switchToHttp();
|
|
139
|
+
const request = http.getRequest();
|
|
140
|
+
// Normally the middleware has already settled this and everything from the
|
|
141
|
+
// guards onwards shares it. It has not on one path: body parsing runs before
|
|
142
|
+
// module middleware, so an oversized or malformed body is rejected with no
|
|
143
|
+
// context open — and §17 promises the client a failure it can quote back.
|
|
144
|
+
// `resolveRequestId` is the middleware's own function, so a caller-supplied
|
|
145
|
+
// id gets the same validation here as everywhere else rather than being
|
|
146
|
+
// echoed unchecked into a log line.
|
|
147
|
+
const requestId = requestContext?.requestId ?? (0, request_context_middleware_js_1.resolveRequestId)(headerOf(request));
|
|
148
|
+
// A token in the query string (as the legacy socket handshake used) would
|
|
149
|
+
// otherwise be written to the log on every failure.
|
|
150
|
+
this.#log(exception, normalized, { method: request.method, url: (0, redaction_js_1.redactUrl)(request.url) });
|
|
151
|
+
const body = (0, envelope_js_1.errorEnvelope)(normalized.payload, {
|
|
152
|
+
requestId,
|
|
153
|
+
...(requestContext?.apiVersion === undefined ? {} : { version: requestContext.apiVersion }),
|
|
154
|
+
});
|
|
155
|
+
const { httpAdapter } = this.adapterHost;
|
|
156
|
+
const response = http.getResponse();
|
|
157
|
+
// The middleware sets this header on its way out; on the pre-middleware path
|
|
158
|
+
// nothing has, and an id in the body that is not in the headers is the one
|
|
159
|
+
// place a client's logging middleware would miss it.
|
|
160
|
+
if (requestContext === undefined) {
|
|
161
|
+
httpAdapter.setHeader(response, constants_js_1.REQUEST_ID_HEADER, requestId);
|
|
162
|
+
}
|
|
163
|
+
httpAdapter.reply(response, body, normalized.status);
|
|
164
|
+
}
|
|
165
|
+
#normalize(exception) {
|
|
166
|
+
if ((0, nage_error_js_1.isNageError)(exception)) {
|
|
167
|
+
return {
|
|
168
|
+
status: exception.httpStatus,
|
|
169
|
+
payload: exception.toPayload(),
|
|
170
|
+
logMeta: { ...exception.meta },
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (exception instanceof common_1.HttpException) {
|
|
174
|
+
const status = exception.getStatus();
|
|
175
|
+
const response = exception.getResponse();
|
|
176
|
+
const details = toValidationDetails(response);
|
|
177
|
+
const code = details === undefined
|
|
178
|
+
? (CODE_BY_STATUS[status] ?? (status >= 500 ? 'INTERNAL_ERROR' : 'INVALID_QUERY'))
|
|
179
|
+
: 'VALIDATION_FAILED';
|
|
180
|
+
// A 5xx from Nest may carry an internal message; only sub-500 messages
|
|
181
|
+
// are considered safe to pass through.
|
|
182
|
+
const message = status >= 500
|
|
183
|
+
? catalog_js_1.DEFAULT_SAFE_MESSAGE.INTERNAL_ERROR
|
|
184
|
+
: typeof response === 'string'
|
|
185
|
+
? response
|
|
186
|
+
: response.message !== undefined &&
|
|
187
|
+
typeof response.message === 'string'
|
|
188
|
+
? response.message
|
|
189
|
+
: catalog_js_1.DEFAULT_SAFE_MESSAGE[code];
|
|
190
|
+
return {
|
|
191
|
+
status: details === undefined ? status : common_1.HttpStatus.UNPROCESSABLE_ENTITY,
|
|
192
|
+
payload: details === undefined
|
|
193
|
+
? { code, message }
|
|
194
|
+
: { code, message: 'Validation failed', details },
|
|
195
|
+
logMeta: {},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
// body-parser rejects an oversized or malformed body from middleware, before
|
|
199
|
+
// any handler exists to throw an `HttpException`. Its errors are plain
|
|
200
|
+
// `Error`s carrying `type` and `status`, so without this they fall through to
|
|
201
|
+
// the bug branch below and a client that sent 6 MB is told the server broke.
|
|
202
|
+
const parserStatus = bodyParserStatus(exception);
|
|
203
|
+
if (parserStatus !== undefined) {
|
|
204
|
+
const code = CODE_BY_STATUS[parserStatus] ?? 'INVALID_QUERY';
|
|
205
|
+
return {
|
|
206
|
+
status: parserStatus,
|
|
207
|
+
payload: { code, message: catalog_js_1.DEFAULT_SAFE_MESSAGE[code] },
|
|
208
|
+
// The declared limit and the length that breached it, for the operator
|
|
209
|
+
// only: a client that can discover the exact ceiling can size requests to
|
|
210
|
+
// sit just under it.
|
|
211
|
+
logMeta: {
|
|
212
|
+
parserErrorType: exception.type,
|
|
213
|
+
limit: exception.limit,
|
|
214
|
+
length: exception.length,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
// Anything else is a bug: nothing about it is safe to return.
|
|
219
|
+
return {
|
|
220
|
+
status: common_1.HttpStatus.INTERNAL_SERVER_ERROR,
|
|
221
|
+
payload: { code: 'INTERNAL_ERROR', message: catalog_js_1.DEFAULT_SAFE_MESSAGE.INTERNAL_ERROR },
|
|
222
|
+
logMeta: {},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
#log(exception, normalized, request) {
|
|
226
|
+
const causes = causeChain(exception);
|
|
227
|
+
const fields = {
|
|
228
|
+
code: normalized.payload.code,
|
|
229
|
+
httpStatus: normalized.status,
|
|
230
|
+
method: request.method,
|
|
231
|
+
url: request.url,
|
|
232
|
+
...normalized.logMeta,
|
|
233
|
+
...(exception instanceof Error
|
|
234
|
+
? { error: exception.name, detail: exception.message, stack: exception.stack }
|
|
235
|
+
: { error: 'UnknownException', detail: String(exception) }),
|
|
236
|
+
...(causes.length === 0 ? {} : { causes }),
|
|
237
|
+
};
|
|
238
|
+
// 5xx is ours to fix; 4xx is the caller's mistake and only worth a warning.
|
|
239
|
+
if (normalized.status >= 500) {
|
|
240
|
+
this.logger.error('Request failed', fields);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
this.logger.warn('Request rejected', fields);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
exports.AllExceptionsFilter = AllExceptionsFilter;
|
|
248
|
+
exports.AllExceptionsFilter = AllExceptionsFilter = __decorate([
|
|
249
|
+
(0, common_1.Catch)(),
|
|
250
|
+
__param(0, (0, common_1.Inject)(core_1.HttpAdapterHost)),
|
|
251
|
+
__param(1, (0, common_1.Inject)(request_context_service_js_1.RequestContextService)),
|
|
252
|
+
__param(2, (0, common_1.Inject)(tokens_js_1.NAGE_LOGGER)),
|
|
253
|
+
__metadata("design:paramtypes", [core_1.HttpAdapterHost,
|
|
254
|
+
request_context_service_js_1.RequestContextService, Object])
|
|
255
|
+
], AllExceptionsFilter);
|
|
256
|
+
//# sourceMappingURL=all-exceptions.filter.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builders for the single API envelope (PLAN.md §16.1).
|
|
3
|
+
*
|
|
4
|
+
* One place constructs the success shape, one place constructs the error shape;
|
|
5
|
+
* the interceptor and the filter are thin wrappers around these. The legacy
|
|
6
|
+
* framework assembled the envelope by hand in 18 controllers.
|
|
7
|
+
*/
|
|
8
|
+
import type { ErrorPayload, ErrorResponse, Paginated, PaginationMeta, ResponseMeta, SuccessResponse } from '@nage-api/contracts';
|
|
9
|
+
export interface EnvelopeMetaInput {
|
|
10
|
+
readonly requestId: string;
|
|
11
|
+
readonly version?: number;
|
|
12
|
+
readonly pagination?: PaginationMeta;
|
|
13
|
+
/** Injectable for deterministic tests; defaults to now. */
|
|
14
|
+
readonly timestamp?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function buildMeta(input: EnvelopeMetaInput): ResponseMeta;
|
|
17
|
+
export declare function successEnvelope<TData>(data: TData, meta: EnvelopeMetaInput): SuccessResponse<TData>;
|
|
18
|
+
export declare function errorEnvelope(error: ErrorPayload, meta: EnvelopeMetaInput): ErrorResponse;
|
|
19
|
+
/**
|
|
20
|
+
* Recognise a repository page so the interceptor can lift `records` to `data`
|
|
21
|
+
* and `pagination` into `meta` — the client contract stays flat while services
|
|
22
|
+
* keep returning a single value.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isPaginated(value: unknown): value is Paginated<unknown>;
|
|
25
|
+
//# sourceMappingURL=envelope.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Builders for the single API envelope (PLAN.md §16.1).
|
|
4
|
+
*
|
|
5
|
+
* One place constructs the success shape, one place constructs the error shape;
|
|
6
|
+
* the interceptor and the filter are thin wrappers around these. The legacy
|
|
7
|
+
* framework assembled the envelope by hand in 18 controllers.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.buildMeta = buildMeta;
|
|
11
|
+
exports.successEnvelope = successEnvelope;
|
|
12
|
+
exports.errorEnvelope = errorEnvelope;
|
|
13
|
+
exports.isPaginated = isPaginated;
|
|
14
|
+
function buildMeta(input) {
|
|
15
|
+
return {
|
|
16
|
+
requestId: input.requestId,
|
|
17
|
+
timestamp: input.timestamp ?? new Date().toISOString(),
|
|
18
|
+
...(input.version === undefined ? {} : { version: input.version }),
|
|
19
|
+
...(input.pagination === undefined ? {} : { pagination: input.pagination }),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function successEnvelope(data, meta) {
|
|
23
|
+
return { success: true, data, meta: buildMeta(meta) };
|
|
24
|
+
}
|
|
25
|
+
function errorEnvelope(error, meta) {
|
|
26
|
+
return { success: false, error, meta: buildMeta(meta) };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Recognise a repository page so the interceptor can lift `records` to `data`
|
|
30
|
+
* and `pagination` into `meta` — the client contract stays flat while services
|
|
31
|
+
* keep returning a single value.
|
|
32
|
+
*/
|
|
33
|
+
function isPaginated(value) {
|
|
34
|
+
if (value === null || typeof value !== 'object')
|
|
35
|
+
return false;
|
|
36
|
+
const candidate = value;
|
|
37
|
+
if (!Array.isArray(candidate.records))
|
|
38
|
+
return false;
|
|
39
|
+
if (candidate.pagination === null || typeof candidate.pagination !== 'object')
|
|
40
|
+
return false;
|
|
41
|
+
const pagination = candidate.pagination;
|
|
42
|
+
return typeof pagination['limit'] === 'number';
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=envelope.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type CustomDecorator } from '@nestjs/common';
|
|
2
|
+
/**
|
|
3
|
+
* Opt a route out of the response envelope — file downloads, health probes
|
|
4
|
+
* consumed by an orchestrator, and third-party webhook callbacks that require
|
|
5
|
+
* an exact body shape.
|
|
6
|
+
*
|
|
7
|
+
* It is the only sanctioned escape hatch, which is what keeps the envelope a
|
|
8
|
+
* guarantee rather than a convention.
|
|
9
|
+
*/
|
|
10
|
+
export declare const NoEnvelope: () => CustomDecorator;
|
|
11
|
+
//# sourceMappingURL=no-envelope.decorator.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NoEnvelope = void 0;
|
|
4
|
+
const common_1 = require("@nestjs/common");
|
|
5
|
+
const constants_js_1 = require("../constants.js");
|
|
6
|
+
/**
|
|
7
|
+
* Opt a route out of the response envelope — file downloads, health probes
|
|
8
|
+
* consumed by an orchestrator, and third-party webhook callbacks that require
|
|
9
|
+
* an exact body shape.
|
|
10
|
+
*
|
|
11
|
+
* It is the only sanctioned escape hatch, which is what keeps the envelope a
|
|
12
|
+
* guarantee rather than a convention.
|
|
13
|
+
*/
|
|
14
|
+
const NoEnvelope = () => (0, common_1.SetMetadata)(constants_js_1.METADATA_KEYS.noEnvelope, true);
|
|
15
|
+
exports.NoEnvelope = NoEnvelope;
|
|
16
|
+
//# sourceMappingURL=no-envelope.decorator.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type CustomDecorator } from '@nestjs/common';
|
|
2
|
+
/**
|
|
3
|
+
* Give one route a different budget from the global one (§21).
|
|
4
|
+
*
|
|
5
|
+
* The routes that need it are the ones whose duration is the caller's business:
|
|
6
|
+
* a large upload, a report, a long poll.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* @RequestTimeout(120_000)
|
|
10
|
+
* @Post('reports/annual')
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export declare const RequestTimeout: (timeoutMs: number) => CustomDecorator;
|
|
14
|
+
/**
|
|
15
|
+
* Remove the ceiling for one route — server-sent events and streaming
|
|
16
|
+
* downloads, where the response is meant to stay open.
|
|
17
|
+
*
|
|
18
|
+
* Prefer `RequestTimeout` with a generous value: "no ceiling" means a wedged
|
|
19
|
+
* dependency on this route holds its connection until the client gives up, and
|
|
20
|
+
* during a deploy it is what the drain waits for.
|
|
21
|
+
*/
|
|
22
|
+
export declare const SkipRequestTimeout: () => CustomDecorator;
|
|
23
|
+
//# sourceMappingURL=request-timeout.decorators.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SkipRequestTimeout = exports.RequestTimeout = void 0;
|
|
4
|
+
const common_1 = require("@nestjs/common");
|
|
5
|
+
const constants_js_1 = require("../constants.js");
|
|
6
|
+
/**
|
|
7
|
+
* Give one route a different budget from the global one (§21).
|
|
8
|
+
*
|
|
9
|
+
* The routes that need it are the ones whose duration is the caller's business:
|
|
10
|
+
* a large upload, a report, a long poll.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* @RequestTimeout(120_000)
|
|
14
|
+
* @Post('reports/annual')
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
const RequestTimeout = (timeoutMs) => (0, common_1.SetMetadata)(constants_js_1.METADATA_KEYS.requestTimeout, timeoutMs);
|
|
18
|
+
exports.RequestTimeout = RequestTimeout;
|
|
19
|
+
/**
|
|
20
|
+
* Remove the ceiling for one route — server-sent events and streaming
|
|
21
|
+
* downloads, where the response is meant to stay open.
|
|
22
|
+
*
|
|
23
|
+
* Prefer `RequestTimeout` with a generous value: "no ceiling" means a wedged
|
|
24
|
+
* dependency on this route holds its connection until the client gives up, and
|
|
25
|
+
* during a deploy it is what the drain waits for.
|
|
26
|
+
*/
|
|
27
|
+
const SkipRequestTimeout = () => (0, common_1.SetMetadata)(constants_js_1.METADATA_KEYS.requestTimeout, 0);
|
|
28
|
+
exports.SkipRequestTimeout = SkipRequestTimeout;
|
|
29
|
+
//# sourceMappingURL=request-timeout.decorators.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A ceiling on how long a request may take (PLAN.md §21).
|
|
3
|
+
*
|
|
4
|
+
* Before this there was none, at any layer: not in `bootstrap`, not in the
|
|
5
|
+
* config surface, not in Nest. A handler awaiting a dependency that never
|
|
6
|
+
* answers held its connection until the caller's own timeout fired, and until
|
|
7
|
+
* then it also held a rate-limit slot, an unfinished log line, and — during a
|
|
8
|
+
* deploy — a drain that could not complete.
|
|
9
|
+
*
|
|
10
|
+
* What this does **not** do, and cannot: cancel the handler. Node cannot abort a
|
|
11
|
+
* promise that is already running, so the work continues in the background after
|
|
12
|
+
* the caller has been answered 504. That is still the right trade — the client
|
|
13
|
+
* and the socket are released on a schedule the server controls — but it means
|
|
14
|
+
* this is not a substitute for a deadline on the dependency itself
|
|
15
|
+
* (`acquireTimeoutMs` on the pool, a timeout on the HTTP client). It bounds the
|
|
16
|
+
* caller's wait, not the process's work.
|
|
17
|
+
*/
|
|
18
|
+
import { type CallHandler, type ExecutionContext, type NestInterceptor } from '@nestjs/common';
|
|
19
|
+
import { Reflector } from '@nestjs/core';
|
|
20
|
+
import type { NageCoreConfig } from '@nage-api/contracts';
|
|
21
|
+
import { type Observable } from 'rxjs';
|
|
22
|
+
export declare class RequestTimeoutInterceptor implements NestInterceptor {
|
|
23
|
+
#private;
|
|
24
|
+
private readonly reflector;
|
|
25
|
+
constructor(reflector: Reflector, config: NageCoreConfig);
|
|
26
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown>;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=request-timeout.interceptor.d.ts.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* A ceiling on how long a request may take (PLAN.md §21).
|
|
4
|
+
*
|
|
5
|
+
* Before this there was none, at any layer: not in `bootstrap`, not in the
|
|
6
|
+
* config surface, not in Nest. A handler awaiting a dependency that never
|
|
7
|
+
* answers held its connection until the caller's own timeout fired, and until
|
|
8
|
+
* then it also held a rate-limit slot, an unfinished log line, and — during a
|
|
9
|
+
* deploy — a drain that could not complete.
|
|
10
|
+
*
|
|
11
|
+
* What this does **not** do, and cannot: cancel the handler. Node cannot abort a
|
|
12
|
+
* promise that is already running, so the work continues in the background after
|
|
13
|
+
* the caller has been answered 504. That is still the right trade — the client
|
|
14
|
+
* and the socket are released on a schedule the server controls — but it means
|
|
15
|
+
* this is not a substitute for a deadline on the dependency itself
|
|
16
|
+
* (`acquireTimeoutMs` on the pool, a timeout on the HTTP client). It bounds the
|
|
17
|
+
* caller's wait, not the process's work.
|
|
18
|
+
*/
|
|
19
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
20
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
21
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
22
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
23
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
24
|
+
};
|
|
25
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
26
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
27
|
+
};
|
|
28
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
29
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
30
|
+
};
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.RequestTimeoutInterceptor = void 0;
|
|
33
|
+
const common_1 = require("@nestjs/common");
|
|
34
|
+
const core_1 = require("@nestjs/core");
|
|
35
|
+
const rxjs_1 = require("rxjs");
|
|
36
|
+
const operators_1 = require("rxjs/operators");
|
|
37
|
+
const constants_js_1 = require("../constants.js");
|
|
38
|
+
const catalog_js_1 = require("../errors/catalog.js");
|
|
39
|
+
const tokens_js_1 = require("../tokens.js");
|
|
40
|
+
let RequestTimeoutInterceptor = class RequestTimeoutInterceptor {
|
|
41
|
+
reflector;
|
|
42
|
+
#timeoutMs;
|
|
43
|
+
constructor(reflector, config) {
|
|
44
|
+
this.reflector = reflector;
|
|
45
|
+
this.#timeoutMs = config.http?.requestTimeoutMs ?? constants_js_1.DEFAULT_REQUEST_TIMEOUT_MS;
|
|
46
|
+
}
|
|
47
|
+
intercept(context, next) {
|
|
48
|
+
// Only HTTP: a queue consumer's deadline is the job's, and a websocket
|
|
49
|
+
// handler has no request to time out.
|
|
50
|
+
if (context.getType() !== 'http')
|
|
51
|
+
return next.handle();
|
|
52
|
+
const ms = this.#resolve(context);
|
|
53
|
+
if (ms <= 0)
|
|
54
|
+
return next.handle();
|
|
55
|
+
return next.handle().pipe((0, operators_1.timeout)({
|
|
56
|
+
// `each`, not `first`: the budget is per emission, so a streaming
|
|
57
|
+
// response is not cut off for having taken a long time in total.
|
|
58
|
+
each: ms,
|
|
59
|
+
with: () => (0, rxjs_1.throwError)(() => new catalog_js_1.RequestTimeoutError(ms)),
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
/** Route metadata wins over the class, and both win over configuration. */
|
|
63
|
+
#resolve(context) {
|
|
64
|
+
const override = this.reflector.getAllAndOverride(constants_js_1.METADATA_KEYS.requestTimeout, [context.getHandler(), context.getClass()]);
|
|
65
|
+
return typeof override === 'number' ? override : this.#timeoutMs;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
exports.RequestTimeoutInterceptor = RequestTimeoutInterceptor;
|
|
69
|
+
exports.RequestTimeoutInterceptor = RequestTimeoutInterceptor = __decorate([
|
|
70
|
+
(0, common_1.Injectable)(),
|
|
71
|
+
__param(0, (0, common_1.Inject)(core_1.Reflector)),
|
|
72
|
+
__param(1, (0, common_1.Inject)(tokens_js_1.NAGE_CONFIG)),
|
|
73
|
+
__metadata("design:paramtypes", [core_1.Reflector, Object])
|
|
74
|
+
], RequestTimeoutInterceptor);
|
|
75
|
+
//# sourceMappingURL=request-timeout.interceptor.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single global response interceptor (PLAN.md §16.1).
|
|
3
|
+
*
|
|
4
|
+
* Controllers `return data`. This wraps it. Nothing in application code touches
|
|
5
|
+
* `res` — the `@Res()` escape hatch used by 18 legacy controllers bypassed the
|
|
6
|
+
* interceptor pipeline entirely and is what made the envelope inconsistent.
|
|
7
|
+
*/
|
|
8
|
+
import { type CallHandler, type ExecutionContext, type NestInterceptor } from '@nestjs/common';
|
|
9
|
+
import { Reflector } from '@nestjs/core';
|
|
10
|
+
import type { Observable } from 'rxjs';
|
|
11
|
+
import { RequestContextService } from '../context/request-context.service.js';
|
|
12
|
+
export declare class ResponseInterceptor implements NestInterceptor {
|
|
13
|
+
#private;
|
|
14
|
+
private readonly reflector;
|
|
15
|
+
private readonly context;
|
|
16
|
+
constructor(reflector: Reflector, context: RequestContextService);
|
|
17
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown>;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=response.interceptor.d.ts.map
|