@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.
Files changed (216) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/README.md +804 -2
  3. package/alerts/verdeect-kit.rules.yml +133 -0
  4. package/dist/cli/commands/crypto.d.ts +9 -0
  5. package/dist/cli/commands/crypto.js +87 -0
  6. package/dist/cli/commands/db.d.ts +11 -0
  7. package/dist/cli/commands/db.js +151 -0
  8. package/dist/cli/commands/openapi.d.ts +9 -0
  9. package/dist/cli/commands/openapi.js +144 -0
  10. package/dist/cli/commands/rabbitmq.d.ts +11 -0
  11. package/dist/cli/commands/rabbitmq.js +124 -0
  12. package/dist/cli/commands/redis.d.ts +11 -0
  13. package/dist/cli/commands/redis.js +119 -0
  14. package/dist/cli/main.d.ts +2 -0
  15. package/dist/cli/main.js +31 -0
  16. package/dist/config/cli-schemas.d.ts +29 -0
  17. package/dist/config/cli-schemas.js +15 -0
  18. package/dist/config/define-env.d.ts +21 -0
  19. package/dist/config/define-env.js +43 -0
  20. package/dist/config/fragments.d.ts +105 -0
  21. package/dist/config/fragments.js +86 -0
  22. package/dist/config/helpers.d.ts +26 -0
  23. package/dist/config/helpers.js +62 -0
  24. package/dist/config/index.d.ts +7 -0
  25. package/dist/config/index.js +7 -0
  26. package/dist/config/kit-config.module.d.ts +20 -0
  27. package/dist/config/kit-config.module.js +23 -0
  28. package/dist/config/parse-env.d.ts +14 -0
  29. package/dist/config/parse-env.js +28 -0
  30. package/dist/crypto/cipher.d.ts +61 -0
  31. package/dist/crypto/cipher.js +136 -0
  32. package/dist/crypto/index.d.ts +2 -0
  33. package/dist/crypto/index.js +2 -0
  34. package/dist/database/batches.d.ts +42 -0
  35. package/dist/database/batches.js +49 -0
  36. package/dist/database/db.d.ts +93 -0
  37. package/dist/database/db.js +234 -0
  38. package/dist/database/if-newer.d.ts +9 -0
  39. package/dist/database/if-newer.js +15 -0
  40. package/dist/database/index.d.ts +17 -0
  41. package/dist/database/index.js +13 -0
  42. package/dist/database/kit-database.module.d.ts +47 -0
  43. package/dist/database/kit-database.module.js +181 -0
  44. package/dist/database/migrations.d.ts +42 -0
  45. package/dist/database/migrations.js +142 -0
  46. package/dist/database/pg.d.ts +20 -0
  47. package/dist/database/pg.js +19 -0
  48. package/dist/database/prepare-database.d.ts +39 -0
  49. package/dist/database/prepare-database.js +75 -0
  50. package/dist/database/run-exclusive.d.ts +19 -0
  51. package/dist/database/run-exclusive.js +62 -0
  52. package/dist/database/tenant.d.ts +34 -0
  53. package/dist/database/tenant.js +28 -0
  54. package/dist/http/api/api.decorator.d.ts +18 -0
  55. package/dist/http/api/api.decorator.js +9 -0
  56. package/dist/http/api/openapi-document.d.ts +40 -0
  57. package/dist/http/api/openapi-document.js +452 -0
  58. package/dist/http/bootstrap-service.d.ts +33 -0
  59. package/dist/http/bootstrap-service.js +107 -0
  60. package/dist/http/create-http-app.d.ts +38 -0
  61. package/dist/http/create-http-app.js +137 -0
  62. package/dist/http/errors.d.ts +118 -0
  63. package/dist/http/errors.js +94 -0
  64. package/dist/http/index.d.ts +12 -0
  65. package/dist/http/index.js +8 -0
  66. package/dist/http/kit-exception.filter.d.ts +18 -0
  67. package/dist/http/kit-exception.filter.js +151 -0
  68. package/dist/http/kit-http-core.module.d.ts +19 -0
  69. package/dist/http/kit-http-core.module.js +122 -0
  70. package/dist/http/pagination.d.ts +30 -0
  71. package/dist/http/pagination.js +73 -0
  72. package/dist/http/zod-validation.pipe.d.ts +15 -0
  73. package/dist/http/zod-validation.pipe.js +47 -0
  74. package/dist/idempotency/idempotency.interceptor.d.ts +55 -0
  75. package/dist/idempotency/idempotency.interceptor.js +317 -0
  76. package/dist/idempotency/idempotent.decorator.d.ts +7 -0
  77. package/dist/idempotency/idempotent.decorator.js +16 -0
  78. package/dist/idempotency/index.d.ts +7 -0
  79. package/dist/idempotency/index.js +6 -0
  80. package/dist/idempotency/kit-idempotency.module.d.ts +47 -0
  81. package/dist/idempotency/kit-idempotency.module.js +173 -0
  82. package/dist/idempotency/peer.d.ts +1 -0
  83. package/dist/idempotency/peer.js +4 -0
  84. package/dist/idempotency/table.d.ts +203 -0
  85. package/dist/idempotency/table.js +86 -0
  86. package/dist/internal/cleanup-result.d.ts +7 -0
  87. package/dist/internal/cleanup-result.js +12 -0
  88. package/dist/internal/ids.d.ts +13 -0
  89. package/dist/internal/ids.js +41 -0
  90. package/dist/internal/metadata.d.ts +12 -0
  91. package/dist/internal/metadata.js +3 -0
  92. package/dist/internal/outbox-policy.d.ts +8 -0
  93. package/dist/internal/outbox-policy.js +10 -0
  94. package/dist/internal/peer.d.ts +16 -0
  95. package/dist/internal/peer.js +56 -0
  96. package/dist/internal/pg-errors.d.ts +21 -0
  97. package/dist/internal/pg-errors.js +115 -0
  98. package/dist/internal/preparsed-env.d.ts +4 -0
  99. package/dist/internal/preparsed-env.js +14 -0
  100. package/dist/internal/rabbitmq-definitions.d.ts +85 -0
  101. package/dist/internal/rabbitmq-definitions.js +73 -0
  102. package/dist/internal/redis-acl.d.ts +21 -0
  103. package/dist/internal/redis-acl.js +30 -0
  104. package/dist/internal/service-name.d.ts +4 -0
  105. package/dist/internal/service-name.js +10 -0
  106. package/dist/internal/sql-identifier.d.ts +5 -0
  107. package/dist/internal/sql-identifier.js +12 -0
  108. package/dist/internal/tokens.d.ts +10 -0
  109. package/dist/internal/tokens.js +12 -0
  110. package/dist/internal/tx-context.d.ts +5 -0
  111. package/dist/internal/tx-context.js +9 -0
  112. package/dist/messaging/cleanup.d.ts +49 -0
  113. package/dist/messaging/cleanup.js +129 -0
  114. package/dist/messaging/config.d.ts +43 -0
  115. package/dist/messaging/config.js +8 -0
  116. package/dist/messaging/define-event.d.ts +29 -0
  117. package/dist/messaging/define-event.js +38 -0
  118. package/dist/messaging/envelope.d.ts +66 -0
  119. package/dist/messaging/envelope.js +49 -0
  120. package/dist/messaging/errors.d.ts +45 -0
  121. package/dist/messaging/errors.js +63 -0
  122. package/dist/messaging/inbox.d.ts +4 -0
  123. package/dist/messaging/inbox.js +9 -0
  124. package/dist/messaging/index.d.ts +13 -0
  125. package/dist/messaging/index.js +10 -0
  126. package/dist/messaging/kit-messaging.module.d.ts +39 -0
  127. package/dist/messaging/kit-messaging.module.js +309 -0
  128. package/dist/messaging/outbox-relay.d.ts +67 -0
  129. package/dist/messaging/outbox-relay.js +260 -0
  130. package/dist/messaging/outbox-store.d.ts +75 -0
  131. package/dist/messaging/outbox-store.js +126 -0
  132. package/dist/messaging/peer.d.ts +1 -0
  133. package/dist/messaging/peer.js +5 -0
  134. package/dist/messaging/rabbit-connection.d.ts +144 -0
  135. package/dist/messaging/rabbit-connection.js +472 -0
  136. package/dist/messaging/redelivery.d.ts +6 -0
  137. package/dist/messaging/redelivery.js +17 -0
  138. package/dist/messaging/subscriber.d.ts +102 -0
  139. package/dist/messaging/subscriber.js +412 -0
  140. package/dist/messaging/tables.d.ts +315 -0
  141. package/dist/messaging/tables.js +72 -0
  142. package/dist/messaging/topology.d.ts +56 -0
  143. package/dist/messaging/topology.js +56 -0
  144. package/dist/observability/fail-startup.d.ts +14 -0
  145. package/dist/observability/fail-startup.js +51 -0
  146. package/dist/observability/health-registry.d.ts +34 -0
  147. package/dist/observability/health-registry.js +81 -0
  148. package/dist/observability/index.d.ts +16 -0
  149. package/dist/observability/index.js +10 -0
  150. package/dist/observability/lifecycle.d.ts +52 -0
  151. package/dist/observability/lifecycle.js +151 -0
  152. package/dist/observability/logger-options.d.ts +54 -0
  153. package/dist/observability/logger-options.js +91 -0
  154. package/dist/observability/metrics-server.d.ts +33 -0
  155. package/dist/observability/metrics-server.js +108 -0
  156. package/dist/observability/metrics.d.ts +30 -0
  157. package/dist/observability/metrics.js +53 -0
  158. package/dist/observability/redaction.d.ts +23 -0
  159. package/dist/observability/redaction.js +65 -0
  160. package/dist/observability/sanitize-error.d.ts +47 -0
  161. package/dist/observability/sanitize-error.js +194 -0
  162. package/dist/observability/sentry.d.ts +64 -0
  163. package/dist/observability/sentry.js +233 -0
  164. package/dist/redis/channels.d.ts +34 -0
  165. package/dist/redis/channels.js +128 -0
  166. package/dist/redis/client.d.ts +42 -0
  167. package/dist/redis/client.js +85 -0
  168. package/dist/redis/index.d.ts +7 -0
  169. package/dist/redis/index.js +7 -0
  170. package/dist/redis/kit-redis.module.d.ts +18 -0
  171. package/dist/redis/kit-redis.module.js +131 -0
  172. package/dist/redis/peer.d.ts +1 -0
  173. package/dist/redis/peer.js +4 -0
  174. package/dist/redis/rate-limiter.d.ts +49 -0
  175. package/dist/redis/rate-limiter.js +139 -0
  176. package/dist/testing/count-queries.d.ts +30 -0
  177. package/dist/testing/count-queries.js +58 -0
  178. package/dist/testing/create-test-app.d.ts +26 -0
  179. package/dist/testing/create-test-app.js +32 -0
  180. package/dist/testing/expect-matches-api.d.ts +8 -0
  181. package/dist/testing/expect-matches-api.js +49 -0
  182. package/dist/testing/expect-problem.d.ts +24 -0
  183. package/dist/testing/expect-problem.js +23 -0
  184. package/dist/testing/images.d.ts +10 -0
  185. package/dist/testing/images.js +14 -0
  186. package/dist/testing/index.d.ts +23 -0
  187. package/dist/testing/index.js +17 -0
  188. package/dist/testing/launch-command.d.ts +52 -0
  189. package/dist/testing/launch-command.js +510 -0
  190. package/dist/testing/outbox-coverage.d.ts +26 -0
  191. package/dist/testing/outbox-coverage.js +98 -0
  192. package/dist/testing/rabbitmq-admin.d.ts +81 -0
  193. package/dist/testing/rabbitmq-admin.js +146 -0
  194. package/dist/testing/redis-worker.d.ts +10 -0
  195. package/dist/testing/redis-worker.js +15 -0
  196. package/dist/testing/repo-conventions.d.ts +10 -0
  197. package/dist/testing/repo-conventions.js +524 -0
  198. package/dist/testing/repo-files.d.ts +113 -0
  199. package/dist/testing/repo-files.js +401 -0
  200. package/dist/testing/scratch-database.d.ts +19 -0
  201. package/dist/testing/scratch-database.js +75 -0
  202. package/dist/testing/start-test-stack.d.ts +85 -0
  203. package/dist/testing/start-test-stack.js +342 -0
  204. package/dist/testing/tcp-proxy.d.ts +40 -0
  205. package/dist/testing/tcp-proxy.js +164 -0
  206. package/dist/testing/tenant-isolation.d.ts +31 -0
  207. package/dist/testing/tenant-isolation.js +205 -0
  208. package/dist/testing/test-events.d.ts +37 -0
  209. package/dist/testing/test-events.js +150 -0
  210. package/dist/testing/timeout-scale.d.ts +4 -0
  211. package/dist/testing/timeout-scale.js +17 -0
  212. package/dist/testing/timing-guard.d.ts +22 -0
  213. package/dist/testing/timing-guard.js +54 -0
  214. package/dist/testing/worker-scope.d.ts +31 -0
  215. package/dist/testing/worker-scope.js +68 -0
  216. package/package.json +177 -4
@@ -0,0 +1,62 @@
1
+ // Помощники zod для схем окружения (prd.md §3.1); эталон — O env.schema.ts + port/httpUrl из B.
2
+ // Умолчание необязательно у всех помощников: без него ключ обязателен. Issue без входного значения (NFR-4).
3
+ import { z } from 'zod';
4
+ /** TCP-порт 0–65535; 0 — порт выбирает ОС (тесты). Пустая строка даёт 0, как в эталоне (§3.1). */
5
+ export function port(fallback) {
6
+ const schema = z.coerce.number().int().min(0).max(65535);
7
+ return fallback === undefined ? schema : schema.default(fallback);
8
+ }
9
+ /** Целое > 0 (§3.1). */
10
+ export function positiveInt(fallback) {
11
+ const schema = z.coerce.number().int().positive();
12
+ return fallback === undefined ? schema : schema.default(fallback);
13
+ }
14
+ /** Целое ≥ 0; внутренний помощник фрагментов, из /config не реэкспортируется. */
15
+ export function nonNegativeInt(fallback) {
16
+ const schema = z.coerce.number().int().nonnegative();
17
+ return fallback === undefined ? schema : schema.default(fallback);
18
+ }
19
+ /** Необязательный текст: пусто или пробелы → null, иначе значение без крайних пробелов (§3.1). */
20
+ export function optionalText() {
21
+ return z
22
+ .string()
23
+ .default('')
24
+ .transform((value) => (value.trim() === '' ? null : value.trim()));
25
+ }
26
+ /** Только строки `true`/`false`: приведение zod к boolean сделало бы строку `false` истиной (§3.1). */
27
+ export function booleanFlag(fallback) {
28
+ const schema = z.enum(['true', 'false']);
29
+ const withDefault = fallback === undefined ? schema : schema.default(fallback ? 'true' : 'false');
30
+ return withDefault.transform((value) => value === 'true');
31
+ }
32
+ /**
33
+ * Ключ с JSON-значением (§3.1). Умолчание — строка до разбора: в zod 4 умолчание после
34
+ * преобразования ожидает уже разобранный тип. Ошибка разбора — без входного значения.
35
+ */
36
+ export function jsonValue(schema, fallback) {
37
+ const raw = fallback === undefined ? z.string() : z.string().default(fallback);
38
+ return raw
39
+ .transform((value, ctx) => {
40
+ try {
41
+ return JSON.parse(value);
42
+ }
43
+ catch {
44
+ ctx.addIssue({ code: 'custom', message: 'неверный JSON', input: '' });
45
+ return z.NEVER;
46
+ }
47
+ })
48
+ .pipe(schema);
49
+ }
50
+ /** Адрес HTTP-ресурса: только http и https (§3.1). */
51
+ export function httpUrl() {
52
+ return z.url({ protocol: /^https?$/ });
53
+ }
54
+ /** Значения поля `key` элементов массива не повторяются; для `superRefine`, issue — без значений (§3.1). */
55
+ export function uniqueBy(key) {
56
+ return (items, ctx) => {
57
+ const seen = new Set(items.map((item) => item[key]));
58
+ if (seen.size !== items.length) {
59
+ ctx.addIssue({ code: 'custom', message: `повтор ${String(key)}`, input: '' });
60
+ }
61
+ };
62
+ }
@@ -0,0 +1,7 @@
1
+ export { migrateEnvSchema, prepareEnvSchema } from './cli-schemas.js';
2
+ export { defineEnv, withBaseChecks } from './define-env.js';
3
+ export { envBase, envCrypto, envDatabase, envMessaging, envRedis, envSentry, envServiceUrls, } from './fragments.js';
4
+ export type { KitBaseEnv, KitCryptoEnv, KitDatabaseEnv, KitMessagingEnv, KitRedisEnv, } from './fragments.js';
5
+ export { booleanFlag, httpUrl, jsonValue, optionalText, port, positiveInt, uniqueBy, } from './helpers.js';
6
+ export { KitConfigModule } from './kit-config.module.js';
7
+ export { InvalidEnvironmentError, parseEnv } from './parse-env.js';
@@ -0,0 +1,7 @@
1
+ // Точка входа @verdeect/kit/config (prd.md §3.1, E1): явный список экспортов.
2
+ export { migrateEnvSchema, prepareEnvSchema } from './cli-schemas.js';
3
+ export { defineEnv, withBaseChecks } from './define-env.js';
4
+ export { envBase, envCrypto, envDatabase, envMessaging, envRedis, envSentry, envServiceUrls, } from './fragments.js';
5
+ export { booleanFlag, httpUrl, jsonValue, optionalText, port, positiveInt, uniqueBy, } from './helpers.js';
6
+ export { KitConfigModule } from './kit-config.module.js';
7
+ export { InvalidEnvironmentError, parseEnv } from './parse-env.js';
@@ -0,0 +1,20 @@
1
+ import type { DynamicModule } from '@nestjs/common';
2
+ import type { z } from 'zod';
3
+ /** Схема окружения: результат — объект ключей (его ожидает validate у ConfigModule). */
4
+ type EnvSchema = z.ZodType<Record<string, unknown>>;
5
+ /** Параметры KitConfigModule.forRoot. */
6
+ export interface KitConfigModuleOptions<S extends EnvSchema> {
7
+ /** Схема окружения сервиса (обычно результат defineEnv). */
8
+ readonly schema: S;
9
+ }
10
+ /** Глобальный модуль окружения; разбирает окружение при каждой сборке контекста Nest (AC-1.7). */
11
+ export declare class KitConfigModule {
12
+ /**
13
+ * Глобальный ConfigService с разобранным окружением. При запуске через `bootstrapService` окружение не
14
+ * разбирается повторно; иначе — `parseEnv(schema, process.env)` при сборке контекста, ошибка —
15
+ * `InvalidEnvironmentError`, сборка контекста отклоняется (сам `forRoot` не отклоняется). Файл `.env` не
16
+ * читается, в `process.env` ничего не пишется, ConfigService читает только разобранные значения.
17
+ */
18
+ static forRoot<S extends EnvSchema>(options: KitConfigModuleOptions<S>): Promise<DynamicModule>;
19
+ }
20
+ export {};
@@ -0,0 +1,23 @@
1
+ import { ConfigModule } from '@nestjs/config';
2
+ import { takePreparsedEnv } from '../internal/preparsed-env.js';
3
+ import { parseEnv } from './parse-env.js';
4
+ /** Глобальный модуль окружения; разбирает окружение при каждой сборке контекста Nest (AC-1.7). */
5
+ export class KitConfigModule {
6
+ /**
7
+ * Глобальный ConfigService с разобранным окружением. При запуске через `bootstrapService` окружение не
8
+ * разбирается повторно; иначе — `parseEnv(schema, process.env)` при сборке контекста, ошибка —
9
+ * `InvalidEnvironmentError`, сборка контекста отклоняется (сам `forRoot` не отклоняется). Файл `.env` не
10
+ * читается, в `process.env` ничего не пишется, ConfigService читает только разобранные значения.
11
+ */
12
+ static forRoot(options) {
13
+ const { schema } = options;
14
+ return ConfigModule.forRoot({
15
+ isGlobal: true,
16
+ ignoreEnvFile: true,
17
+ // Без validate @nestjs/config копирует process.env сам в себя; false — копии нет совсем.
18
+ validatePredefined: false,
19
+ skipProcessEnv: true,
20
+ load: [() => takePreparsedEnv(schema) ?? parseEnv(schema, process.env)],
21
+ });
22
+ }
23
+ }
@@ -0,0 +1,14 @@
1
+ import type { z } from 'zod';
2
+ /** Ошибка окружения: только пути ключей, без значений (NFR-4). */
3
+ export declare class InvalidEnvironmentError extends Error {
4
+ /** Код ошибки конфигурации. */
5
+ readonly code: "INVALID_ENVIRONMENT";
6
+ /** Полные пути ключей через точку, без дублей; без пути — `(root)`. */
7
+ readonly keys: readonly string[];
8
+ constructor(keys: readonly string[]);
9
+ }
10
+ /**
11
+ * Результат схемы или InvalidEnvironmentError (§3.1). Ошибка zod не сохраняется ни в `cause`, ни в
12
+ * полях: её issues содержат входные значения.
13
+ */
14
+ export declare function parseEnv<S extends z.ZodType>(schema: S, env: Readonly<Record<string, unknown>>): z.output<S>;
@@ -0,0 +1,28 @@
1
+ /** Ошибка окружения: только пути ключей, без значений (NFR-4). */
2
+ export class InvalidEnvironmentError extends Error {
3
+ /** Код ошибки конфигурации. */
4
+ code = 'INVALID_ENVIRONMENT';
5
+ /** Полные пути ключей через точку, без дублей; без пути — `(root)`. */
6
+ keys;
7
+ constructor(keys) {
8
+ super(`Неверное окружение: ${keys.join(', ')}`);
9
+ this.name = 'InvalidEnvironmentError';
10
+ this.keys = keys;
11
+ }
12
+ }
13
+ /** Пути ключей issues без дублей, в порядке issues; значения и сообщения zod не берутся. */
14
+ function issueKeys(error) {
15
+ const keys = error.issues.map((issue) => issue.path.length === 0 ? '(root)' : issue.path.map(String).join('.'));
16
+ return [...new Set(keys)];
17
+ }
18
+ /**
19
+ * Результат схемы или InvalidEnvironmentError (§3.1). Ошибка zod не сохраняется ни в `cause`, ни в
20
+ * полях: её issues содержат входные значения.
21
+ */
22
+ export function parseEnv(schema, env) {
23
+ const result = schema.safeParse(env);
24
+ if (!result.success) {
25
+ throw new InvalidEnvironmentError(issueKeys(result.error));
26
+ }
27
+ return result.data;
28
+ }
@@ -0,0 +1,61 @@
1
+ /** Причина отказа `decrypt`: формат значения, версия ключа не в `keys`, проверка подлинности GCM. */
2
+ export type DecryptErrorReason = 'format' | 'unknown_version' | 'auth';
3
+ /** Опции `createCipher` (§3.6). */
4
+ export interface KitCipherOptions {
5
+ /** Имя сервиса `^[a-z][a-z0-9-]{1,31}$`; входит в `info` HKDF. */
6
+ readonly service: string;
7
+ /** Ключи `[{version, secret}]` (как `ENCRYPTION_KEYS`); первый шифрует, остальные только читаются. */
8
+ readonly keys: readonly {
9
+ /** Версия ключа, целое ≥ 1; попадает в префикс значения `v<N>`. */
10
+ readonly version: number;
11
+ /** Секрет base64url без `=`, не короче 32 байт. */
12
+ readonly secret: string;
13
+ }[];
14
+ /** Назначение `^[a-z][a-z0-9-]{1,31}$`; разные назначения — разные ключи. */
15
+ readonly purpose: string;
16
+ }
17
+ /** Шифр одного назначения (§3.6). */
18
+ export interface KitCipher {
19
+ /** Шифрует текст ключом `keys[0]`; результат — `v<N>:<iv>:<ct>:<tag>` (base64url). `aad` обязателен. */
20
+ encrypt(plaintext: string, opts: {
21
+ /** Привязка значения к записи, обычно `<purpose>:<id записи>`; непустая. */
22
+ readonly aad: string;
23
+ }): string;
24
+ /**
25
+ * Расшифровывает значение ключом его версии.
26
+ * @throws {DecryptError} `'format'`, `'unknown_version'` или `'auth'`.
27
+ */
28
+ decrypt(value: string, opts: {
29
+ /** Тот же `aad`, что при шифровании. */
30
+ readonly aad: string;
31
+ }): string;
32
+ /**
33
+ * `true`, если версия значения не совпадает с версией `keys[0]`.
34
+ * @throws {DecryptError} `'format'` — значение вне формата.
35
+ */
36
+ needsReencrypt(value: string): boolean;
37
+ }
38
+ /** Отказ расшифровки; текст и поля без открытого текста, `aad`, значения и ключей (NFR-4). */
39
+ export declare class DecryptError extends Error {
40
+ /** Причина отказа. */
41
+ readonly reason: DecryptErrorReason;
42
+ /** @param reason причина отказа. */
43
+ constructor(reason: DecryptErrorReason);
44
+ }
45
+ /**
46
+ * Создаёт шифр AES-256-GCM для назначения `purpose` сервиса `service`. Ключ каждой версии — HKDF-SHA256 от
47
+ * декодированных байт секрета, соль пустая, `info = verdeect-<service>:<purpose>`, 32 байта; выводится один раз.
48
+ * `aad` значения — `<purpose>:<id записи>`: значение одной записи не расшифровывается в другой.
49
+ *
50
+ * Смена ключа (UC-10): 1) `verdeect-kit crypto generate-secret`; 2) новая версия первой в `ENCRYPTION_KEYS`, старая
51
+ * остаётся; 3) перезапуск всех реплик; 4) задача под `runExclusive` перешифровывает значения с
52
+ * `needsReencrypt(value) === true`; 5) старая версия убирается, когда значений под ней не осталось.
53
+ * @throws {TypeError} `service`, `purpose` вне формата или `keys` не по схеме `ENCRYPTION_KEYS` (пути без значений).
54
+ */
55
+ export declare function createCipher(options: KitCipherOptions): KitCipher;
56
+ /**
57
+ * Новый секрет для `ENCRYPTION_KEYS`: `bytes` случайных байт в base64url без `=`.
58
+ * @param bytes длина в байтах, целое ≥ 32 (по умолчанию 32).
59
+ * @throws {RangeError} `bytes` не целое или меньше 32.
60
+ */
61
+ export declare function generateSecret(bytes?: number): string;
@@ -0,0 +1,136 @@
1
+ // Шифр секретов /crypto (prd.md §3.6, UC-10, AC-7.1–AC-7.5). Эталон — B identity-client session-cipher.ts (версия ключа
2
+ // v<N>, AAD, версия из значения без перебора) + HKDF-SHA256 и строгие длины IV/тега из O private-key-cipher.ts
3
+ // (OR-102 п. 7). Старые форматы не читаются (P0, N9).
4
+ import { createCipheriv, createDecipheriv, hkdfSync, randomBytes } from 'node:crypto';
5
+ import { encryptionKeysSchema } from '../config/fragments.js';
6
+ import { assertServiceName } from '../internal/service-name.js';
7
+ const ALGORITHM = 'aes-256-gcm';
8
+ const IV_BYTES = 12;
9
+ const TAG_BYTES = 16;
10
+ const KEY_BYTES = 32;
11
+ const MIN_SECRET_BYTES = 32;
12
+ const PURPOSE_PATTERN = /^[a-z][a-z0-9-]{1,31}$/;
13
+ const VALUE_PATTERN = /^v([1-9]\d*):([A-Za-z0-9_-]+):([A-Za-z0-9_-]*):([A-Za-z0-9_-]+)$/;
14
+ const DECRYPT_MESSAGES = {
15
+ format: 'значение шифра в неверном формате',
16
+ unknown_version: 'версии ключа значения нет в ENCRYPTION_KEYS',
17
+ auth: 'значение шифра не прошло проверку подлинности',
18
+ };
19
+ /** Отказ расшифровки; текст и поля без открытого текста, `aad`, значения и ключей (NFR-4). */
20
+ export class DecryptError extends Error {
21
+ /** Причина отказа. */
22
+ reason;
23
+ /** @param reason причина отказа. */
24
+ constructor(reason) {
25
+ super(DECRYPT_MESSAGES[reason]);
26
+ this.name = 'DecryptError';
27
+ this.reason = reason;
28
+ }
29
+ }
30
+ /** Строгое декодирование части: только каноничный base64url (лишний символ или хвостовые биты — 'format'; KT-26 п. 2). */
31
+ function decodePart(part) {
32
+ const bytes = Buffer.from(part, 'base64url');
33
+ if (bytes.toString('base64url') !== part) {
34
+ throw new DecryptError('format');
35
+ }
36
+ return bytes;
37
+ }
38
+ function parse(value) {
39
+ const match = VALUE_PATTERN.exec(value);
40
+ if (match === null) {
41
+ throw new DecryptError('format');
42
+ }
43
+ const [, version = '', iv = '', ct = '', tag = ''] = match;
44
+ const parsed = {
45
+ version: Number(version),
46
+ iv: decodePart(iv),
47
+ ct: decodePart(ct),
48
+ tag: decodePart(tag),
49
+ };
50
+ if (parsed.iv.length !== IV_BYTES || parsed.tag.length !== TAG_BYTES) {
51
+ throw new DecryptError('format');
52
+ }
53
+ return parsed;
54
+ }
55
+ function assertAad(aad, where) {
56
+ if (typeof aad !== 'string' || aad.length === 0) {
57
+ throw new TypeError(`${where}: aad обязателен — непустая строка`);
58
+ }
59
+ }
60
+ /**
61
+ * Создаёт шифр AES-256-GCM для назначения `purpose` сервиса `service`. Ключ каждой версии — HKDF-SHA256 от
62
+ * декодированных байт секрета, соль пустая, `info = verdeect-<service>:<purpose>`, 32 байта; выводится один раз.
63
+ * `aad` значения — `<purpose>:<id записи>`: значение одной записи не расшифровывается в другой.
64
+ *
65
+ * Смена ключа (UC-10): 1) `verdeect-kit crypto generate-secret`; 2) новая версия первой в `ENCRYPTION_KEYS`, старая
66
+ * остаётся; 3) перезапуск всех реплик; 4) задача под `runExclusive` перешифровывает значения с
67
+ * `needsReencrypt(value) === true`; 5) старая версия убирается, когда значений под ней не осталось.
68
+ * @throws {TypeError} `service`, `purpose` вне формата или `keys` не по схеме `ENCRYPTION_KEYS` (пути без значений).
69
+ */
70
+ export function createCipher(options) {
71
+ const { service, purpose } = options;
72
+ assertServiceName(service, 'createCipher');
73
+ if (!PURPOSE_PATTERN.test(purpose)) {
74
+ throw new TypeError(`createCipher: purpose «${purpose}» вне формата ^[a-z][a-z0-9-]{1,31}$`);
75
+ }
76
+ const checked = encryptionKeysSchema.safeParse(options.keys);
77
+ if (!checked.success) {
78
+ const paths = checked.error.issues.map((issue) => ['keys', ...issue.path.map(String)].join('.'));
79
+ throw new TypeError(`createCipher: неверные keys: ${[...new Set(paths)].join(', ')}`);
80
+ }
81
+ const info = `verdeect-${service}:${purpose}`;
82
+ const keys = new Map(checked.data.map(({ version, secret }) => [
83
+ version,
84
+ Buffer.from(hkdfSync('sha256', Buffer.from(secret, 'base64url'), Buffer.alloc(0), info, KEY_BYTES)),
85
+ ]));
86
+ const [current] = checked.data;
87
+ const currentKey = current === undefined ? undefined : keys.get(current.version);
88
+ if (current === undefined || currentKey === undefined) {
89
+ // Недостижимо: схема требует min(1); проверка — для noUncheckedIndexedAccess.
90
+ throw new TypeError('createCipher: неверные keys: keys');
91
+ }
92
+ return {
93
+ encrypt(plaintext, { aad }) {
94
+ assertAad(aad, 'encrypt');
95
+ const iv = randomBytes(IV_BYTES);
96
+ const cipher = createCipheriv(ALGORITHM, currentKey, iv, { authTagLength: TAG_BYTES });
97
+ cipher.setAAD(Buffer.from(aad, 'utf8'));
98
+ const ct = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
99
+ const tag = cipher.getAuthTag();
100
+ return `v${current.version}:${iv.toString('base64url')}:${ct.toString('base64url')}:${tag.toString('base64url')}`;
101
+ },
102
+ decrypt(value, { aad }) {
103
+ assertAad(aad, 'decrypt');
104
+ const parsed = parse(value);
105
+ const key = keys.get(parsed.version);
106
+ if (key === undefined) {
107
+ throw new DecryptError('unknown_version');
108
+ }
109
+ const decipher = createDecipheriv(ALGORITHM, key, parsed.iv, {
110
+ authTagLength: TAG_BYTES,
111
+ });
112
+ decipher.setAAD(Buffer.from(aad, 'utf8'));
113
+ decipher.setAuthTag(parsed.tag);
114
+ try {
115
+ return Buffer.concat([decipher.update(parsed.ct), decipher.final()]).toString('utf8');
116
+ }
117
+ catch {
118
+ throw new DecryptError('auth');
119
+ }
120
+ },
121
+ needsReencrypt(value) {
122
+ return parse(value).version !== current.version;
123
+ },
124
+ };
125
+ }
126
+ /**
127
+ * Новый секрет для `ENCRYPTION_KEYS`: `bytes` случайных байт в base64url без `=`.
128
+ * @param bytes длина в байтах, целое ≥ 32 (по умолчанию 32).
129
+ * @throws {RangeError} `bytes` не целое или меньше 32.
130
+ */
131
+ export function generateSecret(bytes = MIN_SECRET_BYTES) {
132
+ if (!Number.isInteger(bytes) || bytes < MIN_SECRET_BYTES) {
133
+ throw new RangeError('generateSecret: bytes — целое не меньше 32');
134
+ }
135
+ return randomBytes(bytes).toString('base64url');
136
+ }
@@ -0,0 +1,2 @@
1
+ export { createCipher, DecryptError, generateSecret } from './cipher.js';
2
+ export type { DecryptErrorReason, KitCipher, KitCipherOptions } from './cipher.js';
@@ -0,0 +1,2 @@
1
+ // Точка входа @verdeect/kit/crypto (prd.md §3.6, E7): явный список экспортов; только node:crypto, zod и config/fragments.
2
+ export { createCipher, DecryptError, generateSecret } from './cipher.js';
@@ -0,0 +1,42 @@
1
+ import type { SQL } from 'drizzle-orm';
2
+ import { type Db } from './db.js';
3
+ import type { PoolClient } from './pg.js';
4
+ /** Опции `deleteInBatches`. */
5
+ export interface KitDeleteInBatchesOptions {
6
+ /** `Db` сервиса: пачки — его транзакции, остановка `Db` прекращает новые пачки. */
7
+ readonly db: Db;
8
+ /**
9
+ * SQL пачки по её размеру; результат — одна строка с целой колонкой `deleted`, например
10
+ * `WITH d AS (DELETE … LIMIT n RETURNING 1) SELECT count(*)::int AS deleted FROM d` или вызов функции
11
+ * `SECURITY DEFINER` (строгий RLS скрывает строки компаний от системного контекста).
12
+ */
13
+ readonly sql: (batchSize: number) => SQL;
14
+ /** Размер пачки, целое ≥ 1. */
15
+ readonly batchSize: number;
16
+ /** Предел пачек за проход, целое ≥ 1. */
17
+ readonly maxBatches: number;
18
+ /** `false` — следующая пачка не начинается, текущая дорабатывает. */
19
+ readonly shouldContinue?: () => boolean;
20
+ /**
21
+ * Соединение для пачек — обычно соединение блокировки из `runExclusive(db, key, (connection) => …)`: второе
22
+ * соединение пула не берётся. Без него — транзакция `Db` на соединении основного пула. Транзакции `Db`
23
+ * (`db.transaction`, `db.read`) внутри `fn` `runExclusive` идут на других соединениях пула: на соединении
24
+ * блокировки работают только пачки этого вызова.
25
+ */
26
+ readonly connection?: PoolClient;
27
+ }
28
+ /** Итог прохода `deleteInBatches`. */
29
+ export interface KitDeleteInBatchesResult {
30
+ /** Удалено строк. */
31
+ readonly deleted: number;
32
+ /** Выполнено пачек. */
33
+ readonly batches: number;
34
+ /** `true` — проход закончился неполной пачкой; `false` — остановлен maxBatches, shouldContinue или остановкой Db. */
35
+ readonly complete: boolean;
36
+ }
37
+ /**
38
+ * Удаление пачками: транзакция на пачку, до неполной пачки или `maxBatches` (AC-4.11). Остановка `Db` (шаг 3) ждёт
39
+ * текущую пачку, новая не начинается.
40
+ * @throws {RangeError} `batchSize` или `maxBatches` не целое ≥ 1.
41
+ */
42
+ export declare function deleteInBatches(options: KitDeleteInBatchesOptions): Promise<KitDeleteInBatchesResult>;
@@ -0,0 +1,49 @@
1
+ // deleteInBatches (prd.md §3.3, AC-4.11, UC-8, UC-9). Эталон — O batches.ts (выход при неполной пачке, maxBatches,
2
+ // shouldContinue); I — отслеживание идущих пачек в Db. По PRD: пачка — Db.transaction с системным контекстом, SQL
3
+ // возвращает одну строку с колонкой deleted.
4
+ import { Logger } from '@nestjs/common';
5
+ import { ulid } from '../internal/ids.js';
6
+ import { isStopping, trackBatchRun, transactionOn } from './db.js';
7
+ const logger = new Logger('deleteInBatches');
8
+ function assertPositiveInt(value, name) {
9
+ if (!Number.isInteger(value) || value < 1) {
10
+ throw new RangeError(`deleteInBatches: ${name} — целое ≥ 1`);
11
+ }
12
+ }
13
+ function deletedOf(rows) {
14
+ const value = rows[0]?.deleted;
15
+ return typeof value === 'number' || typeof value === 'bigint' || typeof value === 'string'
16
+ ? Number(value)
17
+ : 0;
18
+ }
19
+ async function runBatches(options) {
20
+ const ctx = { tenant: null, traceId: ulid(), actor: { channel: 'system' } };
21
+ let deleted = 0;
22
+ let batches = 0;
23
+ while (batches < options.maxBatches &&
24
+ !isStopping(options.db) &&
25
+ (options.shouldContinue?.() ?? true)) {
26
+ const batch = (tx) => tx.execute(options.sql(options.batchSize));
27
+ const result = options.connection === undefined
28
+ ? await options.db.transaction(ctx, batch)
29
+ : await transactionOn(options.db, options.connection, ctx, batch);
30
+ const count = deletedOf(result.rows);
31
+ batches += 1;
32
+ deleted += count;
33
+ logger.debug({ batch: batches, deleted: count }, 'deleteInBatches: пачка');
34
+ if (count < options.batchSize) {
35
+ return { deleted, batches, complete: true };
36
+ }
37
+ }
38
+ return { deleted, batches, complete: false };
39
+ }
40
+ /**
41
+ * Удаление пачками: транзакция на пачку, до неполной пачки или `maxBatches` (AC-4.11). Остановка `Db` (шаг 3) ждёт
42
+ * текущую пачку, новая не начинается.
43
+ * @throws {RangeError} `batchSize` или `maxBatches` не целое ≥ 1.
44
+ */
45
+ export function deleteInBatches(options) {
46
+ assertPositiveInt(options.batchSize, 'batchSize');
47
+ assertPositiveInt(options.maxBatches, 'maxBatches');
48
+ return trackBatchRun(options.db, runBatches(options));
49
+ }
@@ -0,0 +1,93 @@
1
+ import type { DrizzleDriver, NodePgDatabase, Pool, PoolClient } from './pg.js';
2
+ import { type DbContext } from './tenant.js';
3
+ /** Чтение через пул чтения (`readGlobal`): Drizzle node-postgres без транзакции. */
4
+ export type DbReader = NodePgDatabase;
5
+ /** Транзакция Drizzle node-postgres (`Db.transaction`, `Db.read`). */
6
+ export type DbTx = Parameters<Parameters<NodePgDatabase['transaction']>[0]>[0];
7
+ /** Задача после `COMMIT` (`Db.afterCommit`). */
8
+ export type AfterCommitTask = () => void | Promise<void>;
9
+ /** `Db.read` без тенанта: чтение без компании — только `Db.readGlobal` (строгий RLS, P0). */
10
+ export declare class TenantRequiredError extends Error {
11
+ readonly name = "TenantRequiredError";
12
+ constructor();
13
+ }
14
+ /** `Db.afterCommit` в read-only транзакции или на `readGlobal`: задачи после `COMMIT` там невозможны. */
15
+ export declare class AfterCommitInReadOnlyError extends Error {
16
+ readonly name = "AfterCommitInReadOnlyError";
17
+ constructor();
18
+ }
19
+ /**
20
+ * `Db.afterCommit` с дескриптором, который не является транзакцией этого `Db` (другой `Db`, `DRIZZLE`, вложенная
21
+ * `tx.transaction()`): задача никогда бы не выполнилась — ошибка вместо молчаливой потери.
22
+ */
23
+ export declare class AfterCommitOutsideTransactionError extends Error {
24
+ readonly name = "AfterCommitOutsideTransactionError";
25
+ constructor();
26
+ }
27
+ /** Пулы и драйвер `Db`. */
28
+ export interface DbOptions {
29
+ /** Основной пул (`transaction`, `runExclusive`). */
30
+ readonly main: Pool;
31
+ /** Пул чтения: `default_transaction_read_only`, `statement_timeout` 5 с (`read`, `readGlobal`). */
32
+ readonly read: Pool;
33
+ /** `drizzle()` из `loadDrizzle()`. */
34
+ readonly drizzle: DrizzleDriver['drizzle'];
35
+ }
36
+ /** Основной пул `Db` (для `runExclusive`). */
37
+ export declare function mainPoolOf(db: Db): Pool;
38
+ /**
39
+ * Транзакция `Db` (тенант, `afterCommit`) на уже выданном клиенте — например, на соединении блокировки `runExclusive`,
40
+ * без второго соединения пула (KT-22 п. 4). Клиент не возвращается в пул: им владеет вызывающий.
41
+ */
42
+ export declare function transactionOn<T>(db: Db, client: PoolClient, ctx: DbContext, fn: (tx: DbTx) => Promise<T>): Promise<T>;
43
+ /** Идёт остановка `Db` (шаг 3): новые пачки не начинаются. */
44
+ export declare function isStopping(db: Db): boolean;
45
+ /** Регистрирует проход пачек: `beforeApplicationShutdown` дожидается его. */
46
+ export declare function trackBatchRun<T>(db: Db, run: Promise<T>): Promise<T>;
47
+ /**
48
+ * Выделенный клиент пула на одну единицу работы; возвращается в пул всегда. Обрыв (событие `error` клиента или
49
+ * ошибка соединения из `work`) — `release(err)`: pg-pool уничтожает клиента, пул не исчерпывается (AC-4.4). drizzle-orm
50
+ * 0.45.4 выполняет BEGIN вне try/finally своей transaction() — поэтому клиент берётся здесь, а не Drizzle над пулом.
51
+ */
52
+ export declare function withPoolClient<T>(pool: Pool, work: (client: PoolClient) => Promise<T>, onBroken?: (error: Error) => void): Promise<T>;
53
+ /**
54
+ * Работа сервиса с БД под строгим RLS (§3.3): один `set_config('app.tenant', …, true)` на транзакцию, чтение без
55
+ * тенанта — только `readGlobal`, задачи после `COMMIT`, своя переменная `app.*`. Собирается `KitDatabaseModule`.
56
+ */
57
+ export declare class Db {
58
+ private readonly logger;
59
+ private readonly afterCommitTasks;
60
+ private readonly transactions;
61
+ private readonly readOnly;
62
+ private readonly drizzle;
63
+ private readonly readPool;
64
+ private readonly reader;
65
+ /** @param options пулы (с учётом срока — KitDatabaseModule) и драйвер Drizzle. */
66
+ constructor(options: DbOptions);
67
+ /** Транзакция чтения и записи на основном пуле с тенантом `ctx.tenant` (`null` — строки компаний не видны). */
68
+ transaction<T>(ctx: DbContext, fn: (tx: DbTx) => Promise<T>): Promise<T>;
69
+ /**
70
+ * Read-only транзакция на пуле чтения (`statement_timeout` 5 с) с тенантом.
71
+ * @throws {TenantRequiredError} в `ctx` нет тенанта (до подключения).
72
+ */
73
+ read<T>(ctx: DbContext, fn: (tx: DbTx) => Promise<T>): Promise<T>;
74
+ /** Чтение без компании (глобальные таблицы): пул чтения без транзакции и без `set_config`. */
75
+ readGlobal<T>(fn: (db: DbReader) => Promise<T>): Promise<T>;
76
+ /**
77
+ * Задача после `COMMIT` транзакции `tx` — по порядку, до возврата результата `transaction`; при откате не
78
+ * выполняется; ошибка задачи — `error`-лог и GlitchTip, не пробрасывается. Только для того, что можно потерять.
79
+ * @throws {AfterCommitInReadOnlyError} `tx` — транзакция `read` или дескриптор `readGlobal`.
80
+ * @throws {AfterCommitOutsideTransactionError} `tx` — не транзакция этого `Db` (другой `Db`, вложенная).
81
+ */
82
+ afterCommit(tx: DbTx | DbReader, task: AfterCommitTask): void;
83
+ /**
84
+ * Переменная транзакции `app.*` (`set_config(name, value, true)`), имя и значение — параметрами (AC-4.16).
85
+ * Служебная `app.tenant` запрещена: тенант задаётся только `DbContext` в `transaction`/`read`.
86
+ * @throws {TypeError} имя вне `^app\.[a-z_]{1,48}$`, служебное имя пакета или `tx` — не транзакция `Db`.
87
+ */
88
+ setLocal(tx: DbTx | DbReader, name: string, value: string): Promise<void>;
89
+ /** Шаг 3 остановки: новые пачки `deleteInBatches` не начинаются, идущие дорабатывают. */
90
+ beforeApplicationShutdown(): Promise<void>;
91
+ private run;
92
+ private runAfterCommit;
93
+ }