@nitra/cursor 1.8.141 → 1.8.142

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/mdc/js-mssql.mdc CHANGED
@@ -4,11 +4,18 @@ alwaysApply: true
4
4
  version: '1.1'
5
5
  ---
6
6
 
7
- Якщо в проекті в будь-якому `package.json` в секції `dependencies` присутній пакет **`mssql`**,
8
- то його версія повинна бути не менше **12.5.0**.
7
+ ## Підтримувана версія SQL Server
8
+
9
+ Правило орієнтоване на **SQL Server 2019**.
10
+
11
+ ## Версія пакета `mssql`
12
+
13
+ Якщо в проекті в будь-якому `package.json` в секції `dependencies` присутній пакет **`mssql`**, то його версія повинна бути не менше **12.5.0**.
9
14
 
10
15
  Потрібно використовувати connection pool (sql.ConnectionPool) як singleton, а НЕ створювати підключення на кожен запит.
11
16
 
17
+ ## Як виконувати запити (безпечно)
18
+
12
19
  tagged template треба викликати на request-обʼєкті цього пулу:
13
20
  ```javascript
14
21
  javascript// db.js
@@ -46,7 +53,10 @@ const result = await pool.request().query`
46
53
 
47
54
  Ключове: pool.request().query\...`— бекті́ки післяquery`, без круглих дужок. Це той самий tagged template, тільки контекст — конкретний пул, а не глобальний.
48
55
 
49
- Що НЕ робити
56
+ ## Що НЕ робити
57
+
58
+ ### Не робити `query(\`...\`)`
59
+
50
60
  javascript// ❌ Це не tagged template — це конкатенація рядка перед викликом
51
61
  ```javascript
52
62
  await pool.request().query(`SELECT * FROM users WHERE id = ${userId}`);
@@ -56,4 +66,98 @@ await pool.request().query(`SELECT * FROM users WHERE id = ${userId}`);
56
66
  Різниця між query\...`іquery(`...`)` — критична. Перше безпечне, друге — діра.
57
67
  Потрібно використовувати перше. Потрібно шукати в коді друге і заміняти на перше.
58
68
 
69
+ ### Не шарити `Request` між запитами
70
+
71
+ Заборонено робити singleton `request` на рівні модуля на кшталт:
72
+
73
+ ```javascript
74
+ // ❌ НЕ МОЖНА
75
+ export const request = pool.request();
76
+ ```
77
+
78
+ `Request` має створюватися **щоразу заново**:
79
+
80
+ ```javascript
81
+ // ✅ МОЖНА
82
+ const request = pool.request();
83
+ ```
84
+
85
+ Це особливо важливо для **TVP** (табличні параметри) та будь-яких `.input(...)`.
86
+
87
+ ## TVP дозволений і рекомендований (SQL Server 2019)
88
+
89
+ Для списків значень та пар ключів **найкращий** шлях по безпеці/продуктивності — **TVP** (table-valued parameters).
90
+
91
+ ### 1) `IN (...)` для кодів → `JOIN` на TVP
92
+
93
+ Замість складання SQL-рядка:
94
+
95
+ ```javascript
96
+ // ❌ НЕ МОЖНА: динамічний список у SQL
97
+ await pool.request().query`
98
+ SELECT *
99
+ FROM promo.SomeTable t
100
+ WHERE t.ExternalCode IN (${codes.map(c => `'${c}'`).join(',')})
101
+ `;
102
+ ```
103
+
104
+ Роби TVP з 1 колонкою:
105
+
106
+ - створити `new sql.Table()`
107
+ - `columns.add('ExternalCode', sql.NVarChar(N))`
108
+ - `rows.add(code)` після `trim()` + валідації
109
+ - `request.input('codes', table)`
110
+ - в SQL: `JOIN @codes c ON c.ExternalCode = t.ExternalCode`
111
+
112
+ Плюси:
113
+
114
+ - 0% SQL injection
115
+ - текст SQL **не росте** від довжини масиву
116
+ - SQL Server часто оптимізує `JOIN` краще, ніж гігантський `IN (...)`
117
+
118
+ ### 2) `DELETE ... VALUES (...)` / `MERGE ... VALUES (...)` → TVP з 2 колонками
119
+
120
+ Замість генерації списку рядків `(...),(...),...` у SQL (навіть якщо воно “через tagged template”):
121
+
122
+ - сформуй TVP-таблицю `@Pairs` з колонками:
123
+ - `PromoActivitiesId` (INT або BIGINT — як у схемі БД)
124
+ - `SupplierOutletId` (INT або BIGINT — як у схемі БД)
125
+
126
+ Delete:
127
+
128
+ ```sql
129
+ DELETE abi
130
+ FROM promo.ActivitiesByOutlet abi
131
+ JOIN @Pairs p
132
+ ON p.PromoActivitiesId = abi.PromoActivitiesId
133
+ AND p.SupplierOutletId = abi.SupplierOutletId;
134
+ ```
135
+
136
+ Insert (без `MERGE`, простіше і безпечніше):
137
+
138
+ ```sql
139
+ INSERT INTO promo.ActivitiesByOutlet (PromoActivitiesId, SupplierOutletId, Status)
140
+ SELECT p.PromoActivitiesId, p.SupplierOutletId, 2
141
+ FROM @Pairs p
142
+ WHERE NOT EXISTS (
143
+ SELECT 1
144
+ FROM promo.ActivitiesByOutlet abi
145
+ WHERE abi.PromoActivitiesId = p.PromoActivitiesId
146
+ AND abi.SupplierOutletId = p.SupplierOutletId
147
+ );
148
+ ```
149
+
150
+ Плюси:
151
+
152
+ - не збираєш SQL-рядок із даних
153
+ - менше шансів упіймати edge-case `MERGE` (у SQL Server історично багато “сюрпризів”)
154
+
155
+ ## Мінімальна валідація перед наповненням TVP
156
+
157
+ Навіть з TVP потрібно:
158
+
159
+ - `ExternalCode`: `typeof === 'string'`, `trim()`, довжина `<= N` (під схему), відкинути пусті.
160
+ - ліміт на кількість елементів (наприклад 5k/10k — залежить від вашого потоку).
161
+ - `supplierId`/ID: число/BigInt, валідне та скінченне.
162
+
59
163
  Перевірка: `npx @nitra/cursor check js-mssql`.
package/mdc/vue.mdc CHANGED
@@ -221,6 +221,47 @@ export default defineConfig({
221
221
 
222
222
  Потрібно використовувати vite-plugin-vue-layouts-next для автоматичного імпортування layout компонентів.
223
223
 
224
+ ## npm_lifecycle_event
225
+
226
+ у більшості проектів в файлі vite.config.js
227
+ є конструкція виду
228
+
229
+ switch (process.env.npm_lifecycle_event) {
230
+ case 'start-remote-tr': {
231
+
232
+ вона перестала працювати з новим Bun (і за цього не буде працювати bun start-remote-dev та інші)
233
+
234
+ і тепер її нада обрамити в функцію, наприклад
235
+
236
+ ```javascript title="vite.config.js"
237
+ function getProxy(mode) {
238
+ const proxy = {}
239
+
240
+ switch (mode) {
241
+ case 'remote-tr': {
242
+ proxy['^/auth/.*'] = 'https://tr.efes.cloud'
243
+ proxy['/file-link/'] = 'https://tr.efes.cloud'
244
+ break
245
+ }
246
+ default: {
247
+ proxy['^/auth/.*'] = 'https://dev.efes.cloud'
248
+ proxy['/file-link/'] = 'https://dev.efes.cloud'
249
+ }
250
+ }
251
+ return proxy
252
+ }
253
+ ```
254
+
255
+ і викликати всередині
256
+
257
+ ```javascript title="vite.config.js"
258
+ export default defineConfig(({ mode, command }) => {
259
+ ...
260
+ server: {
261
+ proxy: getProxy(mode)
262
+ }
263
+ ```
264
+
224
265
  ## Перевірка
225
266
 
226
267
  `npx @nitra/cursor check vue` — перевіряє залежності, `vite.config`, а також обходить джерела Vue-пакета (`.vue`, `.ts`, `.js` тощо) на заборонені value-імпорти з модуля `vue`; дозволені лише type-only та side-effect `import 'vue'`. Імпорти аналізуються через **oxc-parser** (`module.staticImports`); для `.vue` вміст `<script>` витягується з SFC, далі той самий парсер (логіка в `npm/scripts/utils/vue-forbidden-imports.mjs`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.141",
3
+ "version": "1.8.142",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -15,7 +15,9 @@ import { join, relative, sep } from 'node:path'
15
15
  import { createCheckReporter } from './utils/check-reporter.mjs'
16
16
  import {
17
17
  findMssqlPerRequestConnectionInText,
18
+ findSharedMssqlRequestInText,
18
19
  findUnsafeMssqlQueryTemplateCallInText,
20
+ findUnsafeMssqlDynamicSqlListInText,
19
21
  isMssqlScanSourceFile
20
22
  } from './utils/mssql-pool-scan.mjs'
21
23
  import { walkDir } from './utils/walkDir.mjs'
@@ -172,7 +174,9 @@ export async function check() {
172
174
  }
173
175
 
174
176
  let violations = 0
177
+ let sharedRequestViolations = 0
175
178
  let unsafeQueryCalls = 0
179
+ let unsafeDynamicSqlLists = 0
176
180
  for (const absPath of sourcePaths) {
177
181
  const rel = relative(repoRoot, absPath).split('\\').join('/')
178
182
  const content = await readFile(absPath, 'utf8')
@@ -182,20 +186,38 @@ export async function check() {
182
186
  `js-mssql: ${rel}:${v.line} — не створюй new sql.ConnectionPool(...) на кожен запит; використовуй singleton sql.ConnectionPool: ${v.snippet}`
183
187
  )
184
188
  }
189
+ for (const v of findSharedMssqlRequestInText(content, rel)) {
190
+ sharedRequestViolations++
191
+ fail(
192
+ `js-mssql: ${rel}:${v.line} — заборонено шарити Request (наприклад export const request = pool.request()); створюй pool.request() щоразу заново (js-mssql.mdc): ${v.snippet}`
193
+ )
194
+ }
185
195
  for (const v of findUnsafeMssqlQueryTemplateCallInText(content, rel)) {
186
196
  unsafeQueryCalls++
187
197
  fail(
188
198
  `js-mssql: ${rel}:${v.line} — заборонено query(\`...\`): це не tagged template; використовуй pool.request().query\`...\` (js-mssql.mdc): ${v.snippet}`
189
199
  )
190
200
  }
201
+ for (const v of findUnsafeMssqlDynamicSqlListInText(content, rel)) {
202
+ unsafeDynamicSqlLists++
203
+ fail(
204
+ `js-mssql: ${rel}:${v.line} — заборонено підставляти у SQL динамічні списки через .join(',') (типово IN (...) / VALUES (...)); використовуй TVP (sql.Table) + JOIN/INSERT (js-mssql.mdc): ${v.snippet}`
205
+ )
206
+ }
191
207
  }
192
208
 
193
209
  if (violations === 0) {
194
210
  pass('js-mssql: немає створення new sql.ConnectionPool(...) всередині функцій (singleton pool)')
195
211
  }
212
+ if (sharedRequestViolations === 0) {
213
+ pass('js-mssql: немає shared Request (export const request = pool.request())')
214
+ }
196
215
  if (unsafeQueryCalls === 0) {
197
216
  pass('js-mssql: немає небезпечних викликів query(`...`) (потрібно query`...`)')
198
217
  }
218
+ if (unsafeDynamicSqlLists === 0) {
219
+ pass("js-mssql: немає небезпечних динамічних SQL-списків через .join(',') у IN/VALUES")
220
+ }
199
221
  }
200
222
 
201
223
  return reporter.getExitCode()
@@ -4,6 +4,9 @@
4
4
  * Версії Vite та плагінів, vue-macros, auto-import, layouts, вміст `vite.config`;
5
5
  * у репозиторії — рекомендацію розширення Vue.volar.
6
6
  *
7
+ * У `vite.config.*` заборонено використовувати `process.env.npm_lifecycle_event` (Bun не підставляє його як npm),
8
+ * натомість використовуй `mode` з `defineConfig(({ mode }) => ...)`.
9
+ *
7
10
  * Заборонені явні value-імпорти з `vue` у джерелах пакета — сканування `.vue`/`.ts`/`.js` тощо
8
11
  * через **oxc-parser** (`module.staticImports`; див. `utils/vue-forbidden-imports.mjs`); дозволені лише type-only та side-effect `import 'vue'`.
9
12
  */
@@ -115,6 +118,13 @@ async function checkViteConfig(rootDir, prefix, passFn, fail) {
115
118
  fail(`${prefix}${err}`)
116
119
  }
117
120
  }
121
+
122
+ if (content.includes('process.env.npm_lifecycle_event')) {
123
+ fail(
124
+ `${prefix}${viteConfig} використовує process.env.npm_lifecycle_event — у Bun це не працює. ` +
125
+ `Перенеси логіку на mode (defineConfig(({ mode }) => ...)) і передавай mode в helper-функції.`
126
+ )
127
+ }
118
128
  }
119
129
 
120
130
  /**
@@ -7,6 +7,12 @@
7
7
  * виклик з інтерполяцією рядка, який може призвести до SQL injection. Натомість має
8
8
  * використовуватись tagged template `query\`...\`` (див. js-mssql.mdc).
9
9
  *
10
+ * Додатково знаходить:
11
+ * - shared `Request` (наприклад `export const request = pool.request()`), який не можна
12
+ * повторно використовувати між запитами.
13
+ * - небезпечні “динамічні списки” в SQL, коли в TemplateLiteral/TaggedTemplateExpression
14
+ * підставляють рядки, зібрані через `.join(',')` (типово для `IN (...)` або `VALUES (...)`).
15
+ *
10
16
  * Семантика береться з **oxc-parser** по AST, щоб не покладатися на regex.
11
17
  * Якщо файл не парситься або містить синтаксичні помилки — повертаємо порожній
12
18
  * результат (спочатку треба полагодити синтаксис, потім перезапустити перевірку).
@@ -14,6 +20,7 @@
14
20
  import { parseSync } from 'oxc-parser'
15
21
 
16
22
  const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/
23
+ const SQL_LIST_CONTEXT_RE = /\b(in|values)\b\s*\(/iu
17
24
 
18
25
  /**
19
26
  * Мова для Oxc за шляхом файлу (розширення).
@@ -134,6 +141,62 @@ function isUnsafeQueryCallWithTemplateLiteral(node) {
134
141
  return !!first && typeof first === 'object' && first.type === 'TemplateLiteral'
135
142
  }
136
143
 
144
+ /**
145
+ * Чи це `something.request()` (наприклад `pool.request()`), яку не можна шарити між запитами.
146
+ * @param {unknown} node AST node
147
+ * @returns {boolean} true, якщо це CallExpression з `.request()`
148
+ */
149
+ function isRequestFactoryCall(node) {
150
+ if (!node || node.type !== 'CallExpression') return false
151
+ const callee = node.callee
152
+ if (!callee || callee.type !== 'MemberExpression') return false
153
+ if (callee.computed) return false
154
+ const prop = callee.property
155
+ return !!prop && prop.type === 'Identifier' && prop.name === 'request'
156
+ }
157
+
158
+ /**
159
+ * Чи це `.join(...)` виклик (часто використовується для динамічних списків в SQL).
160
+ * @param {unknown} node AST node
161
+ * @returns {boolean} true, якщо це CallExpression `*.join(...)`
162
+ */
163
+ function isJoinCall(node) {
164
+ if (!node || node.type !== 'CallExpression') return false
165
+ const callee = node.callee
166
+ if (!callee || callee.type !== 'MemberExpression') return false
167
+ if (callee.computed) return false
168
+ const prop = callee.property
169
+ return !!prop && prop.type === 'Identifier' && prop.name === 'join'
170
+ }
171
+
172
+ /**
173
+ * Повертає текст quasis у TemplateLiteral (без expressions).
174
+ * @param {unknown} template TemplateLiteral
175
+ * @returns {string} обʼєднаний текст
176
+ */
177
+ function templateQuasisText(template) {
178
+ if (!template || template.type !== 'TemplateLiteral') return ''
179
+ const quasis = template.quasis
180
+ if (!Array.isArray(quasis) || quasis.length === 0) return ''
181
+ let out = ''
182
+ for (const q of quasis) {
183
+ if (!q || typeof q !== 'object') continue
184
+ const value = q.value
185
+ if (!value || typeof value !== 'object') continue
186
+ if (typeof value.raw === 'string') out += value.raw
187
+ }
188
+ return out
189
+ }
190
+
191
+ /**
192
+ * Чи виглядає TemplateLiteral як SQL-контекст зі списком (IN/VALUES (...)).
193
+ * @param {unknown} template TemplateLiteral
194
+ * @returns {boolean} true, якщо текст містить `IN (` або `VALUES (`
195
+ */
196
+ function isSqlListContextTemplate(template) {
197
+ return SQL_LIST_CONTEXT_RE.test(templateQuasisText(template))
198
+ }
199
+
137
200
  /**
138
201
  * Знаходить створення `ConnectionPool` всередині функцій.
139
202
  * @param {string} content вихідний код
@@ -197,6 +260,92 @@ export function findUnsafeMssqlQueryTemplateCallInText(content, virtualPath = 's
197
260
  return out
198
261
  }
199
262
 
263
+ /**
264
+ * Знаходить shared Request (`export const request = pool.request()` та подібні), які не можна
265
+ * повторно використовувати між запитами.
266
+ * @param {string} content вихідний код
267
+ * @param {string} [virtualPath] шлях для вибору `lang`
268
+ * @returns {{ line: number, snippet: string }[]} список порушень
269
+ */
270
+ export function findSharedMssqlRequestInText(content, virtualPath = 'scan.ts') {
271
+ const lang = langFromPath(virtualPath || 'scan.ts')
272
+ let result
273
+ try {
274
+ result = parseSync(virtualPath, content, { lang, sourceType: 'module' })
275
+ } catch {
276
+ return []
277
+ }
278
+ if (result.errors?.length) return []
279
+
280
+ /** @type {{ line: number, snippet: string }[]} */
281
+ const out = []
282
+ walkAstWithAncestors(result.program, [], node => {
283
+ if (node.type !== 'VariableDeclarator') return
284
+ const id = node.id
285
+ const init = node.init
286
+ if (!id || id.type !== 'Identifier') return
287
+ if (id.name !== 'request') return
288
+ if (!init || typeof init !== 'object') return
289
+ if (!isRequestFactoryCall(init)) return
290
+
291
+ out.push({
292
+ line: offsetToLine(content, node.start),
293
+ snippet: normalizeSnippet(content.slice(node.start, node.end))
294
+ })
295
+ })
296
+ return out
297
+ }
298
+
299
+ /**
300
+ * Знаходить небезпечні динамічні списки в SQL, коли у TemplateLiteral/TaggedTemplateExpression
301
+ * підставляють рядки, зібрані через `.join(...)` у контексті `IN (...)` або `VALUES (...)`.
302
+ *
303
+ * Цей патерн небезпечний навіть якщо зовні використовується tagged template, бо в запит
304
+ * потрапляє “готовий шматок SQL”, а не параметризовані значення.
305
+ *
306
+ * @param {string} content вихідний код
307
+ * @param {string} [virtualPath] шлях для вибору `lang`
308
+ * @returns {{ line: number, snippet: string }[]} список порушень
309
+ */
310
+ export function findUnsafeMssqlDynamicSqlListInText(content, virtualPath = 'scan.ts') {
311
+ const lang = langFromPath(virtualPath || 'scan.ts')
312
+ let result
313
+ try {
314
+ result = parseSync(virtualPath, content, { lang, sourceType: 'module' })
315
+ } catch {
316
+ return []
317
+ }
318
+ if (result.errors?.length) return []
319
+
320
+ /** @type {{ line: number, snippet: string }[]} */
321
+ const out = []
322
+ walkAstWithAncestors(result.program, [], node => {
323
+ /** @type {unknown} */
324
+ let template = null
325
+
326
+ if (node.type === 'TemplateLiteral') {
327
+ template = node
328
+ } else if (node.type === 'TaggedTemplateExpression') {
329
+ template = node.quasi
330
+ }
331
+
332
+ if (!template || typeof template !== 'object' || template.type !== 'TemplateLiteral') return
333
+ if (!isSqlListContextTemplate(template)) return
334
+ const expressions = template.expressions
335
+ if (!Array.isArray(expressions) || expressions.length === 0) return
336
+
337
+ const hasJoin = expressions.some(expr => isJoinCall(expr))
338
+ if (!hasJoin) return
339
+
340
+ out.push({
341
+ line: offsetToLine(content, template.start),
342
+ snippet: normalizeSnippet(content.slice(template.start, template.end))
343
+ })
344
+ })
345
+
346
+ return out
347
+ }
348
+
200
349
  /**
201
350
  * Чи сканувати цей файл за розширенням (JS/TS-сім'я).
202
351
  * @param {string} relativePathPosix відносний шлях (posix)