@verdeect/kit 0.0.0-stage → 1.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +132 -0
- package/README.md +804 -2
- package/alerts/verdeect-kit.rules.yml +133 -0
- package/dist/cli/commands/crypto.d.ts +9 -0
- package/dist/cli/commands/crypto.js +87 -0
- package/dist/cli/commands/db.d.ts +11 -0
- package/dist/cli/commands/db.js +151 -0
- package/dist/cli/commands/openapi.d.ts +9 -0
- package/dist/cli/commands/openapi.js +144 -0
- package/dist/cli/commands/rabbitmq.d.ts +11 -0
- package/dist/cli/commands/rabbitmq.js +124 -0
- package/dist/cli/commands/redis.d.ts +11 -0
- package/dist/cli/commands/redis.js +119 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +31 -0
- package/dist/config/cli-schemas.d.ts +29 -0
- package/dist/config/cli-schemas.js +15 -0
- package/dist/config/define-env.d.ts +21 -0
- package/dist/config/define-env.js +43 -0
- package/dist/config/fragments.d.ts +105 -0
- package/dist/config/fragments.js +86 -0
- package/dist/config/helpers.d.ts +26 -0
- package/dist/config/helpers.js +62 -0
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.js +7 -0
- package/dist/config/kit-config.module.d.ts +20 -0
- package/dist/config/kit-config.module.js +23 -0
- package/dist/config/parse-env.d.ts +14 -0
- package/dist/config/parse-env.js +28 -0
- package/dist/crypto/cipher.d.ts +61 -0
- package/dist/crypto/cipher.js +136 -0
- package/dist/crypto/index.d.ts +2 -0
- package/dist/crypto/index.js +2 -0
- package/dist/database/batches.d.ts +42 -0
- package/dist/database/batches.js +49 -0
- package/dist/database/db.d.ts +93 -0
- package/dist/database/db.js +234 -0
- package/dist/database/if-newer.d.ts +9 -0
- package/dist/database/if-newer.js +15 -0
- package/dist/database/index.d.ts +17 -0
- package/dist/database/index.js +13 -0
- package/dist/database/kit-database.module.d.ts +47 -0
- package/dist/database/kit-database.module.js +181 -0
- package/dist/database/migrations.d.ts +42 -0
- package/dist/database/migrations.js +142 -0
- package/dist/database/pg.d.ts +20 -0
- package/dist/database/pg.js +19 -0
- package/dist/database/prepare-database.d.ts +39 -0
- package/dist/database/prepare-database.js +75 -0
- package/dist/database/run-exclusive.d.ts +19 -0
- package/dist/database/run-exclusive.js +62 -0
- package/dist/database/tenant.d.ts +34 -0
- package/dist/database/tenant.js +28 -0
- package/dist/http/api/api.decorator.d.ts +18 -0
- package/dist/http/api/api.decorator.js +9 -0
- package/dist/http/api/openapi-document.d.ts +40 -0
- package/dist/http/api/openapi-document.js +452 -0
- package/dist/http/bootstrap-service.d.ts +33 -0
- package/dist/http/bootstrap-service.js +107 -0
- package/dist/http/create-http-app.d.ts +38 -0
- package/dist/http/create-http-app.js +137 -0
- package/dist/http/errors.d.ts +118 -0
- package/dist/http/errors.js +94 -0
- package/dist/http/index.d.ts +12 -0
- package/dist/http/index.js +8 -0
- package/dist/http/kit-exception.filter.d.ts +18 -0
- package/dist/http/kit-exception.filter.js +151 -0
- package/dist/http/kit-http-core.module.d.ts +19 -0
- package/dist/http/kit-http-core.module.js +122 -0
- package/dist/http/pagination.d.ts +30 -0
- package/dist/http/pagination.js +73 -0
- package/dist/http/zod-validation.pipe.d.ts +15 -0
- package/dist/http/zod-validation.pipe.js +47 -0
- package/dist/idempotency/idempotency.interceptor.d.ts +55 -0
- package/dist/idempotency/idempotency.interceptor.js +317 -0
- package/dist/idempotency/idempotent.decorator.d.ts +7 -0
- package/dist/idempotency/idempotent.decorator.js +16 -0
- package/dist/idempotency/index.d.ts +7 -0
- package/dist/idempotency/index.js +6 -0
- package/dist/idempotency/kit-idempotency.module.d.ts +47 -0
- package/dist/idempotency/kit-idempotency.module.js +173 -0
- package/dist/idempotency/peer.d.ts +1 -0
- package/dist/idempotency/peer.js +4 -0
- package/dist/idempotency/table.d.ts +203 -0
- package/dist/idempotency/table.js +86 -0
- package/dist/internal/cleanup-result.d.ts +7 -0
- package/dist/internal/cleanup-result.js +12 -0
- package/dist/internal/ids.d.ts +13 -0
- package/dist/internal/ids.js +41 -0
- package/dist/internal/metadata.d.ts +12 -0
- package/dist/internal/metadata.js +3 -0
- package/dist/internal/outbox-policy.d.ts +8 -0
- package/dist/internal/outbox-policy.js +10 -0
- package/dist/internal/peer.d.ts +16 -0
- package/dist/internal/peer.js +56 -0
- package/dist/internal/pg-errors.d.ts +21 -0
- package/dist/internal/pg-errors.js +115 -0
- package/dist/internal/preparsed-env.d.ts +4 -0
- package/dist/internal/preparsed-env.js +14 -0
- package/dist/internal/rabbitmq-definitions.d.ts +85 -0
- package/dist/internal/rabbitmq-definitions.js +73 -0
- package/dist/internal/redis-acl.d.ts +21 -0
- package/dist/internal/redis-acl.js +30 -0
- package/dist/internal/service-name.d.ts +4 -0
- package/dist/internal/service-name.js +10 -0
- package/dist/internal/sql-identifier.d.ts +5 -0
- package/dist/internal/sql-identifier.js +12 -0
- package/dist/internal/tokens.d.ts +10 -0
- package/dist/internal/tokens.js +12 -0
- package/dist/internal/tx-context.d.ts +5 -0
- package/dist/internal/tx-context.js +9 -0
- package/dist/messaging/cleanup.d.ts +49 -0
- package/dist/messaging/cleanup.js +129 -0
- package/dist/messaging/config.d.ts +43 -0
- package/dist/messaging/config.js +8 -0
- package/dist/messaging/define-event.d.ts +29 -0
- package/dist/messaging/define-event.js +38 -0
- package/dist/messaging/envelope.d.ts +66 -0
- package/dist/messaging/envelope.js +49 -0
- package/dist/messaging/errors.d.ts +45 -0
- package/dist/messaging/errors.js +63 -0
- package/dist/messaging/inbox.d.ts +4 -0
- package/dist/messaging/inbox.js +9 -0
- package/dist/messaging/index.d.ts +13 -0
- package/dist/messaging/index.js +10 -0
- package/dist/messaging/kit-messaging.module.d.ts +39 -0
- package/dist/messaging/kit-messaging.module.js +309 -0
- package/dist/messaging/outbox-relay.d.ts +67 -0
- package/dist/messaging/outbox-relay.js +260 -0
- package/dist/messaging/outbox-store.d.ts +75 -0
- package/dist/messaging/outbox-store.js +126 -0
- package/dist/messaging/peer.d.ts +1 -0
- package/dist/messaging/peer.js +5 -0
- package/dist/messaging/rabbit-connection.d.ts +144 -0
- package/dist/messaging/rabbit-connection.js +472 -0
- package/dist/messaging/redelivery.d.ts +6 -0
- package/dist/messaging/redelivery.js +17 -0
- package/dist/messaging/subscriber.d.ts +102 -0
- package/dist/messaging/subscriber.js +412 -0
- package/dist/messaging/tables.d.ts +315 -0
- package/dist/messaging/tables.js +72 -0
- package/dist/messaging/topology.d.ts +56 -0
- package/dist/messaging/topology.js +56 -0
- package/dist/observability/fail-startup.d.ts +14 -0
- package/dist/observability/fail-startup.js +51 -0
- package/dist/observability/health-registry.d.ts +34 -0
- package/dist/observability/health-registry.js +81 -0
- package/dist/observability/index.d.ts +16 -0
- package/dist/observability/index.js +10 -0
- package/dist/observability/lifecycle.d.ts +52 -0
- package/dist/observability/lifecycle.js +151 -0
- package/dist/observability/logger-options.d.ts +54 -0
- package/dist/observability/logger-options.js +91 -0
- package/dist/observability/metrics-server.d.ts +33 -0
- package/dist/observability/metrics-server.js +108 -0
- package/dist/observability/metrics.d.ts +30 -0
- package/dist/observability/metrics.js +53 -0
- package/dist/observability/redaction.d.ts +23 -0
- package/dist/observability/redaction.js +65 -0
- package/dist/observability/sanitize-error.d.ts +47 -0
- package/dist/observability/sanitize-error.js +194 -0
- package/dist/observability/sentry.d.ts +64 -0
- package/dist/observability/sentry.js +233 -0
- package/dist/redis/channels.d.ts +34 -0
- package/dist/redis/channels.js +128 -0
- package/dist/redis/client.d.ts +42 -0
- package/dist/redis/client.js +85 -0
- package/dist/redis/index.d.ts +7 -0
- package/dist/redis/index.js +7 -0
- package/dist/redis/kit-redis.module.d.ts +18 -0
- package/dist/redis/kit-redis.module.js +131 -0
- package/dist/redis/peer.d.ts +1 -0
- package/dist/redis/peer.js +4 -0
- package/dist/redis/rate-limiter.d.ts +49 -0
- package/dist/redis/rate-limiter.js +139 -0
- package/dist/testing/count-queries.d.ts +30 -0
- package/dist/testing/count-queries.js +58 -0
- package/dist/testing/create-test-app.d.ts +26 -0
- package/dist/testing/create-test-app.js +32 -0
- package/dist/testing/expect-matches-api.d.ts +8 -0
- package/dist/testing/expect-matches-api.js +49 -0
- package/dist/testing/expect-problem.d.ts +24 -0
- package/dist/testing/expect-problem.js +23 -0
- package/dist/testing/images.d.ts +10 -0
- package/dist/testing/images.js +14 -0
- package/dist/testing/index.d.ts +23 -0
- package/dist/testing/index.js +17 -0
- package/dist/testing/launch-command.d.ts +52 -0
- package/dist/testing/launch-command.js +510 -0
- package/dist/testing/outbox-coverage.d.ts +26 -0
- package/dist/testing/outbox-coverage.js +98 -0
- package/dist/testing/rabbitmq-admin.d.ts +81 -0
- package/dist/testing/rabbitmq-admin.js +146 -0
- package/dist/testing/redis-worker.d.ts +10 -0
- package/dist/testing/redis-worker.js +15 -0
- package/dist/testing/repo-conventions.d.ts +10 -0
- package/dist/testing/repo-conventions.js +524 -0
- package/dist/testing/repo-files.d.ts +113 -0
- package/dist/testing/repo-files.js +401 -0
- package/dist/testing/scratch-database.d.ts +19 -0
- package/dist/testing/scratch-database.js +75 -0
- package/dist/testing/start-test-stack.d.ts +85 -0
- package/dist/testing/start-test-stack.js +342 -0
- package/dist/testing/tcp-proxy.d.ts +40 -0
- package/dist/testing/tcp-proxy.js +164 -0
- package/dist/testing/tenant-isolation.d.ts +31 -0
- package/dist/testing/tenant-isolation.js +205 -0
- package/dist/testing/test-events.d.ts +37 -0
- package/dist/testing/test-events.js +150 -0
- package/dist/testing/timeout-scale.d.ts +4 -0
- package/dist/testing/timeout-scale.js +17 -0
- package/dist/testing/timing-guard.d.ts +22 -0
- package/dist/testing/timing-guard.js +54 -0
- package/dist/testing/worker-scope.d.ts +31 -0
- package/dist/testing/worker-scope.js +68 -0
- package/package.json +177 -4
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
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;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
// createHttpApp (prd.md §3.5, AC-3.4, AC-3.5, AC-3.12; §3.2 шаги 2–3; KT-11, KT-15 п. 1). Эталоны: O/P main.ts (NestFactory,
|
|
8
|
+
// FastifyAdapter({genReqId}), bufferLogs, useLogger), P/O configure-http-server.ts + security-headers.ts (onRequest на
|
|
9
|
+
// reply.raw: nosniff, no-referrer), регистрация фильтра — KitHttpCoreModule. По PRD: участники KitLifecycle вместо
|
|
10
|
+
// enableShutdownHooks/InstanceShutdown — шаг 2 `http` (server.close Fastify), шаг 3 `nest` (исходный app.close Nest);
|
|
11
|
+
// app.close() экземпляра = lifecycle.close(); своих парсеров тела нет (лимит — bodyLimit адаптера и route.bodyLimit).
|
|
12
|
+
import { Module } from '@nestjs/common';
|
|
13
|
+
import { NestApplication, NestFactory } from '@nestjs/core';
|
|
14
|
+
import { FastifyAdapter } from '@nestjs/platform-fastify';
|
|
15
|
+
import { Logger as NestPinoLogger } from 'nestjs-pino';
|
|
16
|
+
import { assertServiceName } from '../internal/service-name.js';
|
|
17
|
+
import { KIT_LOGGER } from '../internal/tokens.js';
|
|
18
|
+
import { KitLifecycle } from '../observability/lifecycle.js';
|
|
19
|
+
import { traceIdGenerator } from '../observability/logger-options.js';
|
|
20
|
+
import { KitHttpCoreModule } from './kit-http-core.module.js';
|
|
21
|
+
/** Заголовки безопасности API (P/O security-headers.ts): есть и на ошибках, `/ok` и 404. */
|
|
22
|
+
const API_SECURITY_HEADERS = [
|
|
23
|
+
['X-Content-Type-Options', 'nosniff'],
|
|
24
|
+
['Referrer-Policy', 'no-referrer'],
|
|
25
|
+
];
|
|
26
|
+
/** Корень приложения: модуль сервиса и ядро. */
|
|
27
|
+
let KitHttpRootModule = class KitHttpRootModule {
|
|
28
|
+
};
|
|
29
|
+
KitHttpRootModule = __decorate([
|
|
30
|
+
Module({})
|
|
31
|
+
], KitHttpRootModule);
|
|
32
|
+
const defaultCompile = (root, adapter) => NestFactory.create(root, adapter, {
|
|
33
|
+
bufferLogs: true,
|
|
34
|
+
// С умолчанием true Nest при ошибке DI сам вызывает process.exit(1), минуя failStartup.
|
|
35
|
+
abortOnError: false,
|
|
36
|
+
});
|
|
37
|
+
/** Хуки Fastify и участник шага 2: заголовки, Connection: close и закрытие простаивающих соединений при остановке. */
|
|
38
|
+
function installServerHooks(app) {
|
|
39
|
+
const fastify = app.getHttpAdapter().getInstance();
|
|
40
|
+
let closing = false;
|
|
41
|
+
// Все хуки — до init: addHook после listen() бросает FST_ERR_INSTANCE_ALREADY_LISTENING.
|
|
42
|
+
fastify.addHook('onRequest', (_request, reply, done) => {
|
|
43
|
+
for (const [name, value] of API_SECURITY_HEADERS) {
|
|
44
|
+
reply.raw.setHeader(name, value);
|
|
45
|
+
}
|
|
46
|
+
done();
|
|
47
|
+
});
|
|
48
|
+
fastify.addHook('onSend', (_request, reply, _payload, done) => {
|
|
49
|
+
if (closing) {
|
|
50
|
+
void reply.header('Connection', 'close');
|
|
51
|
+
}
|
|
52
|
+
done();
|
|
53
|
+
});
|
|
54
|
+
fastify.addHook('onResponse', (_request, _reply, done) => {
|
|
55
|
+
if (closing) {
|
|
56
|
+
// keep-alive соединение, ответ на котором закончился после начала close(), иначе живёт до keepAliveTimeout.
|
|
57
|
+
fastify.server.closeIdleConnections();
|
|
58
|
+
}
|
|
59
|
+
done();
|
|
60
|
+
});
|
|
61
|
+
return async () => {
|
|
62
|
+
const server = fastify.server;
|
|
63
|
+
if (!server.listening) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
closing = true;
|
|
67
|
+
const closed = new Promise((resolve, reject) => {
|
|
68
|
+
server.close((error) => (error === undefined ? resolve() : reject(error)));
|
|
69
|
+
});
|
|
70
|
+
server.closeIdleConnections();
|
|
71
|
+
await closed;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Регистрирует участников шагов 2–3 и подменяет app.close() на lifecycle.close(). */
|
|
75
|
+
function installLifecycle(app, closeServer, logger) {
|
|
76
|
+
const lifecycle = app.get(KitLifecycle);
|
|
77
|
+
lifecycle.register({
|
|
78
|
+
name: 'http',
|
|
79
|
+
stop: async () => {
|
|
80
|
+
const startedAt = performance.now();
|
|
81
|
+
await closeServer();
|
|
82
|
+
logger.debug({ durationMs: Math.round(performance.now() - startedAt) }, 'http: приём соединений остановлен');
|
|
83
|
+
},
|
|
84
|
+
}, { step: 2 });
|
|
85
|
+
logger.debug({ participant: 'http' }, 'http: участник шага 2 зарегистрирован');
|
|
86
|
+
lifecycle.register({
|
|
87
|
+
name: 'nest',
|
|
88
|
+
// Исходный close — с прототипа: app.close уже подменён и ждал бы сам себя.
|
|
89
|
+
stop: async () => {
|
|
90
|
+
await NestApplication.prototype.close.call(app);
|
|
91
|
+
logger.debug('http: хуки Nest завершены');
|
|
92
|
+
},
|
|
93
|
+
}, { step: 3 });
|
|
94
|
+
// Присваивание не работает: set-ловушка прокси NestFactory не сохраняет значение; defineProperty уходит в цель.
|
|
95
|
+
Object.defineProperty(app, 'close', {
|
|
96
|
+
value: () => lifecycle.close(),
|
|
97
|
+
configurable: true,
|
|
98
|
+
writable: true,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/** Внутренняя сборка приложения: «тот же createHttpApp» для bootstrapService и createTestApp (§3.8). */
|
|
102
|
+
export async function buildHttpApp(options) {
|
|
103
|
+
assertServiceName(options.service, options.caller ?? 'createHttpApp');
|
|
104
|
+
const root = {
|
|
105
|
+
module: KitHttpRootModule,
|
|
106
|
+
imports: [
|
|
107
|
+
options.module,
|
|
108
|
+
KitHttpCoreModule.forRoot({
|
|
109
|
+
service: options.service,
|
|
110
|
+
logger: options.logger,
|
|
111
|
+
redaction: options.redaction,
|
|
112
|
+
}),
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
const adapter = new FastifyAdapter({
|
|
116
|
+
genReqId: traceIdGenerator(options.logger?.trustTraceparent !== false),
|
|
117
|
+
...(options.bodyLimit === undefined ? {} : { bodyLimit: options.bodyLimit }),
|
|
118
|
+
});
|
|
119
|
+
const app = await (options.compile ?? defaultCompile)(root, adapter);
|
|
120
|
+
app.useLogger(app.get(NestPinoLogger));
|
|
121
|
+
const logger = app.get(KIT_LOGGER);
|
|
122
|
+
const closeServer = installServerHooks(app);
|
|
123
|
+
installLifecycle(app, closeServer, logger);
|
|
124
|
+
await app.init();
|
|
125
|
+
await app.getHttpAdapter().getInstance().ready();
|
|
126
|
+
logger.debug({ service: options.service, bodyLimit: options.bodyLimit ?? null }, 'http: приложение собрано');
|
|
127
|
+
return app;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Fastify-приложение Nest сервиса (§3.5): заголовки безопасности API, trace-id из `traceparent`, `GET /ok`, фильтр
|
|
131
|
+
* `KitExceptionFilter`, 404 `ROUTE_NOT_FOUND`, логгер nestjs-pino, `KitLifecycle` и `HealthRegistry` для модулей сервиса.
|
|
132
|
+
* `app.close()` — программная остановка по шагам 1–5 (§3.2) без сторожа и `exit`. Процесс сервиса запускает
|
|
133
|
+
* `bootstrapService`; `createHttpApp` — для своих точек входа и тестов.
|
|
134
|
+
*/
|
|
135
|
+
export function createHttpApp(options) {
|
|
136
|
+
return buildHttpApp(options);
|
|
137
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/** Запись словаря ошибок: HTTP-статус и текст по-русски. */
|
|
2
|
+
export interface KitErrorEntry {
|
|
3
|
+
/** HTTP-статус ответа. */
|
|
4
|
+
readonly status: number;
|
|
5
|
+
/** Текст для пользователя по-русски. */
|
|
6
|
+
readonly message: string;
|
|
7
|
+
}
|
|
8
|
+
/** Словарь ошибок: код (SCREAMING_SNAKE) → статус и текст. */
|
|
9
|
+
export type KitErrorDictionary = Readonly<Record<string, KitErrorEntry>>;
|
|
10
|
+
/** Общие коды пакета (§3.5); сервис дополняет их своими через `defineErrors({...kitErrors, …})`. */
|
|
11
|
+
export declare const kitErrors: {
|
|
12
|
+
readonly UNAUTHENTICATED: {
|
|
13
|
+
readonly status: 401;
|
|
14
|
+
readonly message: "Требуется вход";
|
|
15
|
+
};
|
|
16
|
+
readonly FORBIDDEN: {
|
|
17
|
+
readonly status: 403;
|
|
18
|
+
readonly message: "Доступ запрещён";
|
|
19
|
+
};
|
|
20
|
+
readonly VALIDATION_FAILED: {
|
|
21
|
+
readonly status: 422;
|
|
22
|
+
readonly message: "Некорректные данные запроса";
|
|
23
|
+
};
|
|
24
|
+
readonly BAD_REQUEST: {
|
|
25
|
+
readonly status: 400;
|
|
26
|
+
readonly message: "Некорректный запрос";
|
|
27
|
+
};
|
|
28
|
+
readonly PAYLOAD_TOO_LARGE: {
|
|
29
|
+
readonly status: 413;
|
|
30
|
+
readonly message: "Слишком большой запрос";
|
|
31
|
+
};
|
|
32
|
+
readonly UNSUPPORTED_MEDIA_TYPE: {
|
|
33
|
+
readonly status: 415;
|
|
34
|
+
readonly message: "Неподдерживаемый формат запроса";
|
|
35
|
+
};
|
|
36
|
+
readonly ROUTE_NOT_FOUND: {
|
|
37
|
+
readonly status: 404;
|
|
38
|
+
readonly message: "Адрес не найден";
|
|
39
|
+
};
|
|
40
|
+
readonly VERSION_CONFLICT: {
|
|
41
|
+
readonly status: 409;
|
|
42
|
+
readonly message: "Данные изменились в другом окне. Обновите страницу.";
|
|
43
|
+
};
|
|
44
|
+
readonly RATE_LIMITED: {
|
|
45
|
+
readonly status: 429;
|
|
46
|
+
readonly message: "Слишком много запросов. Повторите позже.";
|
|
47
|
+
};
|
|
48
|
+
readonly INTERNAL_ERROR: {
|
|
49
|
+
readonly status: 500;
|
|
50
|
+
readonly message: "Внутренняя ошибка сервиса";
|
|
51
|
+
};
|
|
52
|
+
readonly SERVICE_UNAVAILABLE: {
|
|
53
|
+
readonly status: 503;
|
|
54
|
+
readonly message: "Сервис временно недоступен. Повторите попытку позже.";
|
|
55
|
+
};
|
|
56
|
+
readonly SERVICE_STOPPING: {
|
|
57
|
+
readonly status: 503;
|
|
58
|
+
readonly message: "Сервис перезапускается. Повторите через несколько секунд.";
|
|
59
|
+
};
|
|
60
|
+
readonly PLATFORM_UNAVAILABLE: {
|
|
61
|
+
readonly status: 503;
|
|
62
|
+
readonly message: "Сервис прав временно недоступен. Повторите позже";
|
|
63
|
+
};
|
|
64
|
+
readonly IDEMPOTENCY_KEY_REUSED: {
|
|
65
|
+
readonly status: 422;
|
|
66
|
+
readonly message: "Ключ Idempotency-Key уже использован с другим запросом.";
|
|
67
|
+
};
|
|
68
|
+
readonly IDEMPOTENCY_IN_PROGRESS: {
|
|
69
|
+
readonly status: 409;
|
|
70
|
+
readonly message: "Запрос с этим ключом ещё выполняется.";
|
|
71
|
+
};
|
|
72
|
+
readonly IDEMPOTENCY_KEY_REQUIRED: {
|
|
73
|
+
readonly status: 400;
|
|
74
|
+
readonly message: "Нужен заголовок Idempotency-Key.";
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
/** Коды пакета в словаре сервиса сохраняют свой статус: иначе свойство — `never` и ошибка компиляции (AC-3.2). */
|
|
78
|
+
type KitCodesKeepStatus<D> = {
|
|
79
|
+
readonly [K in keyof D & keyof typeof kitErrors]: D[K] extends {
|
|
80
|
+
readonly status: (typeof kitErrors)[K]['status'];
|
|
81
|
+
} ? unknown : never;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Регистрирует словарь ошибок сервиса на процесс (§3.5). Повторный вызов объединяет словари; тот же код с другим
|
|
85
|
+
* статусом — `Error` при определении (ошибка разработчика), код пакета с другим статусом — ошибка компиляции.
|
|
86
|
+
* Возвращает тот же словарь.
|
|
87
|
+
*/
|
|
88
|
+
export declare function defineErrors<const D extends KitErrorDictionary>(dict: D & KitCodesKeepStatus<D>): D;
|
|
89
|
+
/** Текст кода по-русски; неизвестный код — «Ошибка запроса». Внутренняя функция. */
|
|
90
|
+
export declare function errorMessage(code: string): string;
|
|
91
|
+
/** Статус кода из реестра; `undefined` — код не зарегистрирован. Внутренняя функция. */
|
|
92
|
+
export declare function errorStatus(code: string): number | undefined;
|
|
93
|
+
/** Тело ответа ошибки (§3.5). */
|
|
94
|
+
interface ErrorBody {
|
|
95
|
+
readonly error: {
|
|
96
|
+
readonly code: string;
|
|
97
|
+
readonly message: string;
|
|
98
|
+
readonly details: Readonly<Record<string, unknown>>;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Ожидаемая ошибка домена (§3.5): статус и текст — из словаря `defineErrors`, `warn` без GlitchTip. Незарегистрированный
|
|
103
|
+
* код — ошибка разработчика: статус 500, фильтр отвечает `INTERNAL_ERROR` и отправляет событие. `options.cause` —
|
|
104
|
+
* исходная ошибка (например, срок пула БД). Заголовков нет; `Retry-After` для `RATE_LIMITED` ставит `KitExceptionFilter` по
|
|
105
|
+
* `details.retryAfterSec`.
|
|
106
|
+
*/
|
|
107
|
+
export declare class DomainError extends Error {
|
|
108
|
+
/** Код ошибки (SCREAMING_SNAKE). */
|
|
109
|
+
readonly code: string;
|
|
110
|
+
/** HTTP-статус из словаря; незарегистрированный код — 500. */
|
|
111
|
+
readonly status: number;
|
|
112
|
+
/** Подробности без секретов и ПДн; по умолчанию `{}`. */
|
|
113
|
+
readonly details: Readonly<Record<string, unknown>>;
|
|
114
|
+
constructor(code: string, details?: Readonly<Record<string, unknown>>, options?: ErrorOptions);
|
|
115
|
+
/** Тело ответа `{error: {code, message, details}}`. */
|
|
116
|
+
toBody(): ErrorBody;
|
|
117
|
+
}
|
|
118
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// kitErrors, defineErrors, DomainError (prd.md §3.5, AC-3.2, AC-3.3). Эталон — O domain-error.ts + error-messages.ts (тело
|
|
2
|
+
// {error: {code, message, details}}, name = new.target.name, общий текст «Ошибка запроса»). По PRD: запись словаря
|
|
3
|
+
// {status, message}, реестр на процесс (как registerErrorSanitizer), статус берётся из словаря, коды §3.5; заголовков в
|
|
4
|
+
// DomainError нет; Retry-After для RATE_LIMITED ставит KitExceptionFilter по details.retryAfterSec (E8, §3.7), третий аргумент — стандартный {cause}. Тексты — O error-messages.ts и guard
|
|
5
|
+
// @verdeect/auth.
|
|
6
|
+
/** Общие коды пакета (§3.5); сервис дополняет их своими через `defineErrors({...kitErrors, …})`. */
|
|
7
|
+
export const kitErrors = {
|
|
8
|
+
UNAUTHENTICATED: { status: 401, message: 'Требуется вход' },
|
|
9
|
+
FORBIDDEN: { status: 403, message: 'Доступ запрещён' },
|
|
10
|
+
VALIDATION_FAILED: { status: 422, message: 'Некорректные данные запроса' },
|
|
11
|
+
BAD_REQUEST: { status: 400, message: 'Некорректный запрос' },
|
|
12
|
+
PAYLOAD_TOO_LARGE: { status: 413, message: 'Слишком большой запрос' },
|
|
13
|
+
UNSUPPORTED_MEDIA_TYPE: { status: 415, message: 'Неподдерживаемый формат запроса' },
|
|
14
|
+
ROUTE_NOT_FOUND: { status: 404, message: 'Адрес не найден' },
|
|
15
|
+
VERSION_CONFLICT: {
|
|
16
|
+
status: 409,
|
|
17
|
+
message: 'Данные изменились в другом окне. Обновите страницу.',
|
|
18
|
+
},
|
|
19
|
+
RATE_LIMITED: { status: 429, message: 'Слишком много запросов. Повторите позже.' },
|
|
20
|
+
INTERNAL_ERROR: { status: 500, message: 'Внутренняя ошибка сервиса' },
|
|
21
|
+
SERVICE_UNAVAILABLE: {
|
|
22
|
+
status: 503,
|
|
23
|
+
message: 'Сервис временно недоступен. Повторите попытку позже.',
|
|
24
|
+
},
|
|
25
|
+
SERVICE_STOPPING: {
|
|
26
|
+
status: 503,
|
|
27
|
+
message: 'Сервис перезапускается. Повторите через несколько секунд.',
|
|
28
|
+
},
|
|
29
|
+
PLATFORM_UNAVAILABLE: {
|
|
30
|
+
status: 503,
|
|
31
|
+
message: 'Сервис прав временно недоступен. Повторите позже',
|
|
32
|
+
},
|
|
33
|
+
IDEMPOTENCY_KEY_REUSED: {
|
|
34
|
+
status: 422,
|
|
35
|
+
message: 'Ключ Idempotency-Key уже использован с другим запросом.',
|
|
36
|
+
},
|
|
37
|
+
IDEMPOTENCY_IN_PROGRESS: { status: 409, message: 'Запрос с этим ключом ещё выполняется.' },
|
|
38
|
+
IDEMPOTENCY_KEY_REQUIRED: { status: 400, message: 'Нужен заголовок Idempotency-Key.' },
|
|
39
|
+
};
|
|
40
|
+
/** Общий текст неизвестного кода (O error-messages.ts FALLBACK_MESSAGE). */
|
|
41
|
+
const FALLBACK_MESSAGE = 'Ошибка запроса';
|
|
42
|
+
/** Реестр кодов процесса; `kitErrors` зарегистрирован всегда. */
|
|
43
|
+
const registry = new Map(Object.entries(kitErrors));
|
|
44
|
+
/**
|
|
45
|
+
* Регистрирует словарь ошибок сервиса на процесс (§3.5). Повторный вызов объединяет словари; тот же код с другим
|
|
46
|
+
* статусом — `Error` при определении (ошибка разработчика), код пакета с другим статусом — ошибка компиляции.
|
|
47
|
+
* Возвращает тот же словарь.
|
|
48
|
+
*/
|
|
49
|
+
export function defineErrors(dict) {
|
|
50
|
+
const entries = Object.entries(dict);
|
|
51
|
+
for (const [code, entry] of entries) {
|
|
52
|
+
const known = registry.get(code);
|
|
53
|
+
if (known !== undefined && known.status !== entry.status) {
|
|
54
|
+
throw new Error(`defineErrors: код ${code} уже зарегистрирован со статусом ${known.status}, а не ${entry.status}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (const [code, entry] of entries) {
|
|
58
|
+
registry.set(code, { status: entry.status, message: entry.message });
|
|
59
|
+
}
|
|
60
|
+
return dict;
|
|
61
|
+
}
|
|
62
|
+
/** Текст кода по-русски; неизвестный код — «Ошибка запроса». Внутренняя функция. */
|
|
63
|
+
export function errorMessage(code) {
|
|
64
|
+
return registry.get(code)?.message ?? FALLBACK_MESSAGE;
|
|
65
|
+
}
|
|
66
|
+
/** Статус кода из реестра; `undefined` — код не зарегистрирован. Внутренняя функция. */
|
|
67
|
+
export function errorStatus(code) {
|
|
68
|
+
return registry.get(code)?.status;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Ожидаемая ошибка домена (§3.5): статус и текст — из словаря `defineErrors`, `warn` без GlitchTip. Незарегистрированный
|
|
72
|
+
* код — ошибка разработчика: статус 500, фильтр отвечает `INTERNAL_ERROR` и отправляет событие. `options.cause` —
|
|
73
|
+
* исходная ошибка (например, срок пула БД). Заголовков нет; `Retry-After` для `RATE_LIMITED` ставит `KitExceptionFilter` по
|
|
74
|
+
* `details.retryAfterSec`.
|
|
75
|
+
*/
|
|
76
|
+
export class DomainError extends Error {
|
|
77
|
+
/** Код ошибки (SCREAMING_SNAKE). */
|
|
78
|
+
code;
|
|
79
|
+
/** HTTP-статус из словаря; незарегистрированный код — 500. */
|
|
80
|
+
status;
|
|
81
|
+
/** Подробности без секретов и ПДн; по умолчанию `{}`. */
|
|
82
|
+
details;
|
|
83
|
+
constructor(code, details, options) {
|
|
84
|
+
super(errorMessage(code), options);
|
|
85
|
+
this.name = new.target.name;
|
|
86
|
+
this.code = code;
|
|
87
|
+
this.status = errorStatus(code) ?? 500;
|
|
88
|
+
this.details = details ?? {};
|
|
89
|
+
}
|
|
90
|
+
/** Тело ответа `{error: {code, message, details}}`. */
|
|
91
|
+
toBody() {
|
|
92
|
+
return { error: { code: this.code, message: this.message, details: this.details } };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { bootstrapService } from './bootstrap-service.js';
|
|
2
|
+
export type { KitBootstrapOptions } from './bootstrap-service.js';
|
|
3
|
+
export { createHttpApp } from './create-http-app.js';
|
|
4
|
+
export type { KitHttpAppOptions } from './create-http-app.js';
|
|
5
|
+
export { defineErrors, DomainError, kitErrors } from './errors.js';
|
|
6
|
+
export type { KitErrorDictionary, KitErrorEntry } from './errors.js';
|
|
7
|
+
export { KitExceptionFilter } from './kit-exception.filter.js';
|
|
8
|
+
export { afterCursor, decodeCursor, encodeCursor, pageOf, PageQuerySchema } from './pagination.js';
|
|
9
|
+
export type { Page } from './pagination.js';
|
|
10
|
+
export { ZodValidationPipe } from './zod-validation.pipe.js';
|
|
11
|
+
export { Api } from './api/api.decorator.js';
|
|
12
|
+
export type { KitApiOptions } from './api/api.decorator.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Публичная поверхность @verdeect/kit/http (prd.md §3.5). Всё, что не перечислено, — не экспортируется.
|
|
2
|
+
export { bootstrapService } from './bootstrap-service.js';
|
|
3
|
+
export { createHttpApp } from './create-http-app.js';
|
|
4
|
+
export { defineErrors, DomainError, kitErrors } from './errors.js';
|
|
5
|
+
export { KitExceptionFilter } from './kit-exception.filter.js';
|
|
6
|
+
export { afterCursor, decodeCursor, encodeCursor, pageOf, PageQuerySchema } from './pagination.js';
|
|
7
|
+
export { ZodValidationPipe } from './zod-validation.pipe.js';
|
|
8
|
+
export { Api } from './api/api.decorator.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ArgumentsHost, type ExceptionFilter } from '@nestjs/common';
|
|
2
|
+
/**
|
|
3
|
+
* Фильтр ошибок HTTP сервиса (§3.5). `DomainError` — статус словаря, `warn`, без GlitchTip (`RATE_LIMITED` с
|
|
4
|
+
* `details.retryAfterSec` — ещё `Retry-After`, §3.7); тело `{error}` guard'ов —
|
|
5
|
+
* без изменений; срок пула БД (в том числе в `cause`) — 503 `SERVICE_UNAVAILABLE` + `Retry-After: 1` (метрику и событие
|
|
6
|
+
* пишет пул /database, AC-4.5); `HttpException` без тела 400/401/403/404/413/415/429 — код по статусу, `warn`; прочее —
|
|
7
|
+
* 500 `INTERNAL_ERROR`, `error`, GlitchTip ровно раз. Текст исходной ошибки, стек и SQL в ответ не попадают.
|
|
8
|
+
* `createHttpApp` регистрирует фильтр сам; BFF собирает свой фильтр на `DomainError` и словаре (различие по роли).
|
|
9
|
+
*/
|
|
10
|
+
export declare class KitExceptionFilter implements ExceptionFilter {
|
|
11
|
+
private readonly logger;
|
|
12
|
+
/** Отвечает на исключение запроса по правилам §3.5. */
|
|
13
|
+
catch(exception: unknown, host: ArgumentsHost): void;
|
|
14
|
+
private handleHttpException;
|
|
15
|
+
private warn;
|
|
16
|
+
private reportUnexpected;
|
|
17
|
+
private send;
|
|
18
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
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;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var KitExceptionFilter_1;
|
|
8
|
+
// KitExceptionFilter (prd.md §3.5, AC-3.1–AC-3.4; KT-15 п. 7–8): единый ответ {error: {code, message, details}}.
|
|
9
|
+
// Эталон — O app-exception-filter.ts: порядок веток, libraryBody, send() с Content-Type: application/json; charset=utf-8,
|
|
10
|
+
// reportUnexpected. Из B: ответ после отправленных заголовков — только warn. По PRD: коды HttpException без тела — таблица
|
|
11
|
+
// HTTP_CODES (прочие статусы → 500 INTERNAL_ERROR), DomainError с незарегистрированным кодом → 500, срок пула —
|
|
12
|
+
// isPoolTimeout по форме, 503 без метрики и события: метрику и событие срока пула пишет пул /database (AC-4.5, KT-19
|
|
13
|
+
// п. 3); Retry-After RATE_LIMITED — по details.retryAfterSec (E8, §3.7); codeOf с путями O, maskSecretPath и instance-shutdown не
|
|
14
|
+
// переносятся, путь в лог не пишется (он уже есть в строке доступа).
|
|
15
|
+
import { Catch, HttpException, Injectable, Logger, } from '@nestjs/common';
|
|
16
|
+
import { isPoolTimeout } from '../internal/pg-errors.js';
|
|
17
|
+
import { sanitizeError } from '../observability/sanitize-error.js';
|
|
18
|
+
import { captureError } from '../observability/sentry.js';
|
|
19
|
+
import { DomainError, errorMessage, errorStatus } from './errors.js';
|
|
20
|
+
/** Код ответа HttpException без своего тела (Nest FastifyAdapter.mapException: FastifyError → HttpException). */
|
|
21
|
+
const HTTP_CODES = {
|
|
22
|
+
400: 'BAD_REQUEST',
|
|
23
|
+
401: 'UNAUTHENTICATED',
|
|
24
|
+
403: 'FORBIDDEN',
|
|
25
|
+
404: 'ROUTE_NOT_FOUND',
|
|
26
|
+
413: 'PAYLOAD_TOO_LARGE',
|
|
27
|
+
415: 'UNSUPPORTED_MEDIA_TYPE',
|
|
28
|
+
429: 'RATE_LIMITED',
|
|
29
|
+
};
|
|
30
|
+
/** Коды библиотек при 5xx — ожидаемая недоступность зависимости (эталон O). */
|
|
31
|
+
const EXPECTED_UNAVAILABLE_CODES = new Set(['PLATFORM_UNAVAILABLE']);
|
|
32
|
+
/** `Retry-After` при сроке пула БД (эталон O POOL_RETRY_AFTER). */
|
|
33
|
+
const POOL_RETRY_AFTER = '1';
|
|
34
|
+
/** `Retry-After` для `RATE_LIMITED` — целое `details.retryAfterSec ≥ 1` (§3.7, `throwRateLimited`, E8). */
|
|
35
|
+
function rateLimitRetryAfter(error) {
|
|
36
|
+
if (error.code !== 'RATE_LIMITED') {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const value = error.details.retryAfterSec;
|
|
40
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function bodyOf(code) {
|
|
46
|
+
return { error: { code, message: errorMessage(code), details: {} } };
|
|
47
|
+
}
|
|
48
|
+
/** Тело HttpException уже в формате §3.5 (guard'ы @verdeect/auth, @verdeect/platform-client). */
|
|
49
|
+
function libraryBody(response) {
|
|
50
|
+
if (typeof response !== 'object' || response === null || !('error' in response)) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const error = response.error;
|
|
54
|
+
if (typeof error !== 'object' || error === null) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const { code, message, details } = error;
|
|
58
|
+
if (typeof code !== 'string' || typeof message !== 'string') {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (typeof details !== 'object' || details === null) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return { error: { code, message, details } };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Фильтр ошибок HTTP сервиса (§3.5). `DomainError` — статус словаря, `warn`, без GlitchTip (`RATE_LIMITED` с
|
|
68
|
+
* `details.retryAfterSec` — ещё `Retry-After`, §3.7); тело `{error}` guard'ов —
|
|
69
|
+
* без изменений; срок пула БД (в том числе в `cause`) — 503 `SERVICE_UNAVAILABLE` + `Retry-After: 1` (метрику и событие
|
|
70
|
+
* пишет пул /database, AC-4.5); `HttpException` без тела 400/401/403/404/413/415/429 — код по статусу, `warn`; прочее —
|
|
71
|
+
* 500 `INTERNAL_ERROR`, `error`, GlitchTip ровно раз. Текст исходной ошибки, стек и SQL в ответ не попадают.
|
|
72
|
+
* `createHttpApp` регистрирует фильтр сам; BFF собирает свой фильтр на `DomainError` и словаре (различие по роли).
|
|
73
|
+
*/
|
|
74
|
+
let KitExceptionFilter = KitExceptionFilter_1 = class KitExceptionFilter {
|
|
75
|
+
logger = new Logger(KitExceptionFilter_1.name);
|
|
76
|
+
/** Отвечает на исключение запроса по правилам §3.5. */
|
|
77
|
+
catch(exception, host) {
|
|
78
|
+
const http = host.switchToHttp();
|
|
79
|
+
const reply = http.getResponse();
|
|
80
|
+
const request = http.getRequest();
|
|
81
|
+
if (reply.raw.headersSent) {
|
|
82
|
+
this.logger.warn({ method: request.method, traceId: request.id, err: sanitizeError(exception) }, 'ошибка после отправки заголовков ответа');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// Срок пула — первой веткой: он может быть в cause DomainError (эталон O).
|
|
86
|
+
if (isPoolTimeout(exception)) {
|
|
87
|
+
// Метрику и событие ставит пул /database (KT-19 п. 3); срок своего пула сервиса — 503 без метрики.
|
|
88
|
+
this.warn('SERVICE_UNAVAILABLE', 503, request);
|
|
89
|
+
void reply.header('Retry-After', POOL_RETRY_AFTER);
|
|
90
|
+
this.send(reply, 503, bodyOf('SERVICE_UNAVAILABLE'));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (exception instanceof DomainError && errorStatus(exception.code) !== undefined) {
|
|
94
|
+
this.warn(exception.code, exception.status, request);
|
|
95
|
+
const retryAfterSec = rateLimitRetryAfter(exception);
|
|
96
|
+
if (retryAfterSec !== undefined) {
|
|
97
|
+
void reply.header('Retry-After', String(retryAfterSec));
|
|
98
|
+
}
|
|
99
|
+
this.send(reply, exception.status, exception.toBody());
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (exception instanceof HttpException) {
|
|
103
|
+
this.handleHttpException(exception, reply, request);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
this.reportUnexpected(exception, request);
|
|
107
|
+
this.send(reply, 500, bodyOf('INTERNAL_ERROR'));
|
|
108
|
+
}
|
|
109
|
+
handleHttpException(exception, reply, request) {
|
|
110
|
+
const status = exception.getStatus();
|
|
111
|
+
const library = libraryBody(exception.getResponse());
|
|
112
|
+
if (library !== null) {
|
|
113
|
+
if (status < 500 || EXPECTED_UNAVAILABLE_CODES.has(library.error.code)) {
|
|
114
|
+
this.warn(library.error.code, status, request);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
this.reportUnexpected(exception, request);
|
|
118
|
+
}
|
|
119
|
+
this.send(reply, status, library);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const code = HTTP_CODES[status];
|
|
123
|
+
if (code === undefined) {
|
|
124
|
+
// Прочие 4xx и ≥ 500 без тела — ошибка разработчика: доменные 4xx — только DomainError (KT-15 п. 8).
|
|
125
|
+
this.reportUnexpected(exception, request);
|
|
126
|
+
this.send(reply, 500, bodyOf('INTERNAL_ERROR'));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
this.warn(code, status, request);
|
|
130
|
+
this.send(reply, status, bodyOf(code));
|
|
131
|
+
}
|
|
132
|
+
warn(code, status, request) {
|
|
133
|
+
this.logger.warn({ code, status, method: request.method, traceId: request.id }, 'ожидаемая ошибка запроса');
|
|
134
|
+
}
|
|
135
|
+
reportUnexpected(exception, request) {
|
|
136
|
+
captureError(exception, { tags: { code: 'INTERNAL_ERROR' } });
|
|
137
|
+
// Тело запроса, заголовки и путь не логируются.
|
|
138
|
+
this.logger.error({ err: sanitizeError(exception), method: request.method, traceId: request.id }, 'неожиданная ошибка запроса');
|
|
139
|
+
}
|
|
140
|
+
send(reply, status, payload) {
|
|
141
|
+
void reply
|
|
142
|
+
.status(status)
|
|
143
|
+
.header('Content-Type', 'application/json; charset=utf-8')
|
|
144
|
+
.send(payload);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
KitExceptionFilter = KitExceptionFilter_1 = __decorate([
|
|
148
|
+
Catch(),
|
|
149
|
+
Injectable()
|
|
150
|
+
], KitExceptionFilter);
|
|
151
|
+
export { KitExceptionFilter };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type DynamicModule } from '@nestjs/common';
|
|
2
|
+
import { type KitLoggerOptions } from '../observability/logger-options.js';
|
|
3
|
+
import type { KitRedaction } from '../observability/redaction.js';
|
|
4
|
+
/** Опции логгера по роли сервиса (§3.5). */
|
|
5
|
+
export type KitHttpLoggerOptions = Pick<KitLoggerOptions, 'trustTraceparent' | 'quietRoutes' | 'extraBase'>;
|
|
6
|
+
/** Опции ядра HTTP. */
|
|
7
|
+
export interface KitHttpCoreOptions {
|
|
8
|
+
/** Имя сервиса (уже проверено вызывающим). */
|
|
9
|
+
readonly service: string;
|
|
10
|
+
/** Опции логгера по роли сервиса. */
|
|
11
|
+
readonly logger?: KitHttpLoggerOptions;
|
|
12
|
+
/** Правила редактирования сервиса. */
|
|
13
|
+
readonly redaction?: KitRedaction;
|
|
14
|
+
}
|
|
15
|
+
/** Внутренний глобальный модуль ядра; подключает его `createHttpApp`. */
|
|
16
|
+
export declare class KitHttpCoreModule {
|
|
17
|
+
/** Ядро одного экземпляра приложения; экспортирует KitLifecycle, HealthRegistry, KIT_METRICS, KIT_LOGGER. */
|
|
18
|
+
static forRoot(options: KitHttpCoreOptions): DynamicModule;
|
|
19
|
+
}
|