@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.
Files changed (216) hide show
  1. package/CHANGELOG.md +138 -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,124 @@
1
+ // Команда verdeect-kit rabbitmq definitions (prd.md §3.4 M3, UC-12, AC-5.10): definitions.json для брокера — vhost,
2
+ // пользователи сервисов с хешами паролей из RABBITMQ_PASSWORD_HASH_<SVC> (П3), права, exchange verdeect.events. Образец —
3
+ // commands/db.ts. Без amqplib, pg и messaging: только internal/rabbitmq-definitions динамическим import().
4
+ import { mkdir, writeFile } from 'node:fs/promises';
5
+ import { dirname, resolve } from 'node:path';
6
+ import { parseArgs } from 'node:util';
7
+ import { pino } from 'pino';
8
+ import { z } from 'zod';
9
+ const defaultContext = {
10
+ cwd: process.cwd(),
11
+ write: (text) => void process.stdout.write(text),
12
+ env: process.env,
13
+ };
14
+ /** base64 соли (4 байта) и sha256 (32 байта) — не короче 44 знаков. */
15
+ const HASH_FORMAT = /^[A-Za-z0-9+/]+={0,2}$/;
16
+ const MIN_HASH_LENGTH = 44;
17
+ /** Ошибка команды: JSON в stdout и код 1. */
18
+ class CommandError extends Error {
19
+ code;
20
+ extra;
21
+ constructor(code, message, extra = {}) {
22
+ super(message);
23
+ this.code = code;
24
+ this.extra = extra;
25
+ }
26
+ }
27
+ async function createLogger(env) {
28
+ const [{ envBase }, { createLoggerOptions }] = await Promise.all([
29
+ import('../../config/fragments.js'),
30
+ import('../../observability/logger-options.js'),
31
+ ]);
32
+ const parsed = z
33
+ .object({ LOG_LEVEL: envBase.LOG_LEVEL, REPLICA_ID: envBase.REPLICA_ID })
34
+ .safeParse(env);
35
+ if (!parsed.success) {
36
+ const keys = [...new Set(parsed.error.issues.map((issue) => issue.path.join('.')))];
37
+ throw new CommandError('INVALID_ENVIRONMENT', 'Неверное окружение', { keys });
38
+ }
39
+ return pino(...createLoggerOptions({ service: 'verdeect-kit', entry: 'cli', env: parsed.data })
40
+ .pinoHttp);
41
+ }
42
+ function parseOptions(args) {
43
+ try {
44
+ return parseArgs({
45
+ args: [...args],
46
+ strict: true,
47
+ options: { services: { type: 'string' }, out: { type: 'string' } },
48
+ }).values;
49
+ }
50
+ catch (error) {
51
+ throw new CommandError('USAGE', error instanceof Error ? error.message : String(error));
52
+ }
53
+ }
54
+ async function servicesOf(raw) {
55
+ const services = (raw ?? '')
56
+ .split(',')
57
+ .map((name) => name.trim())
58
+ .filter((name) => name !== '');
59
+ if (services.length === 0) {
60
+ throw new CommandError('USAGE', 'нужна опция --services <svc,…>');
61
+ }
62
+ const { SERVICE_NAME_PATTERN } = await import('../../internal/service-name.js');
63
+ const bad = services.find((name) => !SERVICE_NAME_PATTERN.test(name));
64
+ if (bad !== undefined) {
65
+ throw new CommandError('USAGE', `--services: имя «${bad}» вне формата ^[a-z][a-z0-9-]{1,31}$`);
66
+ }
67
+ if (new Set(services).size !== services.length) {
68
+ throw new CommandError('USAGE', '--services: имя указано дважды');
69
+ }
70
+ return services;
71
+ }
72
+ async function definitions(args, context, logger) {
73
+ const values = parseOptions(args);
74
+ const services = await servicesOf(values.services);
75
+ const { buildDefinitions, passwordHashKey } = await import('../../internal/rabbitmq-definitions.js');
76
+ // Только имена ключей в ошибке: значения хешей не выводятся.
77
+ const keys = [];
78
+ const users = services.map((service) => {
79
+ const key = passwordHashKey(service);
80
+ const hash = context.env[key] ?? '';
81
+ if (hash.length < MIN_HASH_LENGTH || !HASH_FORMAT.test(hash)) {
82
+ keys.push(key);
83
+ }
84
+ return { service, passwordHash: hash };
85
+ });
86
+ if (keys.length > 0) {
87
+ throw new CommandError('INVALID_ENVIRONMENT', 'Неверное окружение', { keys });
88
+ }
89
+ const json = `${JSON.stringify(buildDefinitions({ users }), null, 2)}\n`;
90
+ if (values.out === undefined) {
91
+ context.write(json);
92
+ logger.info({ services }, 'rabbitmq definitions: собрано');
93
+ return;
94
+ }
95
+ const out = resolve(context.cwd, values.out);
96
+ await mkdir(dirname(out), { recursive: true });
97
+ await writeFile(out, json);
98
+ context.write(`${JSON.stringify({ out, services })}\n`);
99
+ logger.info({ services, out }, 'rabbitmq definitions: собрано');
100
+ }
101
+ function failureOf(error) {
102
+ if (error instanceof CommandError) {
103
+ return { code: error.code, message: error.message, ...error.extra };
104
+ }
105
+ return { code: 'RABBITMQ_COMMAND_FAILED', message: 'команда rabbitmq завершилась ошибкой' };
106
+ }
107
+ /** Обработчик команды `rabbitmq`: 0 — definitions собраны, 1 — ошибка (JSON `{error}` в stdout). */
108
+ export async function rabbitmqCommand(args, context = defaultContext) {
109
+ try {
110
+ if (args[0] !== 'definitions') {
111
+ throw new CommandError('USAGE', 'подкоманда rabbitmq: definitions');
112
+ }
113
+ const logger = await createLogger(context.env);
114
+ logger.debug({ command: 'rabbitmq', sub: 'definitions' }, 'cli: команда');
115
+ await definitions(args.slice(1), context, logger);
116
+ return 0;
117
+ }
118
+ catch (error) {
119
+ const failure = failureOf(error);
120
+ process.stderr.write(`${JSON.stringify({ level: 'error', ...failure })}\n`);
121
+ context.write(`${JSON.stringify({ error: failure })}\n`);
122
+ return 1;
123
+ }
124
+ }
@@ -0,0 +1,11 @@
1
+ /** Окружение команды. */
2
+ export interface RedisCommandContext {
3
+ /** Рабочий каталог (от него — путь `--out`). */
4
+ readonly cwd: string;
5
+ /** Запись результата в stdout. */
6
+ readonly write: (text: string) => void;
7
+ /** Переменные окружения (по умолчанию `process.env`). */
8
+ readonly env: Readonly<Record<string, string | undefined>>;
9
+ }
10
+ /** Обработчик команды `redis`: 0 — aclfile записан, 1 — ошибка (JSON `{error}` в stdout). */
11
+ export declare function redisCommand(args: readonly string[], context?: RedisCommandContext): Promise<number>;
@@ -0,0 +1,119 @@
1
+ // Команда verdeect-kit redis acl (prd.md §3.7, UC-12; D3 плана E8): aclfile Redis — `user default off`, пользователи
2
+ // сервисов с sha256 паролей из REDIS_PASSWORD_HASH_<SVC>, ключи и каналы <svc>:*. Образец — commands/rabbitmq.ts. Без
3
+ // ioredis: только internal/redis-acl динамическим import(). aclfile — не JSON, поэтому --out обязателен.
4
+ import { mkdir, writeFile } from 'node:fs/promises';
5
+ import { dirname, resolve } from 'node:path';
6
+ import { parseArgs } from 'node:util';
7
+ import { pino } from 'pino';
8
+ import { z } from 'zod';
9
+ const defaultContext = {
10
+ cwd: process.cwd(),
11
+ write: (text) => void process.stdout.write(text),
12
+ env: process.env,
13
+ };
14
+ /** Ошибка команды: JSON в stdout и код 1. */
15
+ class CommandError extends Error {
16
+ code;
17
+ extra;
18
+ constructor(code, message, extra = {}) {
19
+ super(message);
20
+ this.code = code;
21
+ this.extra = extra;
22
+ }
23
+ }
24
+ async function createLogger(env) {
25
+ const [{ envBase }, { createLoggerOptions }] = await Promise.all([
26
+ import('../../config/fragments.js'),
27
+ import('../../observability/logger-options.js'),
28
+ ]);
29
+ const parsed = z
30
+ .object({ LOG_LEVEL: envBase.LOG_LEVEL, REPLICA_ID: envBase.REPLICA_ID })
31
+ .safeParse(env);
32
+ if (!parsed.success) {
33
+ const keys = [...new Set(parsed.error.issues.map((issue) => issue.path.join('.')))];
34
+ throw new CommandError('INVALID_ENVIRONMENT', 'Неверное окружение', { keys });
35
+ }
36
+ return pino(...createLoggerOptions({ service: 'verdeect-kit', entry: 'cli', env: parsed.data })
37
+ .pinoHttp);
38
+ }
39
+ function parseOptions(args) {
40
+ try {
41
+ return parseArgs({
42
+ args: [...args],
43
+ strict: true,
44
+ options: { services: { type: 'string' }, out: { type: 'string' } },
45
+ }).values;
46
+ }
47
+ catch (error) {
48
+ throw new CommandError('USAGE', error instanceof Error ? error.message : String(error));
49
+ }
50
+ }
51
+ /** Копия servicesOf из rabbitmq.ts: та бросает свой CommandError, и failureOf здесь его не узнал бы. */
52
+ async function servicesOf(raw) {
53
+ const services = (raw ?? '')
54
+ .split(',')
55
+ .map((name) => name.trim())
56
+ .filter((name) => name !== '');
57
+ if (services.length === 0) {
58
+ throw new CommandError('USAGE', 'нужна опция --services <svc,…>');
59
+ }
60
+ const { SERVICE_NAME_PATTERN } = await import('../../internal/service-name.js');
61
+ const bad = services.find((name) => !SERVICE_NAME_PATTERN.test(name));
62
+ if (bad !== undefined) {
63
+ throw new CommandError('USAGE', `--services: имя «${bad}» вне формата ^[a-z][a-z0-9-]{1,31}$`);
64
+ }
65
+ if (new Set(services).size !== services.length) {
66
+ throw new CommandError('USAGE', '--services: имя указано дважды');
67
+ }
68
+ return services;
69
+ }
70
+ async function acl(args, context, logger) {
71
+ const values = parseOptions(args);
72
+ const services = await servicesOf(values.services);
73
+ if (values.out === undefined) {
74
+ throw new CommandError('USAGE', 'нужна опция --out <файл>: aclfile — не JSON');
75
+ }
76
+ const { buildRedisAclFile, redisPasswordHashKey, REDIS_PASSWORD_HASH } = await import('../../internal/redis-acl.js');
77
+ // Только имена ключей в ошибке: значения хешей не выводятся.
78
+ const keys = [];
79
+ const users = services.map((service) => {
80
+ const key = redisPasswordHashKey(service);
81
+ const hash = context.env[key] ?? '';
82
+ if (!REDIS_PASSWORD_HASH.test(hash)) {
83
+ keys.push(key);
84
+ }
85
+ return { service, passwordHash: hash };
86
+ });
87
+ if (keys.length > 0) {
88
+ throw new CommandError('INVALID_ENVIRONMENT', 'Неверное окружение', { keys });
89
+ }
90
+ const out = resolve(context.cwd, values.out);
91
+ await mkdir(dirname(out), { recursive: true });
92
+ await writeFile(out, buildRedisAclFile({ users }));
93
+ context.write(`${JSON.stringify({ out, services })}\n`);
94
+ logger.info({ services, out }, 'redis acl: собрано');
95
+ }
96
+ function failureOf(error) {
97
+ if (error instanceof CommandError) {
98
+ return { code: error.code, message: error.message, ...error.extra };
99
+ }
100
+ return { code: 'REDIS_COMMAND_FAILED', message: 'команда redis завершилась ошибкой' };
101
+ }
102
+ /** Обработчик команды `redis`: 0 — aclfile записан, 1 — ошибка (JSON `{error}` в stdout). */
103
+ export async function redisCommand(args, context = defaultContext) {
104
+ try {
105
+ if (args[0] !== 'acl') {
106
+ throw new CommandError('USAGE', 'подкоманда redis: acl');
107
+ }
108
+ const logger = await createLogger(context.env);
109
+ logger.debug({ command: 'redis', sub: 'acl' }, 'cli: команда');
110
+ await acl(args.slice(1), context, logger);
111
+ return 0;
112
+ }
113
+ catch (error) {
114
+ const failure = failureOf(error);
115
+ process.stderr.write(`${JSON.stringify({ level: 'error', ...failure })}\n`);
116
+ context.write(`${JSON.stringify({ error: failure })}\n`);
117
+ return 1;
118
+ }
119
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import 'reflect-metadata';
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ // CLI verdeect-kit (prd.md §3.5, AC-3.7; ARCHITECTURE.md «CLI»): разбор команды и динамическая загрузка только её
3
+ // обработчика (openapi, db, rabbitmq, crypto, redis). Результат — JSON в stdout, ошибка — JSON и код выхода 1. process.exit не вызывается: его вызывают только
4
+ // failStartup и сторож KitLifecycle; код выхода — process.exitCode.
5
+ import 'reflect-metadata';
6
+ import { parseArgs } from 'node:util';
7
+ const COMMANDS = {
8
+ openapi: async () => (await import('./commands/openapi.js')).openapiCommand,
9
+ db: async () => (await import('./commands/db.js')).dbCommand,
10
+ rabbitmq: async () => (await import('./commands/rabbitmq.js')).rabbitmqCommand,
11
+ crypto: async () => (await import('./commands/crypto.js')).cryptoCommand,
12
+ redis: async () => (await import('./commands/redis.js')).redisCommand,
13
+ };
14
+ /** Точка входа: `verdeect-kit <команда> [опции]`. */
15
+ async function main(argv) {
16
+ const { positionals } = parseArgs({ args: [...argv], strict: false, allowPositionals: true });
17
+ const command = positionals[0] ?? '';
18
+ const load = Object.hasOwn(COMMANDS, command) ? COMMANDS[command] : undefined;
19
+ if (load === undefined) {
20
+ process.stdout.write(`${JSON.stringify({
21
+ error: {
22
+ code: 'UNKNOWN_COMMAND',
23
+ message: `неизвестная команда «${command}»; доступны: ${Object.keys(COMMANDS).join(', ')}`,
24
+ },
25
+ })}\n`);
26
+ return 1;
27
+ }
28
+ const handler = await load();
29
+ return handler(argv.slice(argv.indexOf(command) + 1));
30
+ }
31
+ process.exitCode = await main(process.argv.slice(2));
@@ -0,0 +1,29 @@
1
+ import { z } from 'zod';
2
+ /** Окружение CLI `db migrate`: адрес БД роли-владельца схемы (§3.1). */
3
+ export declare const migrateEnvSchema: z.ZodObject<{
4
+ DATABASE_MIGRATOR_URL: z.ZodString;
5
+ LOG_LEVEL: z.ZodDefault<z.ZodEnum<{
6
+ error: "error";
7
+ fatal: "fatal";
8
+ warn: "warn";
9
+ info: "info";
10
+ debug: "debug";
11
+ trace: "trace";
12
+ silent: "silent";
13
+ }>>;
14
+ }, z.core.$strip>;
15
+ /** Окружение CLI `db prepare`: суперпользователь и пароли ролей (§3.1). */
16
+ export declare const prepareEnvSchema: z.ZodObject<{
17
+ DATABASE_ADMIN_URL: z.ZodString;
18
+ DB_OWNER_PASSWORD: z.ZodString;
19
+ DB_APP_PASSWORD: z.ZodString;
20
+ LOG_LEVEL: z.ZodDefault<z.ZodEnum<{
21
+ error: "error";
22
+ fatal: "fatal";
23
+ warn: "warn";
24
+ info: "info";
25
+ debug: "debug";
26
+ trace: "trace";
27
+ silent: "silent";
28
+ }>>;
29
+ }, z.core.$strip>;
@@ -0,0 +1,15 @@
1
+ // Схемы окружения CLI (prd.md §3.1); эталон — O env.schema.ts MigrateEnvSchema/PrepareEnvSchema, имена паролей — §3.1.
2
+ import { z } from 'zod';
3
+ import { logLevel } from './fragments.js';
4
+ /** Окружение CLI `db migrate`: адрес БД роли-владельца схемы (§3.1). */
5
+ export const migrateEnvSchema = z.object({
6
+ DATABASE_MIGRATOR_URL: z.string().min(1),
7
+ LOG_LEVEL: logLevel,
8
+ });
9
+ /** Окружение CLI `db prepare`: суперпользователь и пароли ролей (§3.1). */
10
+ export const prepareEnvSchema = z.object({
11
+ DATABASE_ADMIN_URL: z.string().min(1),
12
+ DB_OWNER_PASSWORD: z.string().min(16),
13
+ DB_APP_PASSWORD: z.string().min(16),
14
+ LOG_LEVEL: logLevel,
15
+ });
@@ -0,0 +1,21 @@
1
+ import { z } from 'zod';
2
+ import type { envBase, envCrypto, envDatabase, envMessaging, envRedis, envSentry, envServiceUrls } from './fragments.js';
3
+ type KitShape = typeof envBase & typeof envDatabase & typeof envMessaging & typeof envRedis & typeof envSentry & typeof envCrypto & ReturnType<typeof envServiceUrls>;
4
+ type KitKeyOutput = {
5
+ [K in keyof KitShape]: z.output<KitShape[K]>;
6
+ };
7
+ /** Ключ пакета в shape сервиса обязан давать тот же выходной тип (AC-1.6). */
8
+ export type CompatibleShape<S extends z.ZodRawShape> = {
9
+ [K in keyof S]: K extends keyof KitKeyOutput ? z.ZodType<KitKeyOutput[K]> : S[K];
10
+ };
11
+ /**
12
+ * Проверки между ключами пакета (§3.1, AC-1.4): PORT ≠ METRICS_PORT (кроме 0 — порт выбирает ОС),
13
+ * SHUTDOWN_DRAIN_MS < SHUTDOWN_TIMEOUT_MS. Ключей нет в схеме — проверки нет. Форма схемы сохраняется.
14
+ */
15
+ export declare function withBaseChecks<T extends z.ZodObject>(schema: T): T;
16
+ /**
17
+ * Схема окружения сервиса (§3.1): распаковка фрагментов пакета + свои ключи, проверки withBaseChecks
18
+ * и необязательная своя проверка `refine`. Лишние переменные процесса отбрасываются.
19
+ */
20
+ export declare function defineEnv<S extends z.ZodRawShape>(shape: S & CompatibleShape<S>, refine?: (env: z.output<z.ZodObject<S>>, ctx: z.RefinementCtx) => void): z.ZodObject<S>;
21
+ export {};
@@ -0,0 +1,43 @@
1
+ // defineEnv и withBaseChecks (prd.md §3.1, AC-1.4, AC-1.6). Проверки между ключами пакета — эталон O superRefine.
2
+ import { z } from 'zod';
3
+ /** Число из разобранного окружения или undefined: проверки идут, только если ключи есть в схеме. */
4
+ function numberAt(env, key) {
5
+ const value = env[key];
6
+ return typeof value === 'number' ? value : undefined;
7
+ }
8
+ /**
9
+ * Проверки между ключами пакета (§3.1, AC-1.4): PORT ≠ METRICS_PORT (кроме 0 — порт выбирает ОС),
10
+ * SHUTDOWN_DRAIN_MS < SHUTDOWN_TIMEOUT_MS. Ключей нет в схеме — проверки нет. Форма схемы сохраняется.
11
+ */
12
+ export function withBaseChecks(schema) {
13
+ return schema.superRefine((env, ctx) => {
14
+ const appPort = numberAt(env, 'PORT');
15
+ const metricsPort = numberAt(env, 'METRICS_PORT');
16
+ if (appPort !== undefined && appPort === metricsPort && appPort !== 0) {
17
+ ctx.addIssue({
18
+ code: 'custom',
19
+ path: ['METRICS_PORT'],
20
+ message: 'PORT и METRICS_PORT совпадают',
21
+ input: '',
22
+ });
23
+ }
24
+ const drain = numberAt(env, 'SHUTDOWN_DRAIN_MS');
25
+ const timeout = numberAt(env, 'SHUTDOWN_TIMEOUT_MS');
26
+ if (drain !== undefined && timeout !== undefined && drain >= timeout) {
27
+ ctx.addIssue({
28
+ code: 'custom',
29
+ path: ['SHUTDOWN_DRAIN_MS'],
30
+ message: 'пауза не меньше общего срока остановки',
31
+ input: '',
32
+ });
33
+ }
34
+ });
35
+ }
36
+ /**
37
+ * Схема окружения сервиса (§3.1): распаковка фрагментов пакета + свои ключи, проверки withBaseChecks
38
+ * и необязательная своя проверка `refine`. Лишние переменные процесса отбрасываются.
39
+ */
40
+ export function defineEnv(shape, refine) {
41
+ const schema = withBaseChecks(z.object(shape));
42
+ return refine === undefined ? schema : schema.superRefine(refine);
43
+ }
@@ -0,0 +1,105 @@
1
+ import { z } from 'zod';
2
+ /** Уровень pino (§3.1); внутренний экспорт для схем CLI, из /config не реэкспортируется. */
3
+ export declare const logLevel: z.ZodDefault<z.ZodEnum<{
4
+ error: "error";
5
+ fatal: "fatal";
6
+ warn: "warn";
7
+ info: "info";
8
+ debug: "debug";
9
+ trace: "trace";
10
+ silent: "silent";
11
+ }>>;
12
+ /**
13
+ * Базовые ключи каждого сервиса (§3.1): NODE_ENV (production), LOG_LEVEL (info), PORT (3000),
14
+ * METRICS_PORT (9100), REPLICA_ID (hostname()), SHUTDOWN_TIMEOUT_MS (10000), SHUTDOWN_DRAIN_MS (0).
15
+ */
16
+ export declare const envBase: {
17
+ NODE_ENV: z.ZodDefault<z.ZodEnum<{
18
+ development: "development";
19
+ test: "test";
20
+ production: "production";
21
+ }>>;
22
+ LOG_LEVEL: z.ZodDefault<z.ZodEnum<{
23
+ error: "error";
24
+ fatal: "fatal";
25
+ warn: "warn";
26
+ info: "info";
27
+ debug: "debug";
28
+ trace: "trace";
29
+ silent: "silent";
30
+ }>>;
31
+ PORT: z.ZodCoercedNumber<unknown> | z.ZodDefault<z.ZodCoercedNumber<unknown>>;
32
+ METRICS_PORT: z.ZodCoercedNumber<unknown> | z.ZodDefault<z.ZodCoercedNumber<unknown>>;
33
+ REPLICA_ID: z.ZodPipe<z.ZodPipe<z.ZodPipe<z.ZodDefault<z.ZodString>, z.ZodTransform<string | null, string>>, z.ZodTransform<string, string | null>>, z.ZodString>;
34
+ SHUTDOWN_TIMEOUT_MS: z.ZodCoercedNumber<unknown> | z.ZodDefault<z.ZodCoercedNumber<unknown>>;
35
+ SHUTDOWN_DRAIN_MS: z.ZodCoercedNumber<unknown> | z.ZodDefault<z.ZodCoercedNumber<unknown>>;
36
+ };
37
+ /** БД (§3.1): DATABASE_URL (обязателен), DB_POOL_MAX (10), DB_POOL_ACQUIRE_TIMEOUT_MS (5000, мин. 1). */
38
+ export declare const envDatabase: {
39
+ DATABASE_URL: z.ZodString;
40
+ DB_POOL_MAX: z.ZodCoercedNumber<unknown> | z.ZodDefault<z.ZodCoercedNumber<unknown>>;
41
+ DB_POOL_ACQUIRE_TIMEOUT_MS: z.ZodCoercedNumber<unknown> | z.ZodDefault<z.ZodCoercedNumber<unknown>>;
42
+ };
43
+ /** RabbitMQ (§3.1): RABBITMQ_URL (обязателен), RABBITMQ_RETRY_SECONDS (30, целое ≥ 1). */
44
+ export declare const envMessaging: {
45
+ RABBITMQ_URL: z.ZodURL;
46
+ RABBITMQ_RETRY_SECONDS: z.ZodCoercedNumber<unknown> | z.ZodDefault<z.ZodCoercedNumber<unknown>>;
47
+ };
48
+ /** Redis (§3.1): REDIS_URL (обязателен). */
49
+ export declare const envRedis: {
50
+ REDIS_URL: z.ZodURL;
51
+ };
52
+ /** GlitchTip (§3.1): SENTRY_DSN, SENTRY_ENVIRONMENT, SENTRY_RELEASE — пусто → null; NODE_ENV. */
53
+ export declare const envSentry: {
54
+ SENTRY_DSN: z.ZodPipe<z.ZodDefault<z.ZodString>, z.ZodTransform<string | null, string>>;
55
+ SENTRY_ENVIRONMENT: z.ZodPipe<z.ZodDefault<z.ZodString>, z.ZodTransform<string | null, string>>;
56
+ SENTRY_RELEASE: z.ZodPipe<z.ZodDefault<z.ZodString>, z.ZodTransform<string | null, string>>;
57
+ NODE_ENV: z.ZodDefault<z.ZodEnum<{
58
+ development: "development";
59
+ test: "test";
60
+ production: "production";
61
+ }>>;
62
+ };
63
+ /**
64
+ * @internal Массив ключей шифра `[{version, secret}]`: одна проверка для envCrypto и createCipher (§3.6);
65
+ * из /config не реэкспортируется.
66
+ */
67
+ export declare const encryptionKeysSchema: z.ZodArray<z.ZodObject<{
68
+ version: z.ZodNumber;
69
+ secret: z.ZodString;
70
+ }, z.core.$strip>>;
71
+ /** Ключи шифра (§3.1, §3.6): ENCRYPTION_KEYS — JSON `[{version, secret}]`, версии без повторов. */
72
+ export declare const envCrypto: {
73
+ ENCRYPTION_KEYS: z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<unknown, string>>, z.ZodArray<z.ZodObject<{
74
+ version: z.ZodNumber;
75
+ secret: z.ZodString;
76
+ }, z.core.$strip>>> | z.ZodPipe<z.ZodPipe<z.ZodDefault<z.ZodString>, z.ZodTransform<unknown, string>>, z.ZodArray<z.ZodObject<{
77
+ version: z.ZodNumber;
78
+ secret: z.ZodString;
79
+ }, z.core.$strip>>>;
80
+ };
81
+ /**
82
+ * Адреса других сервисов (§3.1, AC-1.11): SERVICE_URLS — JSON `{имя: http(s)-URL}`; имена `required`
83
+ * обязательны, лишние имена допустимы, но тоже должны быть URL.
84
+ */
85
+ export declare function envServiceUrls(required: readonly string[]): {
86
+ SERVICE_URLS: z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<unknown, string>>, z.ZodPipe<z.ZodObject<{
87
+ [x: string]: z.ZodURL;
88
+ }, z.core.$catchall<z.ZodURL>>, z.ZodTransform<Readonly<Record<string, string>>, {
89
+ [x: string]: string;
90
+ }>>> | z.ZodPipe<z.ZodPipe<z.ZodDefault<z.ZodString>, z.ZodTransform<unknown, string>>, z.ZodPipe<z.ZodObject<{
91
+ [x: string]: z.ZodURL;
92
+ }, z.core.$catchall<z.ZodURL>>, z.ZodTransform<Readonly<Record<string, string>>, {
93
+ [x: string]: string;
94
+ }>>>;
95
+ };
96
+ /** Разобранные базовые ключи (§3.1). */
97
+ export type KitBaseEnv = z.output<z.ZodObject<typeof envBase>>;
98
+ /** Разобранные ключи БД (§3.1). */
99
+ export type KitDatabaseEnv = z.output<z.ZodObject<typeof envDatabase>>;
100
+ /** Разобранные ключи RabbitMQ (§3.1). */
101
+ export type KitMessagingEnv = z.output<z.ZodObject<typeof envMessaging>>;
102
+ /** Разобранные ключи Redis (§3.1). */
103
+ export type KitRedisEnv = z.output<z.ZodObject<typeof envRedis>>;
104
+ /** Разобранные ключи шифра (§3.1). */
105
+ export type KitCryptoEnv = z.output<z.ZodObject<typeof envCrypto>>;
@@ -0,0 +1,86 @@
1
+ // Фрагменты схемы окружения пакета (prd.md §3.1, C0–C6); эталон — O env.schema.ts, SERVICE_URLS — B.
2
+ // Решения PRD поверх эталонов: PORT вместо BFF_PORT/IDENTITY_PORT, RABBITMQ_RETRY_SECONDS = 30, ENCRYPTION_KEYS —
3
+ // JSON [{version, secret}] (§3.6, CE-2). Псевдонимов старых ключей нет (P0, N9).
4
+ // Фрагмент — объект zod-схем (raw shape), а не z.object: он распаковывается в defineEnv.
5
+ import { hostname } from 'node:os';
6
+ import { z } from 'zod';
7
+ import { httpUrl, jsonValue, nonNegativeInt, optionalText, port, positiveInt, uniqueBy, } from './helpers.js';
8
+ /** Режим процесса; одна схема для envBase и envSentry, чтобы распаковка обоих не конфликтовала. */
9
+ const nodeEnv = z.enum(['development', 'test', 'production']).default('production');
10
+ /** Уровень pino (§3.1); внутренний экспорт для схем CLI, из /config не реэкспортируется. */
11
+ export const logLevel = z
12
+ .enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace', 'silent'])
13
+ .default('info');
14
+ /** Имя реплики: пусто → hostname() (§3.1). */
15
+ const replicaId = optionalText()
16
+ .transform((value) => value ?? hostname())
17
+ .pipe(z.string().regex(/^[A-Za-z0-9._-]{1,128}$/));
18
+ /**
19
+ * Базовые ключи каждого сервиса (§3.1): NODE_ENV (production), LOG_LEVEL (info), PORT (3000),
20
+ * METRICS_PORT (9100), REPLICA_ID (hostname()), SHUTDOWN_TIMEOUT_MS (10000), SHUTDOWN_DRAIN_MS (0).
21
+ */
22
+ export const envBase = {
23
+ NODE_ENV: nodeEnv,
24
+ LOG_LEVEL: logLevel,
25
+ PORT: port(3000),
26
+ METRICS_PORT: port(9100),
27
+ REPLICA_ID: replicaId,
28
+ SHUTDOWN_TIMEOUT_MS: positiveInt(10_000),
29
+ SHUTDOWN_DRAIN_MS: nonNegativeInt(0),
30
+ };
31
+ /** БД (§3.1): DATABASE_URL (обязателен), DB_POOL_MAX (10), DB_POOL_ACQUIRE_TIMEOUT_MS (5000, мин. 1). */
32
+ export const envDatabase = {
33
+ DATABASE_URL: z.string().min(1),
34
+ DB_POOL_MAX: positiveInt(10),
35
+ DB_POOL_ACQUIRE_TIMEOUT_MS: positiveInt(5000),
36
+ };
37
+ /** RabbitMQ (§3.1): RABBITMQ_URL (обязателен), RABBITMQ_RETRY_SECONDS (30, целое ≥ 1). */
38
+ export const envMessaging = {
39
+ RABBITMQ_URL: z.url(),
40
+ RABBITMQ_RETRY_SECONDS: positiveInt(30),
41
+ };
42
+ /** Redis (§3.1): REDIS_URL (обязателен). */
43
+ export const envRedis = {
44
+ REDIS_URL: z.url(),
45
+ };
46
+ /** GlitchTip (§3.1): SENTRY_DSN, SENTRY_ENVIRONMENT, SENTRY_RELEASE — пусто → null; NODE_ENV. */
47
+ export const envSentry = {
48
+ SENTRY_DSN: optionalText(),
49
+ SENTRY_ENVIRONMENT: optionalText(),
50
+ SENTRY_RELEASE: optionalText(),
51
+ NODE_ENV: nodeEnv,
52
+ };
53
+ /** Секрет ключа шифра: base64url без `=`, не короче 32 байт (§3.6). Issue — без значения. */
54
+ const encryptionSecret = z
55
+ .string()
56
+ .regex(/^[A-Za-z0-9_-]+$/)
57
+ .superRefine((value, ctx) => {
58
+ if (Buffer.from(value, 'base64url').length < 32) {
59
+ ctx.addIssue({ code: 'custom', message: 'секрет короче 32 байт', input: '' });
60
+ }
61
+ });
62
+ /**
63
+ * @internal Массив ключей шифра `[{version, secret}]`: одна проверка для envCrypto и createCipher (§3.6);
64
+ * из /config не реэкспортируется.
65
+ */
66
+ export const encryptionKeysSchema = z
67
+ .array(z.object({ version: z.number().int().positive(), secret: encryptionSecret }))
68
+ .min(1)
69
+ .superRefine(uniqueBy('version'));
70
+ /** Ключи шифра (§3.1, §3.6): ENCRYPTION_KEYS — JSON `[{version, secret}]`, версии без повторов. */
71
+ export const envCrypto = {
72
+ ENCRYPTION_KEYS: jsonValue(encryptionKeysSchema),
73
+ };
74
+ /**
75
+ * Адреса других сервисов (§3.1, AC-1.11): SERVICE_URLS — JSON `{имя: http(s)-URL}`; имена `required`
76
+ * обязательны, лишние имена допустимы, но тоже должны быть URL.
77
+ */
78
+ export function envServiceUrls(required) {
79
+ const known = Object.fromEntries(required.map((name) => [name, httpUrl()]));
80
+ return {
81
+ SERVICE_URLS: jsonValue(z
82
+ .object(known)
83
+ .catchall(httpUrl())
84
+ .transform((map) => map)),
85
+ };
86
+ }
@@ -0,0 +1,26 @@
1
+ import { z } from 'zod';
2
+ /** TCP-порт 0–65535; 0 — порт выбирает ОС (тесты). Пустая строка даёт 0, как в эталоне (§3.1). */
3
+ export declare function port(fallback?: number): z.ZodCoercedNumber<unknown> | z.ZodDefault<z.ZodCoercedNumber<unknown>>;
4
+ /** Целое > 0 (§3.1). */
5
+ export declare function positiveInt(fallback?: number): z.ZodCoercedNumber<unknown> | z.ZodDefault<z.ZodCoercedNumber<unknown>>;
6
+ /** Целое ≥ 0; внутренний помощник фрагментов, из /config не реэкспортируется. */
7
+ export declare function nonNegativeInt(fallback?: number): z.ZodCoercedNumber<unknown> | z.ZodDefault<z.ZodCoercedNumber<unknown>>;
8
+ /** Необязательный текст: пусто или пробелы → null, иначе значение без крайних пробелов (§3.1). */
9
+ export declare function optionalText(): z.ZodPipe<z.ZodDefault<z.ZodString>, z.ZodTransform<string | null, string>>;
10
+ /** Только строки `true`/`false`: приведение zod к boolean сделало бы строку `false` истиной (§3.1). */
11
+ export declare function booleanFlag(fallback?: boolean): z.ZodPipe<z.ZodDefault<z.ZodEnum<{
12
+ true: "true";
13
+ false: "false";
14
+ }>>, z.ZodTransform<boolean, "true" | "false">> | z.ZodPipe<z.ZodEnum<{
15
+ true: "true";
16
+ false: "false";
17
+ }>, z.ZodTransform<boolean, "true" | "false">>;
18
+ /**
19
+ * Ключ с JSON-значением (§3.1). Умолчание — строка до разбора: в zod 4 умолчание после
20
+ * преобразования ожидает уже разобранный тип. Ошибка разбора — без входного значения.
21
+ */
22
+ export declare function jsonValue<T extends z.ZodType>(schema: T, fallback?: string): z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<unknown, string>>, T> | z.ZodPipe<z.ZodPipe<z.ZodDefault<z.ZodString>, z.ZodTransform<unknown, string>>, T>;
23
+ /** Адрес HTTP-ресурса: только http и https (§3.1). */
24
+ export declare function httpUrl(): z.ZodURL;
25
+ /** Значения поля `key` элементов массива не повторяются; для `superRefine`, issue — без значений (§3.1). */
26
+ export declare function uniqueBy<T>(key: keyof T): (items: readonly T[], ctx: z.RefinementCtx) => void;