@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,17 @@
1
+ // Точка входа @verdeect/kit/testing (prd.md §3.8, E1 → E8): явный список экспортов; только условие development (NFR-4).
2
+ export { createTestApp } from './create-test-app.js';
3
+ export { expectMatchesApi } from './expect-matches-api.js';
4
+ export { expectProblem } from './expect-problem.js';
5
+ export { TEST_IMAGES } from './images.js';
6
+ export { startTestStack } from './start-test-stack.js';
7
+ export { tcpProxy } from './tcp-proxy.js';
8
+ export { TEST_TIMEOUT_SCALE } from './timeout-scale.js';
9
+ export { assertRepoConventions } from './repo-conventions.js';
10
+ export { timingGuard } from './timing-guard.js';
11
+ export { createScratchDatabase } from './scratch-database.js';
12
+ export { redisWorkerOverride } from './redis-worker.js';
13
+ export { workerScope } from './worker-scope.js';
14
+ export { countQueries } from './count-queries.js';
15
+ export { assertAppRoleIsNotOwner, assertStrictTenantIsolation } from './tenant-isolation.js';
16
+ export { assertOutboxCoverage, expectOutbox } from './outbox-coverage.js';
17
+ export { publishTestEvent, waitForInbox } from './test-events.js';
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Строки скрипта shell: перевод строки вне кавычек делит команды (как `;`), `\` перед переводом строки — продолжение
3
+ * строки; комментарий `#` в начале слова вне кавычек снимается до конца своей строки. Строки — без комментариев.
4
+ */
5
+ export declare function shellLines(text: string): string[];
6
+ /**
7
+ * Слова строки без выполнения shell (как Compose делит строковые `command:`/`entrypoint:`): кавычки снимаются,
8
+ * операторы остаются словами. Переменные остаются как есть.
9
+ */
10
+ export declare function shellWords(text: string): string[];
11
+ /** Скрипт вызова `sh|bash|ash|dash [флаги] -c <скрипт>` или null (см. shellCall). */
12
+ export declare function shellScript(tokens: readonly string[]): string | null;
13
+ /**
14
+ * Команды запуска: массив — как есть, строка — по правилам shell; `sh -c "<скрипт>"` (и `sh -ec`, `bash -lc`,
15
+ * `sh -e -c`, `bash -eo pipefail -c`) раскрывается рекурсивно.
16
+ */
17
+ export declare function tokenize(command: string | readonly string[]): string[][];
18
+ /** Входной файл node-команды или null; опции со значением (NODE_VALUE_OPTIONS) без `=` съедают следующий токен. */
19
+ export declare function nodeEntry(tokens: readonly string[]): {
20
+ entry: string;
21
+ imports: string[];
22
+ } | null;
23
+ /** Форма, которую правило не распознаёт (§3.8 v1.10): описание формы и простая команда, где она найдена. */
24
+ export interface UnrecognizedForm {
25
+ /** Найденная форма по-русски: «раннер pnpm start», «обёртка с аргументами gosu» и т. п. */
26
+ readonly form: string;
27
+ /** Команда через пробел. */
28
+ readonly command: string;
29
+ }
30
+ /** Запуск приложения: argv node после снятия обёрток и простая команда, как она записана. */
31
+ export interface AppLaunch {
32
+ /** Argv node без обёрток (`exec`, `tini --`, …). */
33
+ readonly tokens: readonly string[];
34
+ /** Простая команда через пробел, как записана. */
35
+ readonly command: string;
36
+ }
37
+ /** Результат разбора команды запуска. */
38
+ export interface LaunchAnalysis {
39
+ /** Простые команды node с файлом входа приложения (main, main.js, …). */
40
+ readonly launches: readonly AppLaunch[];
41
+ /** Есть подделка `*.main.ts` / `*.main.js`. */
42
+ readonly fake: boolean;
43
+ /** Нераспознанные формы. */
44
+ readonly unrecognized: readonly UnrecognizedForm[];
45
+ }
46
+ /**
47
+ * Разбор команды запуска (argv exec-формы или shell-строка): запуски приложения, подделки и нераспознанные формы
48
+ * (§3.8 v1.10). `--import` засчитывается только в аргументах node, NODE_OPTIONS не учитывается.
49
+ */
50
+ export declare function analyzeLaunch(input: string | readonly string[]): LaunchAnalysis;
51
+ /** У запуска приложения есть `--import` со значением, где есть `instrument`. */
52
+ export declare function hasInstrumentImport(tokens: readonly string[]): boolean;
@@ -0,0 +1,510 @@
1
+ // Команда запуска приложения: разбор shell и распознавание форм (prd.md §3.8 v1.10, AC-1.8) для assertRepoConventions.
2
+ // Поддерживаемые формы: exec- и shell-форма; обёртки без аргументов exec, tini --, dumb-init, env (VAR=…) перед node
3
+ // или sh -c; sh/bash/ash/dash с любыми флагами до -c; несколько команд через перевод строки, ;, &&, ||, |, &,
4
+ // подоболочку; ${VAR:-умолчание}. Остальное, где возможен запуск приложения, — «команда не распознана».
5
+ import { basename } from 'node:path';
6
+ import { parse as parseShell } from 'shell-quote';
7
+ import { expandDefaults } from './repo-files.js';
8
+ const SHELLS = new Set(['sh', 'bash', 'ash', 'dash']);
9
+ /**
10
+ * Опции node, которые без `=` съедают следующий токен. Источник — `node --help` Node.js 22.23.3 и 24.22.1 (опции
11
+ * вида `--opt=...`, объединение версий) плюс алиасы, которые справка выводит отдельной строкой (`--loader`,
12
+ * `--experimental-loader`, `--inspect-port`, `--debug-port`, `-p`/`--print`) и V8-опция `--stack-size`. Опции с
13
+ * необязательным значением (`--inspect[=…]`, `--force-fips[=…]`) следующий токен не съедают.
14
+ */
15
+ const NODE_VALUE_OPTIONS = new Set([
16
+ '--allow-fs-read',
17
+ '--allow-fs-write',
18
+ '--build-snapshot-config',
19
+ '-C',
20
+ '--conditions',
21
+ '--cpu-prof-dir',
22
+ '--cpu-prof-interval',
23
+ '--cpu-prof-name',
24
+ '--debug-port',
25
+ '--diagnostic-dir',
26
+ '--disable-proto',
27
+ '--disable-warning',
28
+ '--dns-result-order',
29
+ '-e',
30
+ '--eval',
31
+ '--env-file',
32
+ '--env-file-if-exists',
33
+ '--experimental-config-file',
34
+ '--experimental-default-type',
35
+ '--experimental-loader',
36
+ '--experimental-package-map',
37
+ '--experimental-sea-config',
38
+ '--experimental-test-isolation',
39
+ '--experimental-test-tag-filter',
40
+ '--heap-prof-dir',
41
+ '--heap-prof-interval',
42
+ '--heap-prof-name',
43
+ '--heapsnapshot-near-heap-limit',
44
+ '--heapsnapshot-signal',
45
+ '--icu-data-dir',
46
+ '--import',
47
+ '--input-type',
48
+ '--inspect-port',
49
+ '--inspect-publish-uid',
50
+ '--loader',
51
+ '--localstorage-file',
52
+ '--max-http-header-size',
53
+ '--max-old-space-size-percentage',
54
+ '--network-family-autoselection-attempt-timeout',
55
+ '--openssl-config',
56
+ '-p',
57
+ '--print',
58
+ '-r',
59
+ '--redirect-warnings',
60
+ '--report-filename',
61
+ '--report-signal',
62
+ '--require',
63
+ '--run',
64
+ '--secure-heap',
65
+ '--secure-heap-min',
66
+ '--snapshot-blob',
67
+ '--stack-size',
68
+ '--test-concurrency',
69
+ '--test-coverage-branches',
70
+ '--test-coverage-exclude',
71
+ '--test-coverage-functions',
72
+ '--test-coverage-include',
73
+ '--test-coverage-lines',
74
+ '--test-global-setup',
75
+ '--test-name-pattern',
76
+ '--test-random-seed',
77
+ '--test-reporter',
78
+ '--test-reporter-destination',
79
+ '--test-rerun-failures',
80
+ '--test-shard',
81
+ '--test-skip-pattern',
82
+ '--test-timeout',
83
+ '--title',
84
+ '--tls-cipher-list',
85
+ '--tls-keylog',
86
+ '--trace-event-categories',
87
+ '--trace-event-file-pattern',
88
+ '--trace-require-module',
89
+ '--unhandled-rejections',
90
+ '--use-largepages',
91
+ '--v8-pool-size',
92
+ '--watch-kill-signal',
93
+ '--watch-path',
94
+ ]);
95
+ /** Файл входа приложения (§3.8 v1.10): main, main.js, main.mjs, main.cjs, main.ts. */
96
+ const APP_ENTRY = /^main(\.(js|mjs|cjs|ts))?$/;
97
+ /** Подделка вида `*.main.ts` / `*.main.js` (например, fake-services.main.ts). */
98
+ const FAKE_ENTRY = /.\.main\.(ts|js)$/;
99
+ /** Присваивание перед командой (`NODE_OPTIONS=… node …`, аргументы env). */
100
+ const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
101
+ const PACKAGE_MANAGERS = new Set(['pnpm', 'npm', 'yarn']);
102
+ /** Раннеры и супервизоры, которые запускают приложение вместо node (§3.8 v1.10). */
103
+ const RUNNERS = new Set(['tsx', 'ts-node', 'pm2', 'pm2-runtime', 'nodemon']);
104
+ /** Обёртки с аргументами (§3.8 v1.10): пользователь перед командой. */
105
+ const ARGUMENT_WRAPPERS = new Set(['gosu', 'su-exec']);
106
+ /** Операторы shell-quote, которые разделяют простые команды. */
107
+ const COMMAND_SEPARATORS = new Set(['&&', '||', ';', ';;', '&', '|', '|&', '(', ')', '<(']);
108
+ /** Операторы перенаправления: следующее слово — файл, а не аргумент команды. */
109
+ const REDIRECTS = new Set(['<', '>', '>>', '>&']);
110
+ /** Заменитель `$` на время разбора: shell-quote подставил бы вместо переменных пустую строку. */
111
+ const DOLLAR = '';
112
+ /**
113
+ * Строки скрипта shell: перевод строки вне кавычек делит команды (как `;`), `\` перед переводом строки — продолжение
114
+ * строки; комментарий `#` в начале слова вне кавычек снимается до конца своей строки. Строки — без комментариев.
115
+ */
116
+ export function shellLines(text) {
117
+ const lines = [];
118
+ let line = '';
119
+ let quote = null;
120
+ let comment = false;
121
+ for (let index = 0; index < text.length; index += 1) {
122
+ const char = text.charAt(index);
123
+ const next = text.charAt(index + 1);
124
+ if (comment) {
125
+ if (char === '\n') {
126
+ comment = false;
127
+ lines.push(line);
128
+ line = '';
129
+ }
130
+ continue;
131
+ }
132
+ if (char === '\\' && quote !== "'") {
133
+ if (next === '\n') {
134
+ index += 1;
135
+ continue;
136
+ }
137
+ line += char + next;
138
+ index += 1;
139
+ continue;
140
+ }
141
+ if (quote !== null) {
142
+ if (char === quote) {
143
+ quote = null;
144
+ }
145
+ line += char;
146
+ continue;
147
+ }
148
+ if (char === '"' || char === "'") {
149
+ quote = char;
150
+ line += char;
151
+ continue;
152
+ }
153
+ if (char === '#' && (line === '' || /\s$/.test(line))) {
154
+ comment = true;
155
+ continue;
156
+ }
157
+ if (char === '\n') {
158
+ lines.push(line);
159
+ line = '';
160
+ continue;
161
+ }
162
+ line += char;
163
+ }
164
+ lines.push(line);
165
+ return lines.filter((item) => item.trim() !== '');
166
+ }
167
+ /** Простые команды одной строки скрипта (без переводов строки и комментариев вне кавычек). */
168
+ function lineCommands(text) {
169
+ const commands = [];
170
+ let words = [];
171
+ let skipNext = false;
172
+ let lastWasWord = false;
173
+ const flush = () => {
174
+ if (words.length > 0) {
175
+ commands.push(words);
176
+ }
177
+ words = [];
178
+ };
179
+ const restore = (word) => word.replaceAll(DOLLAR, '$');
180
+ const entries = parseShell(text.replaceAll('$', DOLLAR), (key) => `$${key}`);
181
+ for (const entry of entries) {
182
+ if (typeof entry === 'string') {
183
+ lastWasWord = !skipNext;
184
+ if (skipNext) {
185
+ skipNext = false;
186
+ continue;
187
+ }
188
+ words.push(restore(entry));
189
+ continue;
190
+ }
191
+ if ('comment' in entry) {
192
+ // Комментарии сняты в shellLines; сюда попадает только `#` внутри слова — конец строки.
193
+ break;
194
+ }
195
+ if (entry.op === 'glob') {
196
+ words.push(restore(entry.pattern));
197
+ lastWasWord = true;
198
+ continue;
199
+ }
200
+ if (COMMAND_SEPARATORS.has(entry.op)) {
201
+ skipNext = false;
202
+ lastWasWord = false;
203
+ flush();
204
+ continue;
205
+ }
206
+ skipNext = REDIRECTS.has(entry.op);
207
+ // `2>&1`: номер дескриптора перед перенаправлением — часть перенаправления, не аргумент команды.
208
+ if (skipNext && lastWasWord && /^\d+$/.test(words.at(-1) ?? '')) {
209
+ words.pop();
210
+ }
211
+ lastWasWord = false;
212
+ }
213
+ flush();
214
+ return commands;
215
+ }
216
+ /**
217
+ * Простые команды shell-строки (shell-quote): `&&`, `||`, `;`, `&`, `|`, `(`, `)` и перевод строки — разделители,
218
+ * кавычки и экранирование — по правилам shell. Переменные `$VAR` и `${VAR}` остаются в токенах как есть.
219
+ */
220
+ function shellCommands(text) {
221
+ return shellLines(text).flatMap(lineCommands);
222
+ }
223
+ /**
224
+ * Слова строки без выполнения shell (как Compose делит строковые `command:`/`entrypoint:`): кавычки снимаются,
225
+ * операторы остаются словами. Переменные остаются как есть.
226
+ */
227
+ export function shellWords(text) {
228
+ return parseShell(text.replaceAll('$', DOLLAR), (key) => `$${key}`).map((entry) => {
229
+ if (typeof entry === 'string') {
230
+ return entry.replaceAll(DOLLAR, '$');
231
+ }
232
+ if ('comment' in entry) {
233
+ return `#${entry.comment}`;
234
+ }
235
+ return entry.op === 'glob' ? entry.pattern : entry.op;
236
+ });
237
+ }
238
+ /** Длинные опции bash со значением (`bash --help`). */
239
+ const SHELL_VALUE_LONG_OPTIONS = new Set(['--rcfile', '--init-file']);
240
+ /** Флаг `-c` оболочки: отдельно или в кластере коротких флагов (`-ec`). */
241
+ const SHELL_C_FLAG = /^-[A-Za-z]*c[A-Za-z]*$/;
242
+ /**
243
+ * Вызов `sh|bash|ash|dash [флаги] -c <скрипт>` (§3.8 v1.11): короткие флаги и кластеры пропускаются, `o`/`O` (в `-o`,
244
+ * `+o`, `-O`, `+O` и в кластере, например `-eo`) забирают следующий токен (имя опции); длинные `--<имя>` пропускаются,
245
+ * `--rcfile` и `--init-file` — со значением. Есть `-c`, а скрипт выделить не удалось — `unparsed`.
246
+ */
247
+ function shellCall(tokens) {
248
+ const [shell] = tokens;
249
+ if (shell === undefined || !SHELLS.has(basename(shell))) {
250
+ return { kind: 'none' };
251
+ }
252
+ for (let index = 1; index < tokens.length; index += 1) {
253
+ const token = tokens[index] ?? '';
254
+ if (/^--[A-Za-z][\w-]*$/.test(token)) {
255
+ index += SHELL_VALUE_LONG_OPTIONS.has(token) ? 1 : 0;
256
+ continue;
257
+ }
258
+ if (!/^[-+][A-Za-z]+$/.test(token)) {
259
+ break;
260
+ }
261
+ const optionNames = [...token.slice(1)].filter((flag) => flag === 'o' || flag === 'O').length;
262
+ if (token.startsWith('-') && token.includes('c')) {
263
+ const script = tokens[index + 1 + optionNames];
264
+ return script === undefined ? { kind: 'unparsed' } : { kind: 'script', script };
265
+ }
266
+ index += optionNames;
267
+ }
268
+ return tokens.slice(1).some((token) => SHELL_C_FLAG.test(token))
269
+ ? { kind: 'unparsed' }
270
+ : { kind: 'none' };
271
+ }
272
+ /** Скрипт вызова `sh|bash|ash|dash [флаги] -c <скрипт>` или null (см. shellCall). */
273
+ export function shellScript(tokens) {
274
+ const call = shellCall(tokens);
275
+ return call.kind === 'script' ? call.script : null;
276
+ }
277
+ /**
278
+ * Команды запуска: массив — как есть, строка — по правилам shell; `sh -c "<скрипт>"` (и `sh -ec`, `bash -lc`,
279
+ * `sh -e -c`, `bash -eo pipefail -c`) раскрывается рекурсивно.
280
+ */
281
+ export function tokenize(command) {
282
+ const commands = typeof command === 'string' ? shellCommands(command) : [[...command]];
283
+ return commands.flatMap((tokens) => {
284
+ const script = shellScript(tokens);
285
+ return script === null ? [tokens] : tokenize(script);
286
+ });
287
+ }
288
+ /** Входной файл node-команды или null; опции со значением (NODE_VALUE_OPTIONS) без `=` съедают следующий токен. */
289
+ export function nodeEntry(tokens) {
290
+ const found = nodeEntryAt(tokens);
291
+ return found === null ? null : { entry: found.entry, imports: found.imports };
292
+ }
293
+ /** nodeEntry с индексом файла входа в tokens. */
294
+ function nodeEntryAt(tokens) {
295
+ const start = tokens.findIndex((token) => basename(token) === 'node');
296
+ if (start === -1) {
297
+ return null;
298
+ }
299
+ const imports = [];
300
+ for (let index = start + 1; index < tokens.length; index += 1) {
301
+ const token = tokens[index] ?? '';
302
+ if (!token.startsWith('-')) {
303
+ return { entry: token, imports, index };
304
+ }
305
+ const eq = token.indexOf('=');
306
+ const name = eq === -1 ? token : token.slice(0, eq);
307
+ let value;
308
+ if (eq !== -1) {
309
+ value = token.slice(eq + 1);
310
+ }
311
+ else if (NODE_VALUE_OPTIONS.has(name)) {
312
+ index += 1;
313
+ value = tokens[index];
314
+ }
315
+ if (name === '--import' && value !== undefined) {
316
+ imports.push(value);
317
+ }
318
+ }
319
+ return null;
320
+ }
321
+ function unrecognized(acc, form, tokens) {
322
+ acc.unrecognized.push({ form, command: tokens.join(' ') });
323
+ }
324
+ /**
325
+ * Опции обёрток init (§3.8 v1.11). Источники: README tini (`-s`, `-g`, `-v`, `-w`, `-l`; `-p <сигнал>`, `-e <код>`) и
326
+ * `dumb-init --help` (`-c`/`--single-child`, `-v`/`--verbose`; `-r`/`--rewrite <s:r>`). `--` — конец опций.
327
+ */
328
+ const WRAPPER_OPTIONS = {
329
+ tini: { flags: new Set(['-s', '-g', '-v', '-w', '-l']), values: new Set(['-p', '-e']) },
330
+ 'dumb-init': {
331
+ flags: new Set(['-c', '--single-child', '-v', '--verbose']),
332
+ values: new Set(['-r', '--rewrite']),
333
+ },
334
+ };
335
+ /** Аргументы обёртки init после её опций; опция вне списка — «опция обёртки <имя>», null. */
336
+ function initWrapperArgs(head, tokens, acc) {
337
+ const spec = WRAPPER_OPTIONS[head];
338
+ const rest = tokens.slice(1);
339
+ if (spec === undefined) {
340
+ return rest;
341
+ }
342
+ let index = 0;
343
+ while (index < rest.length) {
344
+ const token = rest[index] ?? '';
345
+ if (token === '--') {
346
+ return rest.slice(index + 1);
347
+ }
348
+ if (!token.startsWith('-')) {
349
+ break;
350
+ }
351
+ const eq = token.indexOf('=');
352
+ const name = eq === -1 ? token : token.slice(0, eq);
353
+ if (spec.flags.has(token)) {
354
+ index += 1;
355
+ }
356
+ else if (spec.values.has(name)) {
357
+ index += eq === -1 ? 2 : 1;
358
+ }
359
+ else {
360
+ unrecognized(acc, `опция обёртки ${name}`, tokens);
361
+ return null;
362
+ }
363
+ }
364
+ return rest.slice(index);
365
+ }
366
+ /** Обёртка (`exec`, `tini [опции] [--]`, `dumb-init [опции] [--]`, `env [VAR=…] [-флаги]`) снимается; иначе null. */
367
+ function unwrap(tokens, acc) {
368
+ const head = basename(tokens[0] ?? '');
369
+ const rest = tokens.slice(1);
370
+ if (head === 'exec') {
371
+ return rest;
372
+ }
373
+ if (head === 'tini' || head === 'dumb-init') {
374
+ return initWrapperArgs(head, tokens, acc) ?? [];
375
+ }
376
+ if (head === 'env') {
377
+ let index = 0;
378
+ while (index < rest.length &&
379
+ (ASSIGNMENT.test(rest[index] ?? '') || (rest[index] ?? '').startsWith('-'))) {
380
+ index += 1;
381
+ }
382
+ return rest.slice(index);
383
+ }
384
+ return null;
385
+ }
386
+ /** Запуск через раннер (§3.8 v1.10): pnpm/npm/yarn start*, run start*, nest start, tsx, ts-node, pm2*, nodemon. */
387
+ function runnerForm(tokens) {
388
+ const head = basename(tokens[0] ?? '');
389
+ if (PACKAGE_MANAGERS.has(head)) {
390
+ return tokens.slice(1).some((token) => /^start([:-].*)?$/.test(token));
391
+ }
392
+ if (head === 'nest') {
393
+ return tokens[1] === 'start';
394
+ }
395
+ return RUNNERS.has(head) || head.startsWith('pm2');
396
+ }
397
+ /** `node --run start*` — раннер скриптов package.json. */
398
+ function nodeRunsScript(tokens) {
399
+ return tokens.some((token, index) => (token === '--run' && /^start/.test(tokens[index + 1] ?? '')) ||
400
+ /^--run=start/.test(token));
401
+ }
402
+ function analyzeSimple(input, acc) {
403
+ let tokens = input.map((token) => expandDefaults(token));
404
+ while (tokens.length > 0 && ASSIGNMENT.test(tokens[0] ?? '')) {
405
+ tokens = tokens.slice(1);
406
+ }
407
+ for (;;) {
408
+ const head = basename(tokens[0] ?? '');
409
+ if (head.includes('$')) {
410
+ unrecognized(acc, 'переменная без умолчания', input);
411
+ return;
412
+ }
413
+ if (ARGUMENT_WRAPPERS.has(head)) {
414
+ unrecognized(acc, `обёртка с аргументами ${head}`, input);
415
+ return;
416
+ }
417
+ if (head === 'eval') {
418
+ unrecognized(acc, 'eval', input);
419
+ return;
420
+ }
421
+ if (PACKAGE_MANAGERS.has(head) && (tokens[1] === 'exec' || tokens[1] === 'dlx')) {
422
+ tokens = tokens.slice(2);
423
+ continue;
424
+ }
425
+ if (head === 'npx' || head === 'pnpx') {
426
+ tokens = tokens.slice(1);
427
+ while (tokens[0]?.startsWith('-') === true) {
428
+ tokens = tokens.slice(1);
429
+ }
430
+ continue;
431
+ }
432
+ const unwrapped = unwrap(tokens, acc);
433
+ if (unwrapped === null) {
434
+ break;
435
+ }
436
+ tokens = unwrapped;
437
+ }
438
+ if (tokens.length === 0) {
439
+ return;
440
+ }
441
+ const call = shellCall(tokens);
442
+ if (call.kind === 'script') {
443
+ analyzeInto(call.script, acc);
444
+ return;
445
+ }
446
+ if (call.kind === 'unparsed') {
447
+ unrecognized(acc, 'вызов оболочки не распознан', input);
448
+ return;
449
+ }
450
+ const head = basename(tokens[0] ?? '');
451
+ if (runnerForm(tokens)) {
452
+ unrecognized(acc, `раннер ${head}`, input);
453
+ return;
454
+ }
455
+ if (head !== 'node') {
456
+ // main.* в аргументах любой команды — запуск, который правило не распознаёт (`./docker-entrypoint.sh node dist/main.js`).
457
+ if (APP_ENTRY.test(head)) {
458
+ unrecognized(acc, 'файл входа main.* на месте программы', input);
459
+ }
460
+ else if (tokens.some((token) => APP_ENTRY.test(basename(token)))) {
461
+ unrecognized(acc, `файл входа main.* в аргументах ${head}`, input);
462
+ }
463
+ return;
464
+ }
465
+ if (nodeRunsScript(tokens)) {
466
+ unrecognized(acc, 'раннер node --run', input);
467
+ return;
468
+ }
469
+ const entry = nodeEntryAt(tokens);
470
+ // Переменная без умолчания — среди опций node до файла входа и в самом файле входа; после него не проверяется.
471
+ if (tokens
472
+ .slice(0, entry === null ? tokens.length : entry.index + 1)
473
+ .some((token) => token.includes('$'))) {
474
+ unrecognized(acc, 'переменная без умолчания', input);
475
+ return;
476
+ }
477
+ const entryName = entry === null ? '' : basename(entry.entry);
478
+ if (APP_ENTRY.test(entryName)) {
479
+ acc.launches.push({ tokens, command: input.join(' ') });
480
+ return;
481
+ }
482
+ if (FAKE_ENTRY.test(entryName)) {
483
+ acc.fake = true;
484
+ return;
485
+ }
486
+ if (tokens.slice(1).some((token) => APP_ENTRY.test(basename(token)))) {
487
+ unrecognized(acc, 'файл входа main.* не на месте первого аргумента node', input);
488
+ }
489
+ }
490
+ function analyzeInto(input, acc) {
491
+ if (typeof input === 'string' && /\$\(|`/.test(input)) {
492
+ unrecognized(acc, 'подстановка команд $(…) или `…`', [input]);
493
+ }
494
+ for (const tokens of typeof input === 'string' ? shellCommands(input) : [[...input]]) {
495
+ analyzeSimple(tokens, acc);
496
+ }
497
+ }
498
+ /**
499
+ * Разбор команды запуска (argv exec-формы или shell-строка): запуски приложения, подделки и нераспознанные формы
500
+ * (§3.8 v1.10). `--import` засчитывается только в аргументах node, NODE_OPTIONS не учитывается.
501
+ */
502
+ export function analyzeLaunch(input) {
503
+ const acc = { launches: [], fake: false, unrecognized: [] };
504
+ analyzeInto(input, acc);
505
+ return acc;
506
+ }
507
+ /** У запуска приложения есть `--import` со значением, где есть `instrument`. */
508
+ export function hasInstrumentImport(tokens) {
509
+ return (nodeEntry(tokens)?.imports ?? []).some((value) => value.includes('instrument'));
510
+ }
@@ -0,0 +1,26 @@
1
+ import type { z } from 'zod';
2
+ import type { DbTx } from '../database/db.js';
3
+ import type { KitEvent } from '../messaging/define-event.js';
4
+ /** @internal Каталог прогона в процессе globalSetup (startTestStack). */
5
+ export declare function setCoverageDir(dir: string | undefined): void;
6
+ /** @internal Каталог покрытия воркера вместо `inject('kitOutboxCoverageDir')` (тесты пакета). */
7
+ export declare function setWorkerCoverageDir(dir: string | undefined): void;
8
+ /**
9
+ * Проверяет запись события в **текущей** транзакции `tx` (строки других транзакций не учитываются): строка
10
+ * `<schema>.outbox` с типом и версией события есть, `data` проходит схему события и (если задано) совпадает с
11
+ * `expected.data` (`toMatchObject`), `subjectId` (если задан) совпадает. После успешной проверки ключ `<type>.v<version>`
12
+ * дописывается в файл покрытия воркера (`assertOutboxCoverage`).
13
+ * @throws утверждение vitest — строки нет, `data` не по схеме или не совпала.
14
+ * @throws {TypeError} `startTestStack` без `postgres`.
15
+ */
16
+ export declare function expectOutbox<E extends KitEvent>(tx: DbTx, event: E, expected?: {
17
+ readonly subjectId?: string;
18
+ readonly data?: Partial<z.input<E['data']>>;
19
+ }): Promise<void>;
20
+ /**
21
+ * Падает со списком событий без успешного `expectOutbox` за прогон (GAP-2): объединяет файлы воркеров каталога
22
+ * прогона. Вызывает teardown `startTestStack({outboxEvents})` при `KIT_OUTBOX_COVERAGE=1`.
23
+ * @throws {TypeError} вызов вне teardown `startTestStack({outboxEvents})`.
24
+ * @throws {Error} «assertOutboxCoverage: события без успешного expectOutbox: <ключи>».
25
+ */
26
+ export declare function assertOutboxCoverage(events: readonly KitEvent[]): void;