@nitra/cursor 1.13.57 → 1.13.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,11 +4,17 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.13.58] - 2026-05-20
8
+
9
+ ### Added
10
+
11
+ - `check security`: новий concern **`security.sample_secret`** — placeholder фейкових credential-значень у прикладних файлах має бути `sample-secret`, а не bare `secret`. Причина: `sample-secret` містить підрядок `sample` із вшитого списку `DefaultFalsePositives` TruffleHog і відсіюється сканером гарантовано та незалежно від версії; bare `secret` наразі ігнорується лише тому, що випадково присутнє у словнику `fp_words.txt` — крихка, версієзалежна поведінка. [check.mjs](rules/security/fix/sample_secret/check.mjs) обходить дерево, відбирає прикладні файли (basename із суфіксом `.example`/`.sample`/`.template`/`.dist` чи infix `.example.`/`.sample.`/`.template.`, а також усе всередині каталогів `fixtures`/`fixture`/`__fixtures__`) і порядково шукає `secret` у позиції значення — одразу після `=`, `:` або `=>` з опційними лапками; імена ключів (`client_secret`, `JWT_SECRET`) не чіпаються, бо матч прив'язаний до значення. Решта файлів не сканується — там `secret` майже завжди частина реального коду. Скан текстовий (regex, не AST/Rego): прикладні файли — різнорідні конфіги (`.env`, YAML, JSON, TOML, plain `.dist`) без єдиного AST, а відбір файлів потребує обходу дерева. Зачеплено: [check.mjs](rules/security/fix/sample_secret/check.mjs) і [check.test.mjs](rules/security/fix/sample_secret/check.test.mjs) (новий concern + 9 тестів), [security.mdc](rules/security/security.mdc) (нова секція «Placeholder для секретів — `sample-secret`» та секція «Перевірка»). Bump `security.mdc` `2.0` → `2.1`.
12
+
7
13
  ## [1.13.57] - 2026-05-19
8
14
 
9
15
  ### Changed
10
16
 
11
- - `check js-bun-db`: новий **hard fail** на `sql.unsafe(template_literal_with_interpolation)` — будь-який виклик з template-літералом, що містить `${...}`-інтерполяцію, тепер падає **навіть з маркером** `// allow-unsafe`. Причина: шаблонна підстановка `${name}` у `sql.unsafe`-рядок не екранує identifier'ів (reserved words, спецсимволи, пробіли в імені) і не біндить значень; такий код виглядає звично через знайому tagged-template-форму, але насправді робить просту строкову конкатенацію без жодних гарантій. Канон — зібрати `text` окремо: identifiers через `@scaleleap/pg-format` `format('%I', name)`, values як позиційні `$N` + другий аргумент `sql.unsafe(text, [params])`. Раніше дозволений приклад `sql.unsafe(\\\`CREATE TABLE \\\${TABLE} (id int)\\\`)` з marker'ом тепер fail — переписати через `format('CREATE TABLE %I (id int)', TABLE)`. Не зачепило: `sql.unsafe('SELECT 1')` (статичний рядок), `sql.unsafe(\\\`SELECT 1\\\`)` (template без інтерполяції), `sql.unsafe(text, [params])` зі змінною `text`. Зачеплено: [bun-sql-scan.mjs](scripts/utils/bun-sql-scan.mjs) (новий експорт `findBunSqlUnsafeWithInterpolatedTemplateInText`, що флагає лише `<obj>.unsafe(TemplateLiteral)` з `expressions.length > 0`), [check.mjs](rules/js-bun-db/fix/safety/check.mjs) (новий лічильник `unsafeTemplateInterp` + окреме повідомлення з порадою на `@scaleleap/pg-format`), [check.test.mjs](rules/js-bun-db/fix/safety/check.test.mjs) (попередній DDL-тест переписано на безпечний `format('%I', ...)`-варіант, додано **негативний** тест на template-interp + marker і **позитивний** тест на статичний template без інтерполяції), [js-bun-db.mdc](rules/js-bun-db/js-bun-db.mdc) (нова підсекція «sql.unsafe з template-літералом і ${...}-інтерполяцією — заборонено навіть з маркером» зі зразками поганого/гарного коду; основний приклад DDL у секції unsafe-allowlist переписано на `format` + готовий `text`). Bump `js-bun-db.mdc` `1.10` → `1.11`.
17
+ - `check js-bun-db`: новий **hard fail** на `sql.unsafe(template_literal_with_interpolation)` — будь-який виклик з template-літералом, що містить `${...}`-інтерполяцію, тепер падає **навіть з маркером** `// allow-unsafe`. Причина: шаблонна підстановка `${name}` у `sql.unsafe`-рядок не екранує identifier'ів (reserved words, спецсимволи, пробіли в імені) і не біндить значень; такий код виглядає звично через знайому tagged-template-форму, але насправді робить просту строкову конкатенацію без жодних гарантій. Канон — зібрати `text` окремо: identifiers через `@scaleleap/pg-format` `format('%I', name)`, values як позиційні `$N` + другий аргумент `sql.unsafe(text, [params])`. Раніше дозволений приклад `sql.unsafe(\\\`CREATE TABLE \\\${TABLE} (id int)\\\`) marker'ом тепер fail — переписати через`format('CREATE TABLE %I (id int)', TABLE)`. Не зачепило:`sql.unsafe('SELECT 1')`(статичний рядок),`sql.unsafe(\\\`SELECT 1\\\`)`(template без інтерполяції),`sql.unsafe(text, [params])`зі змінною`text`. Зачеплено: [bun-sql-scan.mjs](scripts/utils/bun-sql-scan.mjs) (новий експорт`findBunSqlUnsafeWithInterpolatedTemplateInText`, що флагає лише`obj.unsafe(TemplateLiteral)`з`expressions.length > 0`), [check.mjs](rules/js-bun-db/fix/safety/check.mjs) (новий лічильник`unsafeTemplateInterp`+ окреме повідомлення з порадою на`@scaleleap/pg-format`), [check.test.mjs](rules/js-bun-db/fix/safety/check.test.mjs) (попередній DDL-тест переписано на безпечний`format('%I', ...)`-варіант, додано **негативний** тест на template-interp + marker і **позитивний** тест на статичний template без інтерполяції), [js-bun-db.mdc](rules/js-bun-db/js-bun-db.mdc) (нова підсекція «sql.unsafe з template-літералом і ${...}-інтерполяцією — заборонено навіть з маркером» зі зразками поганого/гарного коду; основний приклад DDL у секції unsafe-allowlist переписано на`format`+ готовий`text`). Bump`js-bun-db.mdc` `1.10`→`1.11`.
12
18
 
13
19
  ## [1.13.56] - 2026-05-19
14
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.13.57",
3
+ "version": "1.13.58",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -53,8 +53,8 @@ async function loadNCursorRules() {
53
53
  */
54
54
  function lintChainHasScript(lintScript, target) {
55
55
  if (!lintScript) return false
56
- const escaped = target.replaceAll(/[.*+?^${}()|[\]\\]/gu, '\\$&')
57
- return new RegExp(`(?:^|\\s)bun\\s+run\\s+${escaped}(?:$|\\s)`, 'u').test(lintScript)
56
+ const escaped = target.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`)
57
+ return new RegExp(String.raw`(?:^|\s)bun\s+run\s+${escaped}(?:$|\s)`, 'u').test(lintScript)
58
58
  }
59
59
 
60
60
  /**
@@ -74,6 +74,17 @@ const RULE_SCRIPTS = [
74
74
  { rules: ['image-avif', 'image-compress'], script: 'lint-image', doc: 'image-avif.mdc / image-compress.mdc' }
75
75
  ]
76
76
 
77
+ /**
78
+ * Загортає кожен ідентифікатор у backticks та зʼєднує через роздільник. Винесено
79
+ * окремою функцією, щоб не нестити template literals у `pass`/`fail`-повідомленнях.
80
+ * @param {string[]} items ідентифікатори правил
81
+ * @param {string} sep роздільник (наприклад `, ` або `/`)
82
+ * @returns {string} рядок виду "`a`, `b`"
83
+ */
84
+ function backtickJoin(items, sep) {
85
+ return items.map(r => '`' + r + '`').join(sep)
86
+ }
87
+
77
88
  /**
78
89
  * Описує стан правил-власників скрипта для повідомлень про reason. Повертає або список увімкнених
79
90
  * правил (для passing-кейсу «правило є»), або компактний опис, чому всі вимкнені (для inverse-fail).
@@ -84,7 +95,7 @@ const RULE_SCRIPTS = [
84
95
  function ownerStatus(owners, cursorRules) {
85
96
  const enabled = owners.filter(r => cursorRules.rules.has(r))
86
97
  if (enabled.length > 0) {
87
- return { enabled, reason: `правил${enabled.length === 1 ? 'о' : 'а'} ${enabled.map(r => `\`${r}\``).join(', ')}` }
98
+ return { enabled, reason: `правил${enabled.length === 1 ? 'о' : 'а'} ${backtickJoin(enabled, ', ')}` }
88
99
  }
89
100
  if (owners.length === 1) {
90
101
  const [only] = owners
@@ -93,7 +104,7 @@ function ownerStatus(owners, cursorRules) {
93
104
  }
94
105
  const disabledCount = owners.filter(r => cursorRules.disabled.has(r)).length
95
106
  const note = disabledCount === owners.length ? 'усі власники в disable-rules' : 'жоден власник не активний у rules'
96
- return { enabled, reason: `${owners.map(r => `\`${r}\``).join('/')} — ${note}` }
107
+ return { enabled, reason: `${backtickJoin(owners, '/')} — ${note}` }
97
108
  }
98
109
 
99
110
  /**
@@ -104,7 +115,7 @@ function ownerStatus(owners, cursorRules) {
104
115
  * вимкненому правилі: `n-cursor lint-<id>` ігнорує `.n-cursor.json` і обходить дерево
105
116
  * незалежно від конфігу (як було в cursor-репо: `disable-rules: ["k8s"]` + залишений `lint-k8s`
106
117
  * ламав chain на template-сорцях власного правила).
107
- * @param {{ pass: (msg: string) => void, fail: (msg: string) => void }} reporter
118
+ * @param {{ pass: (msg: string) => void, fail: (msg: string) => void }} reporter callback-и `pass`/`fail` для звіту
108
119
  * @param {Record<string, string>} scripts scripts з package.json
109
120
  * @param {{ rules: Set<string>, disabled: Set<string> }} cursorRules `rules` та `disable-rules`
110
121
  */
@@ -127,7 +138,7 @@ function checkCursorRuleScripts(reporter, scripts, cursorRules) {
127
138
  }
128
139
  if (present) {
129
140
  fail(
130
- `У .n-cursor.json немає активних власників ${owners.map(r => `\`${r}\``).join('/')} — прибери скрипт \`${script}\` з кореневого package.json (див. ${doc})`
141
+ `У .n-cursor.json немає активних власників ${backtickJoin(owners, '/')} — прибери скрипт \`${script}\` з кореневого package.json (див. ${doc})`
131
142
  )
132
143
  }
133
144
  if (inChain) {
@@ -53,6 +53,12 @@ import { findAllPackageJsonPaths } from '../../../../scripts/utils/find-package-
53
53
  import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
54
54
  import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
55
55
 
56
+ // Дешеві pre-filter regex'и для AST-сканера LISTEN/NOTIFY: уникаємо парсингу
57
+ // файлів, у яких ніяких сигналів немає. Винесено в модульний скоуп, щоб не
58
+ // перекомпілювати RegExp на кожному виклику `collectPgUsageForFile`.
59
+ const LISTEN_NOTIFY_KEYWORD_RE = /\b(LISTEN|UNLISTEN|NOTIFY)\b/iu
60
+ const NOTIFICATION_LITERAL_RE = /['"`]notification['"`]/u
61
+
56
62
  /**
57
63
  * Збирає абсолютні шляхи JS/TS джерел у репозиторії для скану Bun SQL патернів.
58
64
  * @param {string} repoRoot абсолютний шлях до кореня репозиторію
@@ -138,7 +144,7 @@ function collectPgUsageForFile(content, rel, pgUsage) {
138
144
  // Дешевий pre-filter за текстом: AST-парсинг тільки коли файл містить
139
145
  // або імпорт `'pg'`, або хоча б одне зі слів LISTEN / NOTIFY / UNLISTEN /
140
146
  // 'notification' — інакше LISTEN/NOTIFY у ньому точно немає.
141
- const mayHaveListenNotify = /\b(LISTEN|UNLISTEN|NOTIFY)\b/iu.test(content) || /['"`]notification['"`]/u.test(content)
147
+ const mayHaveListenNotify = LISTEN_NOTIFY_KEYWORD_RE.test(content) || NOTIFICATION_LITERAL_RE.test(content)
142
148
  if (!textHasPgLibImport(content) && !mayHaveListenNotify) return
143
149
  const imports = findPgLibImportInText(content, rel)
144
150
  const listenNotify = findPgListenNotifyUsageInText(content, rel)
@@ -263,7 +269,7 @@ async function checkPgDependencyAndUsage(pkgJsonPaths, repoRoot, pgUsage, report
263
269
  }
264
270
  if (!pkg || typeof pkg !== 'object') continue
265
271
  const deps = pkg.dependencies
266
- if (!deps || typeof deps !== 'object' || !Object.prototype.hasOwnProperty.call(deps, 'pg')) continue
272
+ if (!deps || typeof deps !== 'object' || !Object.hasOwn(deps, 'pg')) continue
267
273
  pgDepsFound++
268
274
  if (!hasAnyListenNotify) {
269
275
  pgDepFails++
@@ -397,7 +403,7 @@ export async function check() {
397
403
  }
398
404
  if (unsafeTemplateInterp === 0) {
399
405
  pass(
400
- 'js-bun-db: немає sql.unsafe(`...${x}...`) з template-інтерполяцією ' +
406
+ 'js-bun-db: немає sql.unsafe(template literal з інтерполяцією) ' +
401
407
  '(identifiers через @scaleleap/pg-format %I, values — позиційні $N)'
402
408
  )
403
409
  }
@@ -355,7 +355,7 @@ await sql.unsafe('SELECT pg_advisory_lock($1)', [lockId]) // allow-unsafe: pg_ad
355
355
 
356
356
  ### `sql.unsafe` з template-літералом і `${...}`-інтерполяцією — заборонено навіть з маркером
357
357
 
358
- `sql.unsafe(\`...\${x}...\`)` — окремий **hard fail**, який не знімається маркером `// allow-unsafe`. Шаблонна підстановка `${x}` у `sql.unsafe`-рядок:
358
+ `sql.unsafe(\`...\${x}...\`)` — окремий **hard fail**, який не знімається маркером `// allow-unsafe`. Шаблонна підстановка`${x}` у `sql.unsafe`-рядок:
359
359
 
360
360
  - **не екранує** identifier'ів (reserved words, спецсимволи, пробіли в імені);
361
361
  - **не біндить** значень (вони потрапляють у запит сирим текстом, як injection-вектор);
@@ -379,7 +379,7 @@ const query = format('CREATE TABLE %I (id int)', tableName)
379
379
  await sql.unsafe(query)
380
380
  ```
381
381
 
382
- Статичні `sql.unsafe(\`SELECT 1\`)` (без `${...}`) і `sql.unsafe(text, [params])` зі змінною `text`, зібраною заздалегідь, — допустимі (за наявності `// allow-unsafe`-маркера).
382
+ Статичні `sql.unsafe(\`SELECT 1\`)` (без `${...}`) і`sql.unsafe(text, [params])` зі змінною `text`, зібраною заздалегідь, — допустимі (за наявності`// allow-unsafe`-маркера).
383
383
 
384
384
  ❌ Заборонені кейси (треба переробити на tagged template):
385
385
 
@@ -0,0 +1,105 @@
1
+ /**
2
+ * FS-частина правила `security`: concern `sample_secret`.
3
+ *
4
+ * Перевіряє, що фейкові credential-значення у *прикладних* файлах записані як
5
+ * канонічний placeholder `sample-secret`, а не як bare `secret`.
6
+ *
7
+ * `sample-secret` містить підрядок `sample`, який є у вшитому списку
8
+ * `DefaultFalsePositives` TruffleHog — таке значення сканер відсіює
9
+ * гарантовано й незалежно від версії. Bare `secret` наразі не репортиться
10
+ * лише тому, що випадково присутнє у словнику `fp_words.txt`; це крихка,
11
+ * версієзалежна поведінка, на яку не варто покладатися.
12
+ *
13
+ * Прикладними вважаються файли, чий basename має суфікс `.example` / `.sample`
14
+ * / `.template` / `.dist` або infix `.example.` / `.sample.` / `.template.`, а
15
+ * також будь-які файли всередині каталогів `fixtures` / `fixture` /
16
+ * `__fixtures__`. Решта файлів не сканується — там `secret` майже завжди
17
+ * частина реального коду, а не placeholder.
18
+ *
19
+ * Порушенням є лише `secret` у *позиції значення* — одразу після `=`, `:` чи
20
+ * `=>` (з опційними лапками). Імена ключів (`client_secret`, `JWT_SECRET`) не
21
+ * чіпаються: матч прив'язаний до значення, не до ключа.
22
+ *
23
+ * Чому regex, а не AST: прикладні файли — різнорідні конфіги (`.env`, YAML,
24
+ * JSON, TOML, plain `.dist`), єдиного AST для них немає, тож скан порядковий.
25
+ * Чому JS, а не Rego: щоб знайти прикладні файли, треба обійти дерево
26
+ * (`readdir`), а вміст — неструктурований текст (conftest парсить лише
27
+ * структуровані документи).
28
+ */
29
+ import { readFile } from 'node:fs/promises'
30
+ import { relative, sep } from 'node:path'
31
+
32
+ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
33
+ import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
34
+
35
+ /** Суфікс basename'а прикладного файлу (`config.example`, `.env.dist`). */
36
+ const EXAMPLE_SUFFIX_RE = /\.(?:example|sample|template|dist)$/iu
37
+
38
+ /** Infix у basename'і (`docker-compose.example.yml`, `app.config.sample.json`). */
39
+ const EXAMPLE_INFIX_RE = /\.(?:example|sample|template)\./iu
40
+
41
+ /** Сегмент шляху з фікстурами (`fixtures/`, `fixture/`, `__fixtures__/`). */
42
+ const FIXTURE_DIR_RE = /(?:^|\/)(?:__fixtures__|fixtures?)(?:\/|$)/u
43
+
44
+ /**
45
+ * Bare-`secret` у позиції значення: після `=`, `:` або `=>` (опційні лапки), а
46
+ * далі лише пробіли / завершальна пунктуація / коментар до кінця рядка. Прив'язка
47
+ * до `$` гарантує, що `secret` — увесь токен значення (`secret-key`, `secretValue`
48
+ * не матчаться); прив'язка до `[:=]` відсікає імена ключів (`client_secret`).
49
+ * Регістронезалежно.
50
+ */
51
+ const VALUE_SECRET_RE = /[:=]>?\s*(['"]?)secret\1[\s,;}\])]*(?:(?:#|\/\/).*)?$/iu
52
+
53
+ /**
54
+ * Чи є файл «прикладним» — таким, де `secret` очікувано є placeholder'ом.
55
+ * @param {string} relPosix відносний шлях від cwd у posix-форматі
56
+ * @returns {boolean} `true`, якщо файл треба сканувати
57
+ */
58
+ function isExampleFile(relPosix) {
59
+ const base = relPosix.slice(relPosix.lastIndexOf('/') + 1)
60
+ return EXAMPLE_SUFFIX_RE.test(base) || EXAMPLE_INFIX_RE.test(base) || FIXTURE_DIR_RE.test(relPosix)
61
+ }
62
+
63
+ /**
64
+ * @returns {Promise<number>} exit-код перевірки (0 — OK, 1 — є bare `secret`)
65
+ */
66
+ export async function check() {
67
+ const reporter = createCheckReporter()
68
+ const { pass, fail } = reporter
69
+ const cwd = process.cwd()
70
+
71
+ /** @type {Array<{ abs: string, rel: string }>} */
72
+ const examples = []
73
+ await walkDir(cwd, abs => {
74
+ const rel = relative(cwd, abs).split(sep).join('/')
75
+ if (isExampleFile(rel)) examples.push({ abs, rel })
76
+ })
77
+ examples.sort((a, b) => a.rel.localeCompare(b.rel))
78
+
79
+ if (examples.length === 0) {
80
+ pass('прикладних файлів не знайдено — placeholder перевіряти нема де')
81
+ return reporter.getExitCode()
82
+ }
83
+
84
+ let violations = 0
85
+ for (const { abs, rel } of examples) {
86
+ let content
87
+ try {
88
+ content = await readFile(abs, 'utf8')
89
+ } catch {
90
+ continue
91
+ }
92
+ const lines = content.split('\n')
93
+ for (let i = 0; i < lines.length; i++) {
94
+ const line = lines[i].endsWith('\r') ? lines[i].slice(0, -1) : lines[i]
95
+ if (!VALUE_SECRET_RE.test(line)) continue
96
+ violations++
97
+ fail(`${rel}:${i + 1}: \`${line.trim()}\` — заміни placeholder \`secret\` на \`sample-secret\` (security.mdc)`)
98
+ }
99
+ }
100
+
101
+ if (violations === 0) {
102
+ pass(`прикладні файли (${examples.length}) не містять bare \`secret\``)
103
+ }
104
+ return reporter.getExitCode()
105
+ }
@@ -1,8 +1,8 @@
1
1
  ---
2
- description: Локальний та CI-секюріті-лінт через TruffleHog — скрипт `lint-security`, `.trufflehog-exclude`, інтеграція в агрегований `lint`
2
+ description: Локальний та CI-секюріті-лінт через TruffleHog — скрипт `lint-security`, `.trufflehog-exclude`, інтеграція в агрегований `lint`; канонічний placeholder `sample-secret` у прикладних файлах
3
3
  globs: "**/.trufflehog-exclude,**/package.json,**/.github/workflows/**/*.yml"
4
4
  alwaysApply: false
5
- version: '2.0'
5
+ version: '2.1'
6
6
  ---
7
7
 
8
8
  [TruffleHog](https://github.com/trufflesecurity/trufflehog) — глобальний CLI (як `shellcheck`, `conftest`); **не** додавай до `dependencies`/`devDependencies`.
@@ -35,3 +35,18 @@ Workflow обовʼязковий — забезпечує незалежний
35
35
  - Канон: [lint-security.yml.snippet.yml](./policy/lint_security_yml/template/lint-security.yml.snippet.yml)
36
36
 
37
37
  Перевіряється policy `security.lint_security_yml`: серед `uses:` має бути крок з `trufflesecurity/trufflehog@main`. Універсальні workflow-перевірки (checkout, permissions, persist-credentials) — у `ga.workflow_common`. Для повного скану історії потрібен `fetch-depth: 0`.
38
+
39
+ ## Placeholder для секретів — `sample-secret`
40
+
41
+ Фейкові credential-значення у **прикладних файлах** (`.env.example`, `.env.dist`, `*.example`, `*.sample`, `*.template`, вміст каталогів `fixtures/`) пиши як `sample-secret`, а не як bare `secret`.
42
+
43
+ `sample-secret` містить підрядок `sample` із вшитого списку `DefaultFalsePositives` TruffleHog — таке значення сканер відсіює **гарантовано** й незалежно від версії. Bare `secret` наразі не репортиться лише тому, що випадково присутнє у словнику `fp_words.txt`; це крихка, версієзалежна поведінка.
44
+
45
+ - Правильно: `DB_PASSWORD=sample-secret`, `password: "sample-secret"`
46
+ - Неправильно: `DB_PASSWORD=secret`, `password: "secret"`
47
+
48
+ Перевіряється лише `secret` у позиції значення (після `=`, `:`, `=>`); імена ключів на кшталт `client_secret` не чіпаються. Concern `security.sample_secret` — деталі скану в `fix/sample_secret/check.mjs`.
49
+
50
+ ## Перевірка
51
+
52
+ `npx @nitra/cursor check security`
@@ -784,9 +784,10 @@ export function findPgLibImportInText(content, virtualPath = 'scan.ts') {
784
784
  * `LISTEN ` / `UNLISTEN ` / `NOTIFY ` (case-insensitive);
785
785
  * - `<obj>.on('notification', ...)` — pg-listener notification-подій (другий
786
786
  * аргумент — функція; перший — точно рядок `'notification'`);
787
- * - TaggedTemplateExpression виду `sql\`LISTEN ...\`` на випадок, якщо хтось
788
- * використовує Bun SQL-tagged-template, а LISTEN/NOTIFY все одно лишається у
789
- * тексті запиту (це не запрацює у Bun SQL, але як сигнал — приймаємо).
787
+ * - TaggedTemplateExpression виду sql tagged template з LISTEN/UNLISTEN/NOTIFY
788
+ * на початку першого quasi на випадок, якщо хтось використовує Bun
789
+ * SQL-tagged-template, а LISTEN/NOTIFY все одно лишається у тексті запиту
790
+ * (це не запрацює у Bun SQL, але як сигнал — приймаємо).
790
791
  *
791
792
  * Регістр SQL-слів не важливий, провідні пробіли допускаються.
792
793
  * @param {string} content вихідний код
@@ -873,8 +874,8 @@ function listenNotifyFromCallExpression(node) {
873
874
  }
874
875
 
875
876
  /**
876
- * Аналізує TaggedTemplateExpression `<tag>\`LISTEN ...\``: якщо перший quasi
877
- * починається з LISTEN/UNLISTEN/NOTIFY — повертає відповідний kind.
877
+ * Аналізує TaggedTemplateExpression: якщо перший quasi починається з
878
+ * LISTEN/UNLISTEN/NOTIFY — повертає відповідний kind.
878
879
  * @param {Record<string, unknown>} node AST node
879
880
  * @returns {'listen_sql' | 'notify_sql' | 'unlisten_sql' | null} kind знахідки
880
881
  */