@verdeect/kit 0.0.0-stage → 1.0.0-rc.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/CHANGELOG.md +138 -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,53 @@
|
|
|
1
|
+
// kitMetrics и alertRulesTemplate (prd.md §3.2, AC-2.8, AC-2.11). Эталон — O metrics-server.ts (реестр на экземпляр) и
|
|
2
|
+
// O orch-metrics.ts (метрики на реестре экземпляра). По PRD v1.12: @prometheus-io/client, как у O; метка service по
|
|
3
|
+
// умолчанию, метрики процесса Node, имена verdeect_*. Глобального реестра нет: два приложения в процессе не смешиваются.
|
|
4
|
+
// AC-9.5 (E4): метрика очистки verdeect_cleanup_runs_total.
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { collectDefaultMetrics, Counter, Gauge, Registry } from '@prometheus-io/client';
|
|
7
|
+
import { assertServiceName } from '../internal/service-name.js';
|
|
8
|
+
/** Создаёт реестр экземпляра с меткой `service`, метриками процесса Node и метриками пакета (§3.2, AC-2.8). */
|
|
9
|
+
export function kitMetrics(options) {
|
|
10
|
+
assertServiceName(options.service, 'kitMetrics');
|
|
11
|
+
const registry = new Registry();
|
|
12
|
+
registry.setDefaultLabels({ service: options.service });
|
|
13
|
+
collectDefaultMetrics({ register: registry });
|
|
14
|
+
const registers = [registry];
|
|
15
|
+
return {
|
|
16
|
+
registry,
|
|
17
|
+
outboxPending: new Gauge({
|
|
18
|
+
name: 'verdeect_outbox_pending',
|
|
19
|
+
help: 'Неотправленные строки outbox',
|
|
20
|
+
registers,
|
|
21
|
+
}),
|
|
22
|
+
outboxLagSeconds: new Gauge({
|
|
23
|
+
name: 'verdeect_outbox_lag_seconds',
|
|
24
|
+
help: 'Возраст самой старой неотправленной строки outbox, с',
|
|
25
|
+
registers,
|
|
26
|
+
}),
|
|
27
|
+
eventsConsumed: new Counter({
|
|
28
|
+
name: 'verdeect_events_consumed_total',
|
|
29
|
+
help: 'Принятые события по типу и результату (handled, duplicate, retry, dlq)',
|
|
30
|
+
labelNames: ['type', 'result'],
|
|
31
|
+
registers,
|
|
32
|
+
}),
|
|
33
|
+
dbPoolWaitTimeouts: new Counter({
|
|
34
|
+
name: 'verdeect_db_pool_wait_timeouts_total',
|
|
35
|
+
help: 'Истёк срок ожидания клиента пула БД',
|
|
36
|
+
labelNames: ['pool'],
|
|
37
|
+
registers,
|
|
38
|
+
}),
|
|
39
|
+
rateLimitUnavailable: new Counter({
|
|
40
|
+
name: 'verdeect_rate_limit_unavailable_total',
|
|
41
|
+
help: 'Проверки лимита без Redis (запрос пропущен)',
|
|
42
|
+
registers,
|
|
43
|
+
}),
|
|
44
|
+
cleanupRuns: new Counter({
|
|
45
|
+
name: 'verdeect_cleanup_runs_total',
|
|
46
|
+
help: 'Проходы фоновой очистки по таблице и итогу (ok, partial, interrupted, error)',
|
|
47
|
+
labelNames: ['table', 'result'],
|
|
48
|
+
registers,
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Абсолютный путь к шаблону правил vmalert `alerts/verdeect-kit.rules.yml` внутри установленного пакета (§3.2). */
|
|
53
|
+
export const alertRulesTemplate = fileURLToPath(new URL('../../alerts/verdeect-kit.rules.yml', import.meta.url));
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Базовый список чувствительных имён (§3.2): заголовки, секреты и токены, ПДн, `body.code` и `body.state`.
|
|
3
|
+
* Каждое имя скрывается на уровнях 0–2 в логах и по имени — в событиях GlitchTip. `err.code` не скрывается.
|
|
4
|
+
*/
|
|
5
|
+
export declare const SENSITIVE_KEYS: readonly string[];
|
|
6
|
+
/**
|
|
7
|
+
* Правила редактирования сервиса — один объект на сервис (константа `src/redaction.ts`). Передаётся и в
|
|
8
|
+
* `createLoggerOptions`, и в `initSentry`: имена и маска пути для логов и GlitchTip совпадают по построению.
|
|
9
|
+
* `src/redaction.ts` сервиса загружается в `instrument.ts` до Sentry, поэтому импортирует только типы из
|
|
10
|
+
* `@verdeect/kit/observability` (GAP-9).
|
|
11
|
+
*/
|
|
12
|
+
export interface KitRedaction {
|
|
13
|
+
/** Имена, добавляемые к `SENSITIVE_KEYS` (объединение без дублей); пустой массив базовый список не отключает. */
|
|
14
|
+
readonly sensitiveKeys?: readonly string[];
|
|
15
|
+
/** Маска пути для лога доступа и `request.url` события GlitchTip; по умолчанию путь не меняется. */
|
|
16
|
+
readonly maskPath?: (path: string) => string;
|
|
17
|
+
}
|
|
18
|
+
/** `SENSITIVE_KEYS` ∪ `redaction.sensitiveKeys` без дублей; базовый список присутствует всегда. */
|
|
19
|
+
export declare function sensitiveNames(redaction?: KitRedaction): readonly string[];
|
|
20
|
+
/** Пути `redact` pino: каждое имя на уровнях 0–2 (`k`, `*.k`, `*.*.k`). */
|
|
21
|
+
export declare function redactPaths(redaction?: KitRedaction): string[];
|
|
22
|
+
/** Маска пути сервиса; по умолчанию — тождество. */
|
|
23
|
+
export declare function maskPathOf(redaction?: KitRedaction): (path: string) => string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Список чувствительных имён и KitRedaction (prd.md §3.2, O1, O3, GAP-6, GAP-8; NFR-4). Эталон — REDACT_PATHS O
|
|
2
|
+
// observability.module.ts; имена — по PRD, ключи сервисов (O ORCHESTRATOR_KEYS, I csrfToken/stepToken) — в KitRedaction
|
|
3
|
+
// сервиса.
|
|
4
|
+
/**
|
|
5
|
+
* Базовый список чувствительных имён (§3.2): заголовки, секреты и токены, ПДн, `body.code` и `body.state`.
|
|
6
|
+
* Каждое имя скрывается на уровнях 0–2 в логах и по имени — в событиях GlitchTip. `err.code` не скрывается.
|
|
7
|
+
*/
|
|
8
|
+
export const SENSITIVE_KEYS = Object.freeze([
|
|
9
|
+
'authorization',
|
|
10
|
+
'cookie',
|
|
11
|
+
'set-cookie',
|
|
12
|
+
'idempotency-key',
|
|
13
|
+
'password',
|
|
14
|
+
'newPassword',
|
|
15
|
+
'currentPassword',
|
|
16
|
+
'secret',
|
|
17
|
+
'clientSecret',
|
|
18
|
+
'token',
|
|
19
|
+
'access_token',
|
|
20
|
+
'refresh_token',
|
|
21
|
+
'id_token',
|
|
22
|
+
'logout_token',
|
|
23
|
+
'accessToken',
|
|
24
|
+
'code_verifier',
|
|
25
|
+
'nonce',
|
|
26
|
+
'email',
|
|
27
|
+
'givenName',
|
|
28
|
+
'familyName',
|
|
29
|
+
'middleName',
|
|
30
|
+
'body.code',
|
|
31
|
+
'body.state',
|
|
32
|
+
]);
|
|
33
|
+
/** Сегмент пути pino: имя из `[A-Za-z0-9_]` — через точку, прочее — в скобках `["…"]`. */
|
|
34
|
+
const PLAIN_SEGMENT = /^[A-Za-z0-9_]+$/;
|
|
35
|
+
/** `SENSITIVE_KEYS` ∪ `redaction.sensitiveKeys` без дублей; базовый список присутствует всегда. */
|
|
36
|
+
export function sensitiveNames(redaction) {
|
|
37
|
+
return [...new Set([...SENSITIVE_KEYS, ...(redaction?.sensitiveKeys ?? [])])];
|
|
38
|
+
}
|
|
39
|
+
/** Путь имени без префикса: `body.code` → `body.code`, `set-cookie` → `["set-cookie"]`. */
|
|
40
|
+
function namePath(name) {
|
|
41
|
+
return name
|
|
42
|
+
.split('.')
|
|
43
|
+
.map((segment, index) => {
|
|
44
|
+
if (!PLAIN_SEGMENT.test(segment)) {
|
|
45
|
+
return `[${JSON.stringify(segment)}]`;
|
|
46
|
+
}
|
|
47
|
+
return index === 0 ? segment : `.${segment}`;
|
|
48
|
+
})
|
|
49
|
+
.join('');
|
|
50
|
+
}
|
|
51
|
+
/** Присоединяет путь имени к префиксу `*`/`*.*`: скобка идёт без точки. */
|
|
52
|
+
function withPrefix(prefix, path) {
|
|
53
|
+
return path.startsWith('[') ? `${prefix}${path}` : `${prefix}.${path}`;
|
|
54
|
+
}
|
|
55
|
+
/** Пути `redact` pino: каждое имя на уровнях 0–2 (`k`, `*.k`, `*.*.k`). */
|
|
56
|
+
export function redactPaths(redaction) {
|
|
57
|
+
return sensitiveNames(redaction).flatMap((name) => {
|
|
58
|
+
const path = namePath(name);
|
|
59
|
+
return [path, withPrefix('*', path), withPrefix('*.*', path)];
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** Маска пути сервиса; по умолчанию — тождество. */
|
|
63
|
+
export function maskPathOf(redaction) {
|
|
64
|
+
return redaction?.maskPath ?? ((path) => path);
|
|
65
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Предел обхода цепочки `cause` (§3.2). */
|
|
2
|
+
export declare const MAX_CAUSE_DEPTH = 10;
|
|
3
|
+
/** Ошибка БД без SQL, параметров и значений: только метаданные pg. */
|
|
4
|
+
export declare class SanitizedDbError extends Error {
|
|
5
|
+
readonly name = "SanitizedDbError";
|
|
6
|
+
/** SQLSTATE. */
|
|
7
|
+
readonly code?: string;
|
|
8
|
+
/** Имя ограничения. */
|
|
9
|
+
readonly constraint?: string;
|
|
10
|
+
/** Имя таблицы. */
|
|
11
|
+
readonly table?: string;
|
|
12
|
+
/** Имя столбца. */
|
|
13
|
+
readonly column?: string;
|
|
14
|
+
/** Уровень pg (`ERROR`, `FATAL`). */
|
|
15
|
+
readonly severity?: string;
|
|
16
|
+
}
|
|
17
|
+
/** Ошибка ioredis без аргументов команды: имя, код и имя команды. */
|
|
18
|
+
export declare class SanitizedRedisError extends Error {
|
|
19
|
+
readonly name = "SanitizedRedisError";
|
|
20
|
+
/** Код ошибки ioredis. */
|
|
21
|
+
code?: string;
|
|
22
|
+
/** Имя команды Redis без аргументов. */
|
|
23
|
+
command?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Регистрирует ветку очистки сервиса (§3.2), например SMTP Identity. Глобально на процесс; повторная
|
|
27
|
+
* регистрация того же `match` игнорируется. `match` проверяется на каждом звене цепочки `cause`, `sanitize`
|
|
28
|
+
* получает совпавшее звено.
|
|
29
|
+
*/
|
|
30
|
+
export declare function registerErrorSanitizer(match: (error: unknown) => boolean, sanitize: (error: unknown) => Error): void;
|
|
31
|
+
/**
|
|
32
|
+
* Ошибка без SQL, параметров и значений (§3.2, AC-2.3). Ветки по форме объекта на любом звене цепочки `cause`
|
|
33
|
+
* (до 10): PostgreSQL/Drizzle → `code`, `constraint`, `table`, `column`, `severity` и «Failed query (<code>)»;
|
|
34
|
+
* ioredis → `name`, `code`, имя команды; затем ветки `registerErrorSanitizer`. Срабатывает первая. Ветки нет —
|
|
35
|
+
* исходная ошибка; цепочка глубже 10 или с циклом — копия с обрезанной цепочкой. Уже очищенная — как есть.
|
|
36
|
+
*/
|
|
37
|
+
export declare function sanitizeError(error: unknown): unknown;
|
|
38
|
+
/**
|
|
39
|
+
* formatters.log для pino: ключ `err` и любые Error верхнего уровня (error, cause, …) — через sanitizeError.
|
|
40
|
+
* Вызывается до сериализаторов с исходным объектом: поля pg из cause ещё доступны.
|
|
41
|
+
*/
|
|
42
|
+
export declare function sanitizeLogObject(object: Record<string, unknown>): Record<string, unknown>;
|
|
43
|
+
/**
|
|
44
|
+
* Сериализатор `err` для pino: любой logger.error({err}) проходит через sanitizeError. Получает и Error (pino), и
|
|
45
|
+
* сериализованный объект (pino-http оборачивает через wrapErrorSerializer).
|
|
46
|
+
*/
|
|
47
|
+
export declare function serializeError(error: unknown): unknown;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// sanitizeError и registerErrorSanitizer (prd.md §3.2, AC-2.3, NFR-4). Эталон — O sanitize-error.ts (SanitizedDbError,
|
|
2
|
+
// проверка pg по форме, «Failed query (<code>)», стек без первой строки, sanitizeLogObject, serializeError); ветка
|
|
3
|
+
// ioredis — B sanitize-error.ts (SanitizedRedisError). По PRD: Drizzle распознаётся по форме, без импорта drizzle-orm;
|
|
4
|
+
// цепочка cause — не глубже 10 звеньев и без зацикливания; ветки сервисов — через registerErrorSanitizer (SMTP Identity).
|
|
5
|
+
import { stdSerializers } from 'pino';
|
|
6
|
+
/** Предел обхода цепочки `cause` (§3.2). */
|
|
7
|
+
export const MAX_CAUSE_DEPTH = 10;
|
|
8
|
+
const PG_FIELDS = ['code', 'constraint', 'table', 'column', 'severity'];
|
|
9
|
+
const SQLSTATE = /^[\dA-Z]{5}$/;
|
|
10
|
+
const DRIZZLE_QUERY_ERROR = 'DrizzleQueryError';
|
|
11
|
+
/** Ошибка БД без SQL, параметров и значений: только метаданные pg. */
|
|
12
|
+
export class SanitizedDbError extends Error {
|
|
13
|
+
name = 'SanitizedDbError';
|
|
14
|
+
/** SQLSTATE. */
|
|
15
|
+
code;
|
|
16
|
+
/** Имя ограничения. */
|
|
17
|
+
constraint;
|
|
18
|
+
/** Имя таблицы. */
|
|
19
|
+
table;
|
|
20
|
+
/** Имя столбца. */
|
|
21
|
+
column;
|
|
22
|
+
/** Уровень pg (`ERROR`, `FATAL`). */
|
|
23
|
+
severity;
|
|
24
|
+
}
|
|
25
|
+
/** Ошибка ioredis без аргументов команды: имя, код и имя команды. */
|
|
26
|
+
export class SanitizedRedisError extends Error {
|
|
27
|
+
name = 'SanitizedRedisError';
|
|
28
|
+
/** Код ошибки ioredis. */
|
|
29
|
+
code;
|
|
30
|
+
/** Имя команды Redis без аргументов. */
|
|
31
|
+
command;
|
|
32
|
+
}
|
|
33
|
+
/** Ветки сервисов — глобально на процесс, в порядке регистрации. */
|
|
34
|
+
const registered = [];
|
|
35
|
+
/** Результаты sanitizeError: повторно не обрабатываются. */
|
|
36
|
+
const sanitizedResults = new WeakSet();
|
|
37
|
+
/**
|
|
38
|
+
* Регистрирует ветку очистки сервиса (§3.2), например SMTP Identity. Глобально на процесс; повторная
|
|
39
|
+
* регистрация того же `match` игнорируется. `match` проверяется на каждом звене цепочки `cause`, `sanitize`
|
|
40
|
+
* получает совпавшее звено.
|
|
41
|
+
*/
|
|
42
|
+
export function registerErrorSanitizer(match, sanitize) {
|
|
43
|
+
if (registered.some((item) => item.match === match)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
registered.push({ match, sanitize });
|
|
47
|
+
}
|
|
48
|
+
/** Цепочка error → cause → … не длиннее MAX_CAUSE_DEPTH, без повторов; truncated — дальше есть звенья. */
|
|
49
|
+
function causeChain(error) {
|
|
50
|
+
const chain = [];
|
|
51
|
+
const seen = new Set();
|
|
52
|
+
let current = error;
|
|
53
|
+
while (typeof current === 'object' && current !== null) {
|
|
54
|
+
if (seen.has(current) || chain.length >= MAX_CAUSE_DEPTH) {
|
|
55
|
+
return { chain, truncated: true };
|
|
56
|
+
}
|
|
57
|
+
seen.add(current);
|
|
58
|
+
chain.push(current);
|
|
59
|
+
current = current.cause;
|
|
60
|
+
}
|
|
61
|
+
return { chain, truncated: false };
|
|
62
|
+
}
|
|
63
|
+
/** Ошибка драйвера pg (DatabaseError): SQLSTATE в code и строковый severity — без импорта pg. */
|
|
64
|
+
function isPgError(value) {
|
|
65
|
+
const { code, severity } = value;
|
|
66
|
+
return typeof code === 'string' && SQLSTATE.test(code) && typeof severity === 'string';
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* DrizzleQueryError по форме: `name` экземпляра или `type` сериализованной формы (pino-http сначала
|
|
70
|
+
* применяет stdSerializers.err). Без импорта drizzle-orm (§3.2).
|
|
71
|
+
*/
|
|
72
|
+
function isDrizzleQueryError(value) {
|
|
73
|
+
const { name, type } = value;
|
|
74
|
+
return name === DRIZZLE_QUERY_ERROR || type === DRIZZLE_QUERY_ERROR;
|
|
75
|
+
}
|
|
76
|
+
/** Ошибка ioredis: поле command `{name, args}` — без импорта ioredis. */
|
|
77
|
+
function isRedisCommandError(value) {
|
|
78
|
+
const command = value.command;
|
|
79
|
+
return typeof command === 'object' && command !== null && 'args' in command;
|
|
80
|
+
}
|
|
81
|
+
/** Строки стека без первой (в ней message исходной ошибки). */
|
|
82
|
+
function framesOnly(stack) {
|
|
83
|
+
if (typeof stack !== 'string') {
|
|
84
|
+
return '';
|
|
85
|
+
}
|
|
86
|
+
const start = stack.indexOf('\n at ');
|
|
87
|
+
return start === -1 ? '' : stack.slice(start);
|
|
88
|
+
}
|
|
89
|
+
/** Ошибка БД → SanitizedDbError: поля pg из первого pg-звена, сообщение «Failed query» (+ code). */
|
|
90
|
+
function sanitizeDbError(error, chain) {
|
|
91
|
+
const fields = {};
|
|
92
|
+
const pg = chain.find(isPgError);
|
|
93
|
+
for (const field of PG_FIELDS) {
|
|
94
|
+
const value = pg?.[field];
|
|
95
|
+
if (typeof value === 'string') {
|
|
96
|
+
fields[field] = value;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const sanitized = new SanitizedDbError(fields.code === undefined ? 'Failed query' : `Failed query (${fields.code})`);
|
|
100
|
+
Object.assign(sanitized, fields);
|
|
101
|
+
sanitized.stack = `${sanitized.name}: ${sanitized.message}${framesOnly(error.stack)}`;
|
|
102
|
+
return sanitized;
|
|
103
|
+
}
|
|
104
|
+
/** Ошибка ioredis → SanitizedRedisError: code и имя команды, без args. */
|
|
105
|
+
function sanitizeRedisError(error, redis) {
|
|
106
|
+
const { code, command } = redis;
|
|
107
|
+
const commandName = typeof command.name === 'string' ? command.name : undefined;
|
|
108
|
+
const sanitized = new SanitizedRedisError(commandName === undefined
|
|
109
|
+
? 'Redis command failed'
|
|
110
|
+
: `Redis command failed (${commandName})`);
|
|
111
|
+
if (typeof code === 'string') {
|
|
112
|
+
sanitized.code = code;
|
|
113
|
+
}
|
|
114
|
+
if (commandName !== undefined) {
|
|
115
|
+
sanitized.command = commandName;
|
|
116
|
+
}
|
|
117
|
+
sanitized.stack = `${sanitized.name}: ${sanitized.message}${framesOnly(error.stack)}`;
|
|
118
|
+
return sanitized;
|
|
119
|
+
}
|
|
120
|
+
/** Копия звеньев chain (прототип и собственные свойства), последняя — без cause. */
|
|
121
|
+
function truncatedCopy(chain) {
|
|
122
|
+
const copies = chain.map((link) => Object.create(Object.getPrototypeOf(link), Object.getOwnPropertyDescriptors(link)));
|
|
123
|
+
copies.forEach((copy, index) => {
|
|
124
|
+
const next = copies[index + 1];
|
|
125
|
+
if (next === undefined) {
|
|
126
|
+
delete copy.cause;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
Object.defineProperty(copy, 'cause', {
|
|
130
|
+
value: next,
|
|
131
|
+
writable: true,
|
|
132
|
+
configurable: true,
|
|
133
|
+
enumerable: Object.getOwnPropertyDescriptor(copy, 'cause')?.enumerable ?? false,
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
return copies[0] ?? {};
|
|
137
|
+
}
|
|
138
|
+
/** Применяет ветку и помечает результат очищенным. */
|
|
139
|
+
function mark(result) {
|
|
140
|
+
sanitizedResults.add(result);
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Ошибка без SQL, параметров и значений (§3.2, AC-2.3). Ветки по форме объекта на любом звене цепочки `cause`
|
|
145
|
+
* (до 10): PostgreSQL/Drizzle → `code`, `constraint`, `table`, `column`, `severity` и «Failed query (<code>)»;
|
|
146
|
+
* ioredis → `name`, `code`, имя команды; затем ветки `registerErrorSanitizer`. Срабатывает первая. Ветки нет —
|
|
147
|
+
* исходная ошибка; цепочка глубже 10 или с циклом — копия с обрезанной цепочкой. Уже очищенная — как есть.
|
|
148
|
+
*/
|
|
149
|
+
export function sanitizeError(error) {
|
|
150
|
+
if (typeof error !== 'object' || error === null || sanitizedResults.has(error)) {
|
|
151
|
+
return error;
|
|
152
|
+
}
|
|
153
|
+
const { chain, truncated } = causeChain(error);
|
|
154
|
+
if (chain.some((link) => isDrizzleQueryError(link) || isPgError(link))) {
|
|
155
|
+
return mark(sanitizeDbError(error, chain));
|
|
156
|
+
}
|
|
157
|
+
const redis = chain.find(isRedisCommandError);
|
|
158
|
+
if (redis !== undefined) {
|
|
159
|
+
return mark(sanitizeRedisError(error, redis));
|
|
160
|
+
}
|
|
161
|
+
for (const branch of registered) {
|
|
162
|
+
const link = chain.find((item) => branch.match(item));
|
|
163
|
+
if (link !== undefined) {
|
|
164
|
+
return mark(branch.sanitize(link));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return truncated ? mark(truncatedCopy(chain)) : error;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* formatters.log для pino: ключ `err` и любые Error верхнего уровня (error, cause, …) — через sanitizeError.
|
|
171
|
+
* Вызывается до сериализаторов с исходным объектом: поля pg из cause ещё доступны.
|
|
172
|
+
*/
|
|
173
|
+
export function sanitizeLogObject(object) {
|
|
174
|
+
let result = object;
|
|
175
|
+
for (const [key, value] of Object.entries(object)) {
|
|
176
|
+
if (key !== 'err' && !(value instanceof Error)) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const sanitized = sanitizeError(value);
|
|
180
|
+
if (sanitized !== value) {
|
|
181
|
+
result = result === object ? { ...object } : result;
|
|
182
|
+
result[key] = sanitized;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Сериализатор `err` для pino: любой logger.error({err}) проходит через sanitizeError. Получает и Error (pino), и
|
|
189
|
+
* сериализованный объект (pino-http оборачивает через wrapErrorSerializer).
|
|
190
|
+
*/
|
|
191
|
+
export function serializeError(error) {
|
|
192
|
+
const safe = sanitizeError(error);
|
|
193
|
+
return safe instanceof Error ? stdSerializers.err(safe) : safe;
|
|
194
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Breadcrumb, Event } from '@sentry/nestjs';
|
|
2
|
+
import { type KitRedaction } from './redaction.js';
|
|
3
|
+
/** Опции `initSentry` (§3.2). */
|
|
4
|
+
export interface KitSentryOptions {
|
|
5
|
+
/** Имя сервиса — тег `service` каждого события. */
|
|
6
|
+
readonly service: string;
|
|
7
|
+
/** Правила редактирования сервиса (общие с `createLoggerOptions`). */
|
|
8
|
+
readonly redaction?: KitRedaction;
|
|
9
|
+
}
|
|
10
|
+
/** Sentry 11: вместо sendDefaultPii — ничего из ПДн, заголовков, тел, query и данных БД не собирается. */
|
|
11
|
+
export declare const NO_PERSONAL_DATA: {
|
|
12
|
+
userInfo: false;
|
|
13
|
+
cookies: false;
|
|
14
|
+
httpHeaders: false;
|
|
15
|
+
httpBodies: never[];
|
|
16
|
+
urlQueryParams: false;
|
|
17
|
+
databaseQueryData: false;
|
|
18
|
+
queues: false;
|
|
19
|
+
stackFrameVariables: false;
|
|
20
|
+
genAI: {
|
|
21
|
+
inputs: false;
|
|
22
|
+
outputs: false;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
/** Хлебная крошка без чувствительных данных; адрес — без query и через maskPath. Внутренняя функция. */
|
|
26
|
+
export declare function scrubBreadcrumb(crumb: Breadcrumb, redaction?: KitRedaction): Breadcrumb;
|
|
27
|
+
/**
|
|
28
|
+
* Событие без query, `request.data`, cookie, ПДн пользователя (кроме `id`) и значений чувствительных имён в
|
|
29
|
+
* extra, contexts, tags и breadcrumbs (§3.2, AC-2.5). Внутренняя функция. При исключении отбрасываются
|
|
30
|
+
* request, extra, breadcrumbs и exception.
|
|
31
|
+
*/
|
|
32
|
+
export declare function scrubEvent<T extends Event>(event: T, originalException: unknown, redaction?: KitRedaction): T;
|
|
33
|
+
/**
|
|
34
|
+
* DSN в форме, которую принимает SDK: иначе он сам печатает значение в console.error (NFR-4). Внутренняя функция: её же
|
|
35
|
+
* использует bootstrapService для SENTRY_NOT_PRELOADED.
|
|
36
|
+
*/
|
|
37
|
+
export declare function isValidDsn(dsn: string): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Включает GlitchTip (§3.2): вызывается в `instrument.ts` сервиса, подключённом через `node --import` до загрузки
|
|
40
|
+
* приложения. Включается только при непустом и верном `SENTRY_DSN` и `NODE_ENV = production` (не задан — production);
|
|
41
|
+
* неверное окружение — Sentry выключен, без исключения и лога. `dataCollection: NO_PERSONAL_DATA`,
|
|
42
|
+
* `tracesSampleRate: 0`, тег `service`. Возвращает, включён ли SDK.
|
|
43
|
+
*/
|
|
44
|
+
export declare function initSentry(options: KitSentryOptions): boolean;
|
|
45
|
+
/** Контекст события `captureError`. */
|
|
46
|
+
interface CaptureContext {
|
|
47
|
+
/** Теги события. */
|
|
48
|
+
readonly tags?: Readonly<Record<string, string>>;
|
|
49
|
+
/** Дополнительные данные (проходят скраббер). */
|
|
50
|
+
readonly extra?: Readonly<Record<string, unknown>>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Отправляет ошибку в GlitchTip через `sanitizeError` — ровно одно событие на ошибку (§3.2): объект ошибки
|
|
54
|
+
* помечается отправленным, повторный вызов ничего не шлёт.
|
|
55
|
+
*/
|
|
56
|
+
export declare function captureError(error: unknown, context?: CaptureContext): void;
|
|
57
|
+
/**
|
|
58
|
+
* Одно событие за окно `windowMs` (умолч. 60 000) на ключ в процессе (§3.2, AC-2.6). Уже отправленная ошибка
|
|
59
|
+
* (например, границей через `captureError`) повторно не шлётся. Возвращает, отправлено ли событие.
|
|
60
|
+
*/
|
|
61
|
+
export declare function captureThrottled(key: string, error: unknown, options?: {
|
|
62
|
+
readonly windowMs?: number;
|
|
63
|
+
}): boolean;
|
|
64
|
+
export {};
|