@nitra/cursor 1.8.159 → 1.8.160

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,6 +4,14 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.8.160] - 2026-05-01
8
+
9
+ ### Changed
10
+
11
+ - `js-bun-db.mdc` (v1.4): `sql.unsafe(...)` тепер заборонено за замовчуванням — допустимо лише для підстановки назви таблиці/колонки чи dynamic SQL/DDL з code-controlled значенням; інакше переробляємо на tagged template `sql\`...${value}...\``. Кожен легітимний виклик має супроводжуватись маркером `// allow-unsafe: <причина>` на тому ж рядку або рядком вище.
12
+ - `check-js-bun-db.mjs`: замість вузької перевірки `sql.unsafe(\`...${expr}...\`)` тепер сканер `findBunSqlUnsafeUseWithoutAllowMarkerInText` падає на будь-якому `<obj>.unsafe(...)` без маркера-коментаря з непорожньою причиною (line- або block-коментар на тому ж рядку чи безпосередньо перед викликом).
13
+ - `ast-scan-utils.mjs`: додано `parseProgramAndCommentsOrNull` — окремий вхід для перевірок, яким потрібні коментарі поряд з AST.
14
+
7
15
  ## [1.8.159] - 2026-05-01
8
16
 
9
17
  ### Added
package/mdc/js-bun-db.mdc CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Використання pg / mysql2 / Bun SQL у Node.js та Bun
3
3
  alwaysApply: true
4
- version: '1.3'
4
+ version: '1.4'
5
5
  ---
6
6
 
7
7
  ## Підтримувані версії баз даних
@@ -116,22 +116,43 @@ await sql.begin(async tx => {
116
116
  await sql`SELECT * FROM users WHERE id IN ${sql(ids)}`
117
117
  ```
118
118
 
119
- ## Що НЕ робити
119
+ ## `sql.unsafe(...)` за замовчуванням заборонено
120
+
121
+ Будь-який виклик `sql.unsafe(...)` (так само `tx.unsafe(...)` всередині `sql.begin`) **заборонено**, окрім випадків, коли **обидві** умови виконані:
122
+
123
+ 1. значення підставляється з **коду** — константа, конфіг, whitelist; **не з user input**;
124
+ 2. треба підставити те, що **не можна параметризувати** через tagged template:
125
+ - назву **таблиці**,
126
+ - назву **колонки**,
127
+ - **dynamic SQL / DDL** (`CREATE`, `ALTER`, `DROP`, multi-statement migration, серверні `SET`/`SHOW` і подібне).
128
+
129
+ В усіх інших випадках — переробити на звичайний tagged template `sql\`...\${value}...\``: значення біндяться як параметри й injection не лишається.
120
130
 
121
- ### Не використовувати `sql.unsafe(...)` з конкатенацією
131
+ Кожен легітимний `sql.unsafe(...)` має супроводжуватись **маркером-коментарем** з причиною — на тому ж рядку (trailing) або на рядку безпосередньо перед викликом. Маркер — opt-in для перевірки `js-bun-db` і слід для ревʼюера:
122
132
 
123
133
  ```javascript
124
- // конкатенація даних у SQL SQL injection
134
+ // allow-unsafe: DDL назву таблиці параметризувати не можна
135
+ await sql.unsafe(`CREATE TABLE ${tableName} (id int)`)
136
+
137
+ await sql.unsafe('SELECT pg_advisory_lock($1)', [lockId]) // allow-unsafe: pg_advisory_lock — окремий шлях, без tagged template
138
+ ```
139
+
140
+ Формат маркера: `allow-unsafe: <непорожня причина>` у line- або block-коментарі. Без причини (`// allow-unsafe:`) і без маркера взагалі — **fail** перевірки.
141
+
142
+ ❌ Заборонені кейси (треба переробити на tagged template):
143
+
144
+ ```javascript
145
+ // ❌ дані від користувача — параметризуй через tagged template
125
146
  await sql.unsafe(`SELECT * FROM users WHERE id = ${userId}`)
126
147
 
127
148
  // ❌ навіть у tagged template — динамічний список через .join(',')
128
149
  await sql`SELECT * FROM users WHERE id IN (${ids.join(',')})`
129
150
  ```
130
151
 
131
- `sql.unsafe(text, params)` допустимий лише для **статичного** SQL без даних від користувача (наприклад, разовий DDL-скрипт), і обов'язково з масивом параметрів — ніяких `${...}` у самому рядку.
132
-
133
152
  Для динамічних списків — `sql([...])` або `sql(rows, 'colA', 'colB')`, **не** `.join(',')`.
134
153
 
154
+ ## Що НЕ робити
155
+
135
156
  ### Не створювати підключення на кожен запит
136
157
 
137
158
  ```javascript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.159",
3
+ "version": "1.8.160",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -10,7 +10,11 @@
10
10
  * 2) Якщо в коді використовується Bun SQL (імпорт `sql`/`SQL` з `'bun'`), додатково
11
11
  * перевіряє небезпечні патерни:
12
12
  * - `new SQL(...)` всередині функції (пул має бути singleton на рівні модуля).
13
- * - `sql.unsafe(\`...${expr}...\`)` (інтерполяція даних у `unsafe` ламає параметризацію).
13
+ * - Будь-який `<obj>.unsafe(...)` без маркера-коментаря `// allow-unsafe: <reason>`
14
+ * на тому ж рядку або рядком вище. `sql.unsafe` за замовчуванням заборонено;
15
+ * допустимий лише для підстановки назви таблиці/колонки чи dynamic SQL/DDL,
16
+ * коли значення контролюється кодом (не user input) — в інших випадках
17
+ * переробляємо на tagged template `sql\`...\${value}...\``.
14
18
  * - Динамічні SQL-списки через `.join(',')` у `IN (...)` / `VALUES (...)`
15
19
  * (треба `sql([...])`).
16
20
  */
@@ -21,9 +25,9 @@ import { join, relative, sep } from 'node:path'
21
25
  import { createCheckReporter } from './utils/check-reporter.mjs'
22
26
  import {
23
27
  findBunSqlPerRequestConnectionInText,
28
+ findBunSqlUnsafeUseWithoutAllowMarkerInText,
24
29
  findUnsafeBunSqlDynamicSqlListInText,
25
30
  findUnsafeBunSqlInListMissingEmptyGuardInText,
26
- findUnsafeBunSqlUnsafeCallInText,
27
31
  isBunSqlScanSourceFile,
28
32
  textHasBunSqlImport
29
33
  } from './utils/bun-sql-scan.mjs'
@@ -155,11 +159,14 @@ function scanFileForBunSqlPatterns(content, rel, fail, counts) {
155
159
  `тримай singleton на рівні модуля (js-bun-db.mdc): ${v.snippet}`
156
160
  )
157
161
  }
158
- for (const v of findUnsafeBunSqlUnsafeCallInText(content, rel)) {
162
+ for (const v of findBunSqlUnsafeUseWithoutAllowMarkerInText(content, rel)) {
159
163
  counts.unsafeCall++
160
164
  fail(
161
- `js-bun-db: ${rel}:${v.line} — sql.unsafe(\`...\${...}...\`) недопустимо: ` +
162
- `використовуй tagged template sql\`...\${value}...\` або sql.unsafe('static', [params]) (js-bun-db.mdc): ${v.snippet}`
165
+ `js-bun-db: ${rel}:${v.line} — sql.unsafe(...) заборонено за замовчуванням; ` +
166
+ `допустимо лише для підстановки назви таблиці/колонки чи dynamic SQL/DDL з code-controlled значенням, ` +
167
+ `інакше переробити на tagged template sql\`...\${value}...\`. ` +
168
+ `Якщо випадок легітимний — додай маркер "// allow-unsafe: <причина>" на тому ж рядку або рядком вище ` +
169
+ `(js-bun-db.mdc): ${v.snippet}`
163
170
  )
164
171
  }
165
172
  for (const v of findUnsafeBunSqlDynamicSqlListInText(content, rel)) {
@@ -245,7 +252,7 @@ export async function check() {
245
252
  pass('js-bun-db: немає створення new SQL(...) всередині функцій (singleton на рівні модуля)')
246
253
  }
247
254
  if (unsafeCall === 0) {
248
- pass('js-bun-db: немає небезпечних викликів sql.unsafe з інтерполяцією в шаблонному рядку')
255
+ pass('js-bun-db: усі sql.unsafe(...) або відсутні, або супроводжуються маркером "// allow-unsafe: <причина>"')
249
256
  }
250
257
  if (dynamicList === 0) {
251
258
  pass("js-bun-db: немає небезпечних динамічних SQL-списків через .join(',') у IN/VALUES")
@@ -112,6 +112,29 @@ export function parseProgramOrNull(content, virtualPath) {
112
112
  return result.program
113
113
  }
114
114
 
115
+ /**
116
+ * Парсить файл і повертає `{ program, comments }` або null. Окремий вхід для перевірок,
117
+ * яким потрібні коментарі (наприклад, маркер `// allow-unsafe: ...` біля виклику) —
118
+ * базовий `parseProgramOrNull` свідомо лишається без коментарів, щоб не змінювати API.
119
+ * @param {string} content вихідний код
120
+ * @param {string} virtualPath шлях для вибору `lang` (також для діагностики)
121
+ * @returns {{ program: unknown, comments: { type: 'Line' | 'Block', value: string, start: number, end: number }[] } | null}
122
+ */
123
+ export function parseProgramAndCommentsOrNull(content, virtualPath) {
124
+ const lang = langFromPath(virtualPath || 'scan.ts')
125
+ let result
126
+ try {
127
+ result = parseSync(virtualPath || 'scan.ts', content, { lang, sourceType: 'module' })
128
+ } catch {
129
+ return null
130
+ }
131
+ if (result.errors?.length) return null
132
+ return {
133
+ program: result.program,
134
+ comments: Array.isArray(result.comments) ? result.comments : []
135
+ }
136
+ }
137
+
115
138
  /**
116
139
  * Чи це `.join(...)` виклик (типово для динамічних списків у SQL).
117
140
  * @param {unknown} node AST node
@@ -4,9 +4,11 @@
4
4
  * Знаходить:
5
5
  * - `new SQL(...)` всередині функції — пул має бути singleton на рівні модуля,
6
6
  * а не на кожен виклик handler-а.
7
- * - Виклик `sql.unsafe(\`...${expr}...\`)` з даними у TemplateLiteral
8
- * `sql.unsafe` приймає лише статичний SQL (плюс масив параметрів); інтерполяція
9
- * у текст руйнує параметризацію і відкриває SQL injection.
7
+ * - Будь-який виклик `<obj>.unsafe(...)` без маркера-коментаря `// allow-unsafe: <reason>`
8
+ * на тому ж рядку або рядком вище. `sql.unsafe` за замовчуванням заборонено: дозволено
9
+ * тільки якщо значення контролюється кодом (не user input) і потрібно підставити
10
+ * назву таблиці/колонки або dynamic SQL/DDL. Інакше — переробити на tagged template
11
+ * `sql\`...\${value}...\``. Маркер фіксує цю причину для ревʼюера.
10
12
  * - Динамічні SQL-списки у tagged template `sql\`... IN (${arr.join(',')}) ...\``:
11
13
  * навіть «через tagged template» у запит потрапляє готовий шматок SQL замість
12
14
  * параметризованих значень — треба `sql([...])`.
@@ -21,6 +23,7 @@ import {
21
23
  isSqlListContextTemplate,
22
24
  normalizeSnippet,
23
25
  offsetToLine,
26
+ parseProgramAndCommentsOrNull,
24
27
  parseProgramOrNull,
25
28
  walkAstWithAncestors
26
29
  } from './ast-scan-utils.mjs'
@@ -28,6 +31,9 @@ import {
28
31
  const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
29
32
  const BUN_SQL_IMPORT_RE = /\bimport\s*\{[\s\S]*?\b(sql|SQL)\b[\s\S]*?\}\s*from\s*["']bun["']/u
30
33
  const IN_PLACEHOLDER_END_RE = /\bin\s*(\(\s*)?$/iu
34
+ // `// allow-unsafe: <reason>` — `allow-unsafe`, двокрапка, **непорожня** причина.
35
+ // Без причини маркер не приймається: ціль — лишити слід для ревʼюера, а не «німий» прапорець.
36
+ const ALLOW_UNSAFE_MARKER_RE = /\ballow-unsafe\s*:\s*\S+/u
31
37
 
32
38
  /**
33
39
  * @param {unknown} node AST node
@@ -192,23 +198,46 @@ function isNewSqlConstructor(node) {
192
198
  }
193
199
 
194
200
  /**
195
- * Чи це виклик `<obj>.unsafe(...)` з TemplateLiteral як першим аргументом і expressions усередині нього.
196
- * Допустимий лише `sql.unsafe('static text', [params])`; з `${...}` у TemplateLiteral — небезпечно.
201
+ * Чи це виклик `<obj>.unsafe(...)` (будь-який обʼєкт, не тільки `sql`).
202
+ * Файл сканується лише якщо є `import { sql|SQL } from 'bun'`, тож у практиці це
203
+ * або `sql.unsafe`, або `tx.unsafe` всередині `sql.begin(async tx => ...)` —
204
+ * обидва однаково небезпечні, тому розрізняти імʼя обʼєкта не треба.
197
205
  * @param {unknown} node AST node
198
- * @returns {boolean} true для небезпечного `sql.unsafe(\`... ${x} ...\`)`
206
+ * @returns {boolean} true для будь-якого `<obj>.unsafe(...)`
199
207
  */
200
- function isUnsafeCallWithInterpolatedTemplate(node) {
208
+ function isUnsafeCall(node) {
201
209
  if (!node || node.type !== 'CallExpression') return false
202
210
  const callee = node.callee
203
211
  if (!callee || callee.type !== 'MemberExpression' || callee.computed) return false
204
212
  const prop = callee.property
205
- if (!prop || prop.type !== 'Identifier' || prop.name !== 'unsafe') return false
206
- const args = node.arguments
207
- if (!Array.isArray(args) || args.length === 0) return false
208
- const first = args[0]
209
- if (!first || first.type !== 'TemplateLiteral') return false
210
- const expressions = first.expressions
211
- return Array.isArray(expressions) && expressions.length > 0
213
+ return !!prop && prop.type === 'Identifier' && prop.name === 'unsafe'
214
+ }
215
+
216
+ /**
217
+ * Чи є біля виклику `<obj>.unsafe(...)` маркер-коментар `// allow-unsafe: <reason>`
218
+ * (або `/* allow-unsafe: <reason> *\/`) на тому ж рядку, що й початок виклику,
219
+ * або на рядку, що передує початку виклику. Це навмисно строга суміжність:
220
+ * відірваний коментар через порожній рядок не зараховується — щоб маркер
221
+ * стояв саме біля виклику, а не «загубився десь вище».
222
+ * @param {{ start: number }} callNode виклик `<obj>.unsafe(...)`
223
+ * @param {{ type: 'Line' | 'Block', value: string, start: number, end: number }[]} comments коментарі з парсера
224
+ * @param {string} content вихідний код
225
+ * @returns {boolean} true, якщо маркер знайдено
226
+ */
227
+ function hasAllowUnsafeMarkerNear(callNode, comments, content) {
228
+ const callStartLine = offsetToLine(content, callNode.start)
229
+ for (const c of comments) {
230
+ if (!c || (c.type !== 'Line' && c.type !== 'Block')) continue
231
+ if (typeof c.value !== 'string' || !ALLOW_UNSAFE_MARKER_RE.test(c.value)) continue
232
+ const startLine = offsetToLine(content, c.start)
233
+ const endLine = offsetToLine(content, c.end)
234
+ // trailing-коментар на тому ж рядку (`sql.unsafe(...) // allow-unsafe: ...`)
235
+ if (startLine === callStartLine) return true
236
+ // коментар на рядку, що безпосередньо передує виклику — для блокових
237
+ // коментарів важливим є саме `endLine`, бо block може займати кілька рядків.
238
+ if (endLine === callStartLine - 1) return true
239
+ }
240
+ return false
212
241
  }
213
242
 
214
243
  /**
@@ -236,19 +265,28 @@ export function findBunSqlPerRequestConnectionInText(content, virtualPath = 'sca
236
265
  }
237
266
 
238
267
  /**
239
- * Знаходить виклики `sql.unsafe(\`...${...}...\`)` (TemplateLiteral з expressions).
268
+ * Знаходить виклики `<obj>.unsafe(...)` без маркера-коментаря `// allow-unsafe: <reason>`
269
+ * на тому ж рядку або рядком вище. `sql.unsafe` за замовчуванням заборонено: дозволено
270
+ * лише коли значення контролюється кодом (не user input) і потрібно підставити те, що
271
+ * не можна параметризувати — назву таблиці/колонки або dynamic SQL/DDL. У всіх інших
272
+ * випадках — переробити на tagged template `sql\`...\${value}...\``.
273
+ *
274
+ * Маркер-коментар фіксує причину для ревʼюера й одночасно слугує opt-in: без нього
275
+ * перевірка падає, навіть якщо у `unsafe` лежить статичний рядок без інтерполяції.
240
276
  * @param {string} content вихідний код
241
277
  * @param {string} [virtualPath] шлях для вибору `lang`
242
278
  * @returns {{ line: number, snippet: string }[]} список порушень
243
279
  */
244
- export function findUnsafeBunSqlUnsafeCallInText(content, virtualPath = 'scan.ts') {
245
- const program = parseProgramOrNull(content, virtualPath)
246
- if (!program) return []
280
+ export function findBunSqlUnsafeUseWithoutAllowMarkerInText(content, virtualPath = 'scan.ts') {
281
+ const parsed = parseProgramAndCommentsOrNull(content, virtualPath)
282
+ if (!parsed) return []
283
+ const { program, comments } = parsed
247
284
 
248
285
  /** @type {{ line: number, snippet: string }[]} */
249
286
  const out = []
250
287
  walkAstWithAncestors(program, [], node => {
251
- if (!isUnsafeCallWithInterpolatedTemplate(node)) return
288
+ if (!isUnsafeCall(node)) return
289
+ if (hasAllowUnsafeMarkerNear(node, comments, content)) return
252
290
  out.push({
253
291
  line: offsetToLine(content, node.start),
254
292
  snippet: normalizeSnippet(content.slice(node.start, node.end))