@hedeqiang/dsh-nl2sql 0.14.1 → 0.14.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.
- package/CHANGELOG.md +9 -1
- package/package.json +1 -1
- package/service/query.js +3 -3
- package/service/schema.js +20 -2
- package/service/shared.js +15 -6
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.14.2] - 2026-11-13
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- A column named just `token` was always masked, but in crypto / payment databases it is the currency (USDT, BTC…). It is now masked only in credential tables (sessions, oauth, api keys…) or when its comment says it is a credential; credential names such as `access_token`, `api_token` and `token_hash` are still masked. Mask any other column explicitly with the data source's access policy.
|
|
12
|
+
- `tel`, `mail` and `mobile` no longer match inside other words (`whitelist_status`, `hotel`, `telegram_id`, `mailbox`).
|
|
13
|
+
- Cached schema analyses are re-classified with the new rules on load (no re-analysis needed), and cached query results masked under the old rules are not reused.
|
|
14
|
+
|
|
8
15
|
## [0.14.1] - 2026-11-13
|
|
9
16
|
|
|
10
17
|
### Fixed
|
|
@@ -144,7 +151,8 @@ These features are tested with local databases and fake IM / Lark servers; real
|
|
|
144
151
|
|
|
145
152
|
- Redesigned UI; Lark push (Markdown and native charts); access control, feedback loop, dashboard global filters; PostgreSQL / Elasticsearch, SSH tunnels, privacy settings, automatic overviews, scheduled reports; semantic layer, ER graph, scan guard, history and audit, dashboards; first version.
|
|
146
153
|
|
|
147
|
-
[Unreleased]: https://github.com/hedeqiang/dsh-nl2sql/compare/v0.14.
|
|
154
|
+
[Unreleased]: https://github.com/hedeqiang/dsh-nl2sql/compare/v0.14.2...HEAD
|
|
155
|
+
[0.14.2]: https://github.com/hedeqiang/dsh-nl2sql/compare/v0.14.1...v0.14.2
|
|
148
156
|
[0.14.1]: https://github.com/hedeqiang/dsh-nl2sql/compare/v0.14.0...v0.14.1
|
|
149
157
|
[0.14.0]: https://github.com/hedeqiang/dsh-nl2sql/compare/v0.13.1...v0.14.0
|
|
150
158
|
[0.13.1]: https://github.com/hedeqiang/dsh-nl2sql/compare/v0.13.0...v0.13.1
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hedeqiang/dsh-nl2sql",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.2",
|
|
4
4
|
"description": "Ask MySQL, PostgreSQL, Elasticsearch and CSV/Excel in plain language inside DeepSeek Harness: charts, dashboards, alerts, and IM push and bots for Lark/Feishu, Telegram, WeCom and DingTalk.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/service/query.js
CHANGED
|
@@ -9,7 +9,7 @@ import { admitSql, prepareSql, stripSql } from '../sql-guard.js'
|
|
|
9
9
|
import { cacheKey } from '../store.js'
|
|
10
10
|
import { createHash } from 'node:crypto'
|
|
11
11
|
import { writeXlsx } from '../xlsx.js'
|
|
12
|
-
import { RESULT_CACHE_LIMIT, SENSITIVE_NAME, UserError, callContext, columnKind, fillChartHint, fmtNum, maskValue, newId, normalizeChart, suggestChart, summarize } from './shared.js'
|
|
12
|
+
import { RESULT_CACHE_LIMIT, SENSITIVE_NAME, SENSITIVE_RULES_VERSION, isCredentialToken, UserError, callContext, columnKind, fillChartHint, fmtNum, maskValue, newId, normalizeChart, suggestChart, summarize } from './shared.js'
|
|
13
13
|
|
|
14
14
|
export const methods = {
|
|
15
15
|
// ---------------------------------------------------------------- query
|
|
@@ -87,7 +87,7 @@ export const methods = {
|
|
|
87
87
|
}
|
|
88
88
|
// Query cache: dashboards, reruns and reports reuse a recent identical result.
|
|
89
89
|
const ttlMs = (settings.cacheTtlSec ?? 0) * 1000
|
|
90
|
-
const key = ttlMs > 0 && useCache && this.store ? cacheKey([ds.id, admitted.sql, settings.maxRows, settings.maskSensitive, ds.policy ?? null, ds.updatedAt ?? 0, rowRules, rowRulesSig ?? null]) : null
|
|
90
|
+
const key = ttlMs > 0 && useCache && this.store ? cacheKey([ds.id, admitted.sql, settings.maxRows, settings.maskSensitive, SENSITIVE_RULES_VERSION, ds.policy ?? null, ds.updatedAt ?? 0, rowRules, rowRulesSig ?? null]) : null
|
|
91
91
|
if (key) {
|
|
92
92
|
const hit = this.store.cached(key, ttlMs)
|
|
93
93
|
if (hit && (hit.rowRulesSig ?? undefined) === rowRulesSig) {
|
|
@@ -142,7 +142,7 @@ export const methods = {
|
|
|
142
142
|
if (settings.maskSensitive) {
|
|
143
143
|
res.columns.forEach((f, i) => {
|
|
144
144
|
const col = f.table ? schema?.tables.find(t => t.name === f.table)?.columns.find(c => c.name === f.column) : undefined
|
|
145
|
-
if (col?.roles.includes('sensitive') || SENSITIVE_NAME.test(f.name)) sensitive.add(i)
|
|
145
|
+
if (col?.roles.includes('sensitive') || SENSITIVE_NAME.test(f.name) || (!col && isCredentialToken(f.name, f.table))) sensitive.add(i)
|
|
146
146
|
})
|
|
147
147
|
}
|
|
148
148
|
for (const row of cells) for (const i of sensitive) row[i] = maskValue(res.columns[i].name, row[i])
|
package/service/schema.js
CHANGED
|
@@ -7,7 +7,7 @@ import { annotateSchema, columnMasked, filterSchema, normalizePolicy, policyActi
|
|
|
7
7
|
import { admitSql } from '../sql-guard.js'
|
|
8
8
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
9
9
|
import { join } from 'node:path'
|
|
10
|
-
import { DIALECT_HINTS, UserError, classifyColumn, classifyTable, inferRelations, maskValue, newId, requireText, singular, upsert } from './shared.js'
|
|
10
|
+
import { DIALECT_HINTS, SENSITIVE_RULES_VERSION, UserError, classifyColumn, classifyTable, inferRelations, maskValue, newId, requireText, singular, upsert } from './shared.js'
|
|
11
11
|
|
|
12
12
|
/** Character bigrams (whitespace removed, works for Chinese) plus ASCII words of 3+ characters. */
|
|
13
13
|
export function textGrams(text) {
|
|
@@ -18,6 +18,23 @@ export function textGrams(text) {
|
|
|
18
18
|
return out
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Re-apply the current sensitive-column rules to a cached schema (rules change between
|
|
23
|
+
* versions, e.g. a bare `token` currency column is no longer sensitive). Only the
|
|
24
|
+
* 'sensitive' role is touched; other roles stay as analysed.
|
|
25
|
+
*/
|
|
26
|
+
export function reclassifySensitive(schema) {
|
|
27
|
+
if (!schema?.tables || schema.sensitiveRules === SENSITIVE_RULES_VERSION) return schema
|
|
28
|
+
for (const t of schema.tables) for (const c of t.columns ?? []) {
|
|
29
|
+
const want = classifyColumn(c, t).includes('sensitive')
|
|
30
|
+
const has = (c.roles ?? []).includes('sensitive')
|
|
31
|
+
if (want && !has) c.roles = [...(c.roles ?? []), 'sensitive']
|
|
32
|
+
if (!want && has) c.roles = c.roles.filter(r => r !== 'sensitive')
|
|
33
|
+
}
|
|
34
|
+
schema.sensitiveRules = SENSITIVE_RULES_VERSION
|
|
35
|
+
return schema
|
|
36
|
+
}
|
|
37
|
+
|
|
21
38
|
export const methods = {
|
|
22
39
|
// --------------------------------------------------------------- schema
|
|
23
40
|
|
|
@@ -87,6 +104,7 @@ export const methods = {
|
|
|
87
104
|
if (previous && settings.schemaWatch !== false) {
|
|
88
105
|
try { this.recordSchemaChange(ds, previous, schema) } catch (error) { console.warn('nl2sql: schema diff failed: %s', error.message) }
|
|
89
106
|
}
|
|
107
|
+
schema.sensitiveRules = SENSITIVE_RULES_VERSION
|
|
90
108
|
this.schemas.set(ds.id, schema)
|
|
91
109
|
ds.schemaAnalyzedAt = schema.analyzedAt
|
|
92
110
|
if (raw.version && !ds.serverVersion) ds.serverVersion = raw.version
|
|
@@ -97,7 +115,7 @@ export const methods = {
|
|
|
97
115
|
|
|
98
116
|
async readSchemaFile(id) {
|
|
99
117
|
try {
|
|
100
|
-
return JSON.parse(await readFile(join(this.dir, `schema-${id}.json`), 'utf8'))
|
|
118
|
+
return reclassifySensitive(JSON.parse(await readFile(join(this.dir, `schema-${id}.json`), 'utf8')))
|
|
101
119
|
} catch {
|
|
102
120
|
return undefined
|
|
103
121
|
}
|
package/service/shared.js
CHANGED
|
@@ -57,14 +57,24 @@ export function pick(obj, keys) {
|
|
|
57
57
|
return out
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
// "token" alone is ambiguous
|
|
61
|
-
//
|
|
62
|
-
|
|
60
|
+
// "token" alone is ambiguous: in crypto / payment schemas it is the currency (USDT, BTC…), so a
|
|
61
|
+
// bare `token` column is NOT sensitive by name. Credential-like names (access_token, api_token,
|
|
62
|
+
// token_hash, …) are; a bare `token` counts only in credential tables or with a credential comment
|
|
63
|
+
// (see isCredentialToken). Mask anything else explicitly with the access policy's masked columns.
|
|
64
|
+
export const SENSITIVE_NAME = /(phone|(?:^|_)mobile(?:_?(?:no|num|number))?(?:_|$)|(?:^|_)tel(?:ephone)?(?:_?(?:no|num|number))?(?:_|$)|e_?mail|(?:^|_)mail(?:_?addr(?:ess)?)?(?:_|$)|id_?card|idno|identity_?(?:no|num|number|card)|passport|password|passwd|pwd|secret|(?:access|auth|api|refresh|session|bearer|csrf|jwt|push|device)_?token|token_?(?:hash|secret|key)$|bank_?card|card_?no|身份证|手机|电话|邮箱|密码)/i
|
|
65
|
+
const CREDENTIAL_TABLE = /(session|oauth|auth|login|credential|api_?key|access_?key|personal_?access|user_?token|refresh_?token|jwt|sso)/i
|
|
66
|
+
const CREDENTIAL_COMMENT = /(令牌|凭证|凭据|密钥|访问码|登录态|credential|access token|auth token|bearer|jwt)/i
|
|
67
|
+
/** A bare `token` column that holds a credential (by table name or comment), not a currency. */
|
|
68
|
+
export function isCredentialToken(name, table, comment) {
|
|
69
|
+
return /^token$/i.test(String(name ?? '')) && (CREDENTIAL_TABLE.test(String(table ?? '')) || CREDENTIAL_COMMENT.test(String(comment ?? '')))
|
|
70
|
+
}
|
|
71
|
+
/** Bumped whenever the sensitive-column rules change (part of result cache keys). */
|
|
72
|
+
export const SENSITIVE_RULES_VERSION = 3
|
|
63
73
|
|
|
64
74
|
export function maskValue(column, value) {
|
|
65
75
|
if (value === null || value === undefined) return value
|
|
66
76
|
const s = String(value)
|
|
67
|
-
if (/(password|passwd|pwd|secret
|
|
77
|
+
if (/(password|passwd|pwd|secret|密码)/i.test(column) || /(?:access|auth|api|refresh|session|bearer|csrf|jwt|push|device)_?token|token_?(?:hash|secret|key)$|^token$/i.test(column)) return '******'
|
|
68
78
|
if (/(email|mail|邮箱)/i.test(column) && s.includes('@')) {
|
|
69
79
|
const [u, d] = s.split('@')
|
|
70
80
|
return `${u.slice(0, 1)}***@${d}`
|
|
@@ -91,10 +101,9 @@ export function classifyColumn(col, table) {
|
|
|
91
101
|
if (t === 'enum' || t === 'set' || (ENUM_NAME.test(name) && (t === 'tinyint' || t === 'smallint' || t === 'char' || t === 'varchar' || t === 'int'))) roles.push('enum')
|
|
92
102
|
if (NUMERIC_TYPES.has(t) && !roles.includes('id') && !roles.includes('enum') && !roles.includes('time') && !/^is_|^has_/.test(name)) roles.push('measure')
|
|
93
103
|
if (TEXT_TYPES.has(t) && GEO_NAME.test(name)) roles.push('geo')
|
|
94
|
-
if (SENSITIVE_NAME.test(name) || SENSITIVE_NAME.test(col.comment ?? '')) roles.push('sensitive')
|
|
104
|
+
if (SENSITIVE_NAME.test(name) || SENSITIVE_NAME.test(col.comment ?? '') || isCredentialToken(name, typeof table === 'string' ? table : table?.name, col.comment)) roles.push('sensitive')
|
|
95
105
|
if (/^is_|^has_/.test(name) || col.columnType === 'tinyint(1)' || t === 'bit') roles.push('flag')
|
|
96
106
|
if (roles.length === 0 && TEXT_TYPES.has(t)) roles.push('text')
|
|
97
|
-
void table
|
|
98
107
|
return roles
|
|
99
108
|
}
|
|
100
109
|
|