@carllee1983/dbcli 4.0.0 → 6.0.0
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +78 -0
- package/dist/cli-runtime.mjs +1220 -895
- package/dist/cli.mjs +1 -1
- package/dist/core.d.ts +25 -8
- package/dist/core.mjs +345 -70
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/plugins/dbcli-agent/.codex-plugin/plugin.json +1 -1
package/dist/cli.mjs
CHANGED
package/dist/core.d.ts
CHANGED
|
@@ -540,10 +540,6 @@ declare const SQL_DIALECTS: readonly [
|
|
|
540
540
|
"mariadb"
|
|
541
541
|
];
|
|
542
542
|
type SqlDialect = (typeof SQL_DIALECTS)[number];
|
|
543
|
-
/**
|
|
544
|
-
* Manager class for loading and querying blacklist rules.
|
|
545
|
-
* Instantiate once per CLI invocation.
|
|
546
|
-
*/
|
|
547
543
|
export declare class BlacklistManager {
|
|
548
544
|
private config;
|
|
549
545
|
private state;
|
|
@@ -551,28 +547,49 @@ export declare class BlacklistManager {
|
|
|
551
547
|
constructor(config: DbcliConfig, overrideEnvValue?: string);
|
|
552
548
|
/**
|
|
553
549
|
* Deserialize config.blacklist JSON into efficient Set/Map structures.
|
|
554
|
-
* Case-insensitive table names (stored as lowercase).
|
|
555
|
-
*
|
|
550
|
+
* Case-insensitive table names (stored as lowercase). Column entries are
|
|
551
|
+
* stored as written and folded where they are compared — see ADR-0018.
|
|
556
552
|
*
|
|
557
553
|
* @returns BlacklistState with Set<string> for tables, Map<string, Set<string>> for columns
|
|
558
554
|
*/
|
|
559
555
|
loadBlacklist(): BlacklistState;
|
|
560
556
|
/**
|
|
561
557
|
* Check if a table is blacklisted.
|
|
562
|
-
*
|
|
558
|
+
*
|
|
559
|
+
* Case-insensitive, and each entry is a glob: `secrets*` covers
|
|
560
|
+
* `secrets_2026`. The same array is already a glob for Redis keys and
|
|
561
|
+
* Elasticsearch index expressions, and one config file gets one answer
|
|
562
|
+
* (ADR-0019 Decision 4). `report\*` escapes back to a literal name.
|
|
563
563
|
*
|
|
564
564
|
* @param tableName Table name to check
|
|
565
565
|
* @returns true if the table is blacklisted
|
|
566
566
|
*/
|
|
567
567
|
isTableBlacklisted(tableName: string): boolean;
|
|
568
|
+
/**
|
|
569
|
+
* Table entries carrying glob metacharacters.
|
|
570
|
+
*
|
|
571
|
+
* Computed with `state` rather than lazily: `getState()` hands the same `Set`
|
|
572
|
+
* out, and a lazy cache filled after someone added to it would leave `Set.has`
|
|
573
|
+
* seeing the new entry and the glob scan not — fail-open, and silently.
|
|
574
|
+
* Nothing mutates it today; the point is that nothing can start to.
|
|
575
|
+
*/
|
|
576
|
+
private readonly wildcardTables;
|
|
568
577
|
/**
|
|
569
578
|
* Check if a specific column in a table is blacklisted.
|
|
570
|
-
*
|
|
579
|
+
* Both are case-insensitive on their first segment. A column entry's later
|
|
580
|
+
* segments are nested object keys and keep their case (ADR-0018).
|
|
571
581
|
*
|
|
572
582
|
* @param tableName Table name
|
|
573
583
|
* @param columnName Column name
|
|
574
584
|
* @returns true if the column is blacklisted
|
|
575
585
|
*/
|
|
586
|
+
/**
|
|
587
|
+
* The rules for a table, found by the reference as written and by its last
|
|
588
|
+
* segment. `extractTableReferences` keeps a qualified name whole, so a rule
|
|
589
|
+
* under `public.users` did not apply to `SELECT * FROM users` and the reverse
|
|
590
|
+
* was equally true. ADR-0018 Decision 3.
|
|
591
|
+
*/
|
|
592
|
+
private columnRulesFor;
|
|
576
593
|
isColumnBlacklisted(tableName: string, columnName: string): boolean;
|
|
577
594
|
/**
|
|
578
595
|
* Get all blacklisted column names for a specific table.
|
package/dist/core.mjs
CHANGED
|
@@ -2024,6 +2024,224 @@ var messageLoader = MessageLoader.getInstance();
|
|
|
2024
2024
|
var t = (key) => messageLoader.t(key);
|
|
2025
2025
|
var t_vars = (key, vars) => messageLoader.interpolate(key, vars);
|
|
2026
2026
|
|
|
2027
|
+
// src/utils/glob.ts
|
|
2028
|
+
function findClassEnd(glob, open) {
|
|
2029
|
+
for (let i = open + 1;i < glob.length; i++) {
|
|
2030
|
+
if (glob[i] === "\\") {
|
|
2031
|
+
i++;
|
|
2032
|
+
continue;
|
|
2033
|
+
}
|
|
2034
|
+
if (glob[i] === "]")
|
|
2035
|
+
return i;
|
|
2036
|
+
}
|
|
2037
|
+
return -1;
|
|
2038
|
+
}
|
|
2039
|
+
function isValidCharacterClass(body) {
|
|
2040
|
+
try {
|
|
2041
|
+
new RegExp(body);
|
|
2042
|
+
return true;
|
|
2043
|
+
} catch {
|
|
2044
|
+
return false;
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
var parsedGlobs = new Map;
|
|
2048
|
+
function parseGlob(glob) {
|
|
2049
|
+
const memo = parsedGlobs.get(glob);
|
|
2050
|
+
if (memo !== undefined)
|
|
2051
|
+
return memo;
|
|
2052
|
+
const parsed = parseGlobUncached(glob);
|
|
2053
|
+
parsedGlobs.set(glob, parsed);
|
|
2054
|
+
return parsed;
|
|
2055
|
+
}
|
|
2056
|
+
function parseGlobUncached(glob) {
|
|
2057
|
+
const runs = [];
|
|
2058
|
+
let current = [];
|
|
2059
|
+
let leadingStar = false;
|
|
2060
|
+
let trailingStar = false;
|
|
2061
|
+
let sawStar = false;
|
|
2062
|
+
const endRun = () => {
|
|
2063
|
+
if (current.length > 0)
|
|
2064
|
+
runs.push(current);
|
|
2065
|
+
current = [];
|
|
2066
|
+
};
|
|
2067
|
+
for (let i = 0;i < glob.length; i++) {
|
|
2068
|
+
const c = glob[i];
|
|
2069
|
+
if (c === "\\" && i + 1 < glob.length) {
|
|
2070
|
+
trailingStar = false;
|
|
2071
|
+
current.push({ kind: "literal", char: glob[++i] });
|
|
2072
|
+
continue;
|
|
2073
|
+
}
|
|
2074
|
+
if (c === "*") {
|
|
2075
|
+
if (!sawStar && current.length === 0)
|
|
2076
|
+
leadingStar = true;
|
|
2077
|
+
sawStar = true;
|
|
2078
|
+
trailingStar = true;
|
|
2079
|
+
endRun();
|
|
2080
|
+
continue;
|
|
2081
|
+
}
|
|
2082
|
+
trailingStar = false;
|
|
2083
|
+
if (c === "?") {
|
|
2084
|
+
current.push({ kind: "any" });
|
|
2085
|
+
continue;
|
|
2086
|
+
}
|
|
2087
|
+
if (c === "[") {
|
|
2088
|
+
const end = findClassEnd(glob, i);
|
|
2089
|
+
const body = end === -1 ? "" : glob.slice(i, end + 1);
|
|
2090
|
+
if (body !== "" && isValidCharacterClass(body)) {
|
|
2091
|
+
current.push({ kind: "class", test: new RegExp(`^${body}$`, "s") });
|
|
2092
|
+
i = end;
|
|
2093
|
+
continue;
|
|
2094
|
+
}
|
|
2095
|
+
current.push({ kind: "literal", char: "[" });
|
|
2096
|
+
continue;
|
|
2097
|
+
}
|
|
2098
|
+
current.push({ kind: "literal", char: c });
|
|
2099
|
+
}
|
|
2100
|
+
endRun();
|
|
2101
|
+
return { runs, leadingStar, trailingStar, anchored: !sawStar };
|
|
2102
|
+
}
|
|
2103
|
+
function runMatchesAt(run, text, at) {
|
|
2104
|
+
if (at + run.length > text.length)
|
|
2105
|
+
return false;
|
|
2106
|
+
for (let i = 0;i < run.length; i++) {
|
|
2107
|
+
const token = run[i];
|
|
2108
|
+
const ch = text[at + i];
|
|
2109
|
+
if (token.kind === "literal") {
|
|
2110
|
+
if (token.char !== ch)
|
|
2111
|
+
return false;
|
|
2112
|
+
} else if (token.kind === "class") {
|
|
2113
|
+
if (!token.test.test(ch))
|
|
2114
|
+
return false;
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
return true;
|
|
2118
|
+
}
|
|
2119
|
+
function globMatches(glob, text) {
|
|
2120
|
+
const { runs, leadingStar, trailingStar, anchored } = parseGlob(glob);
|
|
2121
|
+
if (anchored) {
|
|
2122
|
+
const run = runs[0] ?? [];
|
|
2123
|
+
return text.length === run.length && runMatchesAt(run, text, 0);
|
|
2124
|
+
}
|
|
2125
|
+
if (runs.length === 0)
|
|
2126
|
+
return true;
|
|
2127
|
+
let cursor = 0;
|
|
2128
|
+
let first = 0;
|
|
2129
|
+
let last = runs.length;
|
|
2130
|
+
if (!leadingStar) {
|
|
2131
|
+
const head = runs[0];
|
|
2132
|
+
if (!runMatchesAt(head, text, 0))
|
|
2133
|
+
return false;
|
|
2134
|
+
cursor = head.length;
|
|
2135
|
+
first = 1;
|
|
2136
|
+
}
|
|
2137
|
+
if (!trailingStar) {
|
|
2138
|
+
const tail = runs[last - 1];
|
|
2139
|
+
const at = text.length - tail.length;
|
|
2140
|
+
if (at < cursor || !runMatchesAt(tail, text, at))
|
|
2141
|
+
return false;
|
|
2142
|
+
last -= 1;
|
|
2143
|
+
}
|
|
2144
|
+
const limit = trailingStar ? text.length : text.length - runs[runs.length - 1].length;
|
|
2145
|
+
for (let r = first;r < last; r++) {
|
|
2146
|
+
const run = runs[r];
|
|
2147
|
+
let found = -1;
|
|
2148
|
+
for (let at = cursor;at + run.length <= limit; at++) {
|
|
2149
|
+
if (runMatchesAt(run, text, at)) {
|
|
2150
|
+
found = at;
|
|
2151
|
+
break;
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
if (found === -1)
|
|
2155
|
+
return false;
|
|
2156
|
+
cursor = found + run.length;
|
|
2157
|
+
}
|
|
2158
|
+
return true;
|
|
2159
|
+
}
|
|
2160
|
+
function globNeverMatches(glob) {
|
|
2161
|
+
for (let i = 0;i < glob.length; i++) {
|
|
2162
|
+
const c = glob[i];
|
|
2163
|
+
if (c === "\\") {
|
|
2164
|
+
i++;
|
|
2165
|
+
continue;
|
|
2166
|
+
}
|
|
2167
|
+
if (c !== "[")
|
|
2168
|
+
continue;
|
|
2169
|
+
const end = findClassEnd(glob, i);
|
|
2170
|
+
if (end === -1) {
|
|
2171
|
+
return "unclosed character class";
|
|
2172
|
+
}
|
|
2173
|
+
const body = glob.slice(i, end + 1);
|
|
2174
|
+
if (body === "[]" || body === "[^]")
|
|
2175
|
+
return "empty character class matches nothing";
|
|
2176
|
+
if (!isValidCharacterClass(body))
|
|
2177
|
+
return "invalid character class";
|
|
2178
|
+
i = end;
|
|
2179
|
+
}
|
|
2180
|
+
return null;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
// src/core/mongo/path-matcher.ts
|
|
2184
|
+
function compilePatterns(raw) {
|
|
2185
|
+
const patterns = [];
|
|
2186
|
+
const rejected = [];
|
|
2187
|
+
for (const entry of raw) {
|
|
2188
|
+
if (typeof entry !== "string" || entry.length === 0) {
|
|
2189
|
+
rejected.push({ raw: String(entry ?? ""), reason: "must be a non-empty string" });
|
|
2190
|
+
continue;
|
|
2191
|
+
}
|
|
2192
|
+
const segments = entry.split(".");
|
|
2193
|
+
if (segments.some((s) => s.length === 0)) {
|
|
2194
|
+
rejected.push({ raw: entry, reason: "empty path segment" });
|
|
2195
|
+
continue;
|
|
2196
|
+
}
|
|
2197
|
+
const globstar = segments.find((seg) => seg.includes("**"));
|
|
2198
|
+
if (globstar !== undefined) {
|
|
2199
|
+
rejected.push({
|
|
2200
|
+
raw: entry,
|
|
2201
|
+
reason: "`**` matches one segment, not a subtree; write `*` or a longer path"
|
|
2202
|
+
});
|
|
2203
|
+
continue;
|
|
2204
|
+
}
|
|
2205
|
+
const dead = segments.map((seg) => globNeverMatches(seg)).find((r) => r !== null);
|
|
2206
|
+
if (dead !== undefined && dead !== null) {
|
|
2207
|
+
rejected.push({ raw: entry, reason: dead });
|
|
2208
|
+
continue;
|
|
2209
|
+
}
|
|
2210
|
+
const wildcardTail = segments.length > 1 && segments[segments.length - 1] === "*";
|
|
2211
|
+
const literal = wildcardTail ? segments.slice(0, -1) : segments;
|
|
2212
|
+
patterns.push({
|
|
2213
|
+
raw: entry,
|
|
2214
|
+
segments: literal,
|
|
2215
|
+
wildcardTail
|
|
2216
|
+
});
|
|
2217
|
+
}
|
|
2218
|
+
return { patterns, rejected };
|
|
2219
|
+
}
|
|
2220
|
+
function matchAny(path, patterns) {
|
|
2221
|
+
if (patterns.length === 0)
|
|
2222
|
+
return false;
|
|
2223
|
+
const pathSegments = path.split(".");
|
|
2224
|
+
for (const pat of patterns) {
|
|
2225
|
+
if (pat.wildcardTail) {
|
|
2226
|
+
if (pathSegments.length < pat.segments.length)
|
|
2227
|
+
continue;
|
|
2228
|
+
} else {
|
|
2229
|
+
if (pat.segments.length !== pathSegments.length)
|
|
2230
|
+
continue;
|
|
2231
|
+
}
|
|
2232
|
+
let ok = true;
|
|
2233
|
+
for (let i = 0;i < pat.segments.length; i++) {
|
|
2234
|
+
if (!globMatches(pat.segments[i], pathSegments[i])) {
|
|
2235
|
+
ok = false;
|
|
2236
|
+
break;
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
if (ok)
|
|
2240
|
+
return true;
|
|
2241
|
+
}
|
|
2242
|
+
return false;
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2027
2245
|
// src/core/mongo/request-fields.ts
|
|
2028
2246
|
var WHOLE_DOCUMENT_VARIABLES = new Set(["ROOT", "CURRENT"]);
|
|
2029
2247
|
var RESHAPES_DOCUMENT = new Set(["$objectToArray", "$replaceRoot", "$replaceWith"]);
|
|
@@ -2468,57 +2686,6 @@ var REDIS_COMMAND_TABLE = Object.freeze({
|
|
|
2468
2686
|
})
|
|
2469
2687
|
});
|
|
2470
2688
|
|
|
2471
|
-
// src/utils/glob.ts
|
|
2472
|
-
function globToRegex(glob) {
|
|
2473
|
-
let out = "^";
|
|
2474
|
-
for (let i = 0;i < glob.length; i++) {
|
|
2475
|
-
const c = glob[i];
|
|
2476
|
-
if (c === "\\" && i + 1 < glob.length) {
|
|
2477
|
-
out += glob[++i].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2478
|
-
continue;
|
|
2479
|
-
}
|
|
2480
|
-
if (c === "*")
|
|
2481
|
-
out += ".*";
|
|
2482
|
-
else if (c === "?")
|
|
2483
|
-
out += ".";
|
|
2484
|
-
else if (c === "[") {
|
|
2485
|
-
const end = findClassEnd(glob, i);
|
|
2486
|
-
const body = end === -1 ? "" : glob.slice(i, end + 1);
|
|
2487
|
-
if (body !== "" && isValidCharacterClass(body)) {
|
|
2488
|
-
out += body;
|
|
2489
|
-
i = end;
|
|
2490
|
-
} else {
|
|
2491
|
-
out += "\\[";
|
|
2492
|
-
}
|
|
2493
|
-
} else if (".^$+(){}|\\".includes(c)) {
|
|
2494
|
-
out += "\\" + c;
|
|
2495
|
-
} else {
|
|
2496
|
-
out += c;
|
|
2497
|
-
}
|
|
2498
|
-
}
|
|
2499
|
-
out += "$";
|
|
2500
|
-
return new RegExp(out);
|
|
2501
|
-
}
|
|
2502
|
-
function findClassEnd(glob, open) {
|
|
2503
|
-
for (let i = open + 1;i < glob.length; i++) {
|
|
2504
|
-
if (glob[i] === "\\") {
|
|
2505
|
-
i++;
|
|
2506
|
-
continue;
|
|
2507
|
-
}
|
|
2508
|
-
if (glob[i] === "]")
|
|
2509
|
-
return i;
|
|
2510
|
-
}
|
|
2511
|
-
return -1;
|
|
2512
|
-
}
|
|
2513
|
-
function isValidCharacterClass(body) {
|
|
2514
|
-
try {
|
|
2515
|
-
new RegExp(body);
|
|
2516
|
-
return true;
|
|
2517
|
-
} catch {
|
|
2518
|
-
return false;
|
|
2519
|
-
}
|
|
2520
|
-
}
|
|
2521
|
-
|
|
2522
2689
|
// src/adapters/redis/returned-key-names.ts
|
|
2523
2690
|
var RETURNS_KEY_NAMES = new Set(["SCAN", "KEYS"]);
|
|
2524
2691
|
|
|
@@ -7977,10 +8144,11 @@ var RedisMaskRuleSchema = exports_external.object({
|
|
|
7977
8144
|
var RedisConfigSchema = exports_external.object({
|
|
7978
8145
|
mask: exports_external.array(RedisMaskRuleSchema).default([])
|
|
7979
8146
|
}).optional();
|
|
8147
|
+
var DEFAULT_AUDIT_ROTATION = { max_bytes: 10485760, max_entries: 1e4 };
|
|
7980
8148
|
var AuditRotationConfigSchema = exports_external.object({
|
|
7981
|
-
max_bytes: exports_external.number().int().positive().default(
|
|
7982
|
-
max_entries: exports_external.number().int().positive().default(
|
|
7983
|
-
}).optional().default({
|
|
8149
|
+
max_bytes: exports_external.number().int().positive().default(DEFAULT_AUDIT_ROTATION.max_bytes),
|
|
8150
|
+
max_entries: exports_external.number().int().positive().default(DEFAULT_AUDIT_ROTATION.max_entries)
|
|
8151
|
+
}).optional().default({ ...DEFAULT_AUDIT_ROTATION });
|
|
7984
8152
|
var AuditConfigSchema = exports_external.object({
|
|
7985
8153
|
enabled: exports_external.boolean().default(true),
|
|
7986
8154
|
strict: exports_external.boolean().default(false),
|
|
@@ -7991,7 +8159,7 @@ var AuditConfigSchema = exports_external.object({
|
|
|
7991
8159
|
}).optional().default({
|
|
7992
8160
|
enabled: true,
|
|
7993
8161
|
strict: false,
|
|
7994
|
-
rotation: {
|
|
8162
|
+
rotation: { ...DEFAULT_AUDIT_ROTATION }
|
|
7995
8163
|
});
|
|
7996
8164
|
var DbcliConfigSchema = exports_external.object({
|
|
7997
8165
|
connection: ConnectionConfigSchema,
|
|
@@ -8509,7 +8677,7 @@ function migrateV1ToV2(v1) {
|
|
|
8509
8677
|
schemas: {},
|
|
8510
8678
|
metadata: v1.metadata ?? { version: "2.0" },
|
|
8511
8679
|
blacklist: v1.blacklist ?? { tables: [], columns: {} },
|
|
8512
|
-
audit: v1.audit ?? { enabled: true, rotation: {
|
|
8680
|
+
audit: v1.audit ?? { enabled: true, rotation: { ...DEFAULT_AUDIT_ROTATION } }
|
|
8513
8681
|
};
|
|
8514
8682
|
}
|
|
8515
8683
|
function upsertConnection(config, input) {
|
|
@@ -8781,7 +8949,7 @@ var DEFAULT_CONFIG = {
|
|
|
8781
8949
|
audit: {
|
|
8782
8950
|
enabled: true,
|
|
8783
8951
|
strict: false,
|
|
8784
|
-
rotation: {
|
|
8952
|
+
rotation: { ...DEFAULT_AUDIT_ROTATION }
|
|
8785
8953
|
}
|
|
8786
8954
|
};
|
|
8787
8955
|
function isEnvReference(value) {
|
|
@@ -9036,6 +9204,23 @@ DBCLI_PASSWORD=${password}
|
|
|
9036
9204
|
}
|
|
9037
9205
|
};
|
|
9038
9206
|
// src/core/blacklist-manager.ts
|
|
9207
|
+
function normalizeBlacklistEntry(raw) {
|
|
9208
|
+
const trimmed = raw.trim();
|
|
9209
|
+
const quote = trimmed[0];
|
|
9210
|
+
if ((quote === '"' || quote === "`") && trimmed.length > 1 && trimmed.endsWith(quote)) {
|
|
9211
|
+
return trimmed.slice(1, -1).trim();
|
|
9212
|
+
}
|
|
9213
|
+
return trimmed;
|
|
9214
|
+
}
|
|
9215
|
+
function assertNotTableQualified(tableKey, column) {
|
|
9216
|
+
const dot = column.indexOf(".");
|
|
9217
|
+
if (dot <= 0)
|
|
9218
|
+
return;
|
|
9219
|
+
if (column.slice(0, dot).toLowerCase() !== tableKey.toLowerCase())
|
|
9220
|
+
return;
|
|
9221
|
+
throw new Error(`[BlacklistManager] blacklist.columns["${tableKey}"] entry ${JSON.stringify(column)} is ` + `qualified with its own table and would never match. Write it as ` + `${JSON.stringify(column.slice(dot + 1))}.`);
|
|
9222
|
+
}
|
|
9223
|
+
|
|
9039
9224
|
class BlacklistManager {
|
|
9040
9225
|
config;
|
|
9041
9226
|
state;
|
|
@@ -9044,6 +9229,7 @@ class BlacklistManager {
|
|
|
9044
9229
|
this.config = config;
|
|
9045
9230
|
this.overrideEnabled = (overrideEnvValue ?? Bun.env.DBCLI_OVERRIDE_BLACKLIST ?? "") === "true";
|
|
9046
9231
|
this.state = this.loadBlacklist();
|
|
9232
|
+
this.wildcardTables = [...this.state.tables].filter((entry) => /[*?[\\]/.test(entry));
|
|
9047
9233
|
}
|
|
9048
9234
|
loadBlacklist() {
|
|
9049
9235
|
const tables = new Set;
|
|
@@ -9055,7 +9241,7 @@ class BlacklistManager {
|
|
|
9055
9241
|
if (Array.isArray(blacklistConfig.tables)) {
|
|
9056
9242
|
for (const tableName of blacklistConfig.tables) {
|
|
9057
9243
|
if (typeof tableName === "string") {
|
|
9058
|
-
tables.add(tableName.toLowerCase());
|
|
9244
|
+
tables.add(normalizeBlacklistEntry(tableName).toLowerCase());
|
|
9059
9245
|
} else {
|
|
9060
9246
|
console.warn(`[BlacklistManager] Invalid table name in blacklist config: ${JSON.stringify(tableName)}`);
|
|
9061
9247
|
}
|
|
@@ -9076,13 +9262,15 @@ class BlacklistManager {
|
|
|
9076
9262
|
const columnSet = new Set;
|
|
9077
9263
|
for (const col of cols) {
|
|
9078
9264
|
if (typeof col === "string") {
|
|
9079
|
-
|
|
9265
|
+
const entry = normalizeBlacklistEntry(col);
|
|
9266
|
+
assertNotTableQualified(normalizeBlacklistEntry(tableName), entry);
|
|
9267
|
+
columnSet.add(entry);
|
|
9080
9268
|
} else {
|
|
9081
9269
|
console.warn(`[BlacklistManager] Invalid column name in blacklist.columns["${tableName}"]: ${JSON.stringify(col)}`);
|
|
9082
9270
|
}
|
|
9083
9271
|
}
|
|
9084
9272
|
if (columnSet.size > 0) {
|
|
9085
|
-
columns.set(tableName.toLowerCase(), columnSet);
|
|
9273
|
+
columns.set(normalizeBlacklistEntry(tableName).toLowerCase(), columnSet);
|
|
9086
9274
|
}
|
|
9087
9275
|
}
|
|
9088
9276
|
} else if (blacklistConfig.columns !== undefined) {
|
|
@@ -9091,17 +9279,47 @@ class BlacklistManager {
|
|
|
9091
9279
|
return { tables, columns };
|
|
9092
9280
|
}
|
|
9093
9281
|
isTableBlacklisted(tableName) {
|
|
9094
|
-
|
|
9282
|
+
const name = tableName.toLowerCase();
|
|
9283
|
+
if (this.state.tables.has(name))
|
|
9284
|
+
return true;
|
|
9285
|
+
for (const pattern of this.wildcardTables) {
|
|
9286
|
+
if (globMatches(pattern, name))
|
|
9287
|
+
return true;
|
|
9288
|
+
}
|
|
9289
|
+
return false;
|
|
9290
|
+
}
|
|
9291
|
+
wildcardTables;
|
|
9292
|
+
columnRulesFor(tableName) {
|
|
9293
|
+
const key = normalizeBlacklistEntry(tableName).toLowerCase();
|
|
9294
|
+
const direct = this.state.columns.get(key);
|
|
9295
|
+
if (direct)
|
|
9296
|
+
return direct;
|
|
9297
|
+
const dot = key.lastIndexOf(".");
|
|
9298
|
+
if (dot > 0) {
|
|
9299
|
+
const bare = this.state.columns.get(key.slice(dot + 1));
|
|
9300
|
+
if (bare)
|
|
9301
|
+
return bare;
|
|
9302
|
+
}
|
|
9303
|
+
let merged;
|
|
9304
|
+
for (const [ruleKey, columns] of this.state.columns) {
|
|
9305
|
+
const ruleDot = ruleKey.lastIndexOf(".");
|
|
9306
|
+
if (ruleDot <= 0 || ruleKey.slice(ruleDot + 1) !== key)
|
|
9307
|
+
continue;
|
|
9308
|
+
merged ??= new Set;
|
|
9309
|
+
for (const column of columns)
|
|
9310
|
+
merged.add(column);
|
|
9311
|
+
}
|
|
9312
|
+
return merged;
|
|
9095
9313
|
}
|
|
9096
9314
|
isColumnBlacklisted(tableName, columnName) {
|
|
9097
|
-
const columnSet = this.
|
|
9315
|
+
const columnSet = this.columnRulesFor(tableName);
|
|
9098
9316
|
if (!columnSet) {
|
|
9099
9317
|
return false;
|
|
9100
9318
|
}
|
|
9101
|
-
return columnSet.has(columnName);
|
|
9319
|
+
return columnSet.has(normalizeBlacklistEntry(columnName).toLowerCase());
|
|
9102
9320
|
}
|
|
9103
9321
|
getBlacklistedColumns(tableName) {
|
|
9104
|
-
const columnSet = this.
|
|
9322
|
+
const columnSet = this.columnRulesFor(tableName);
|
|
9105
9323
|
if (!columnSet) {
|
|
9106
9324
|
return [];
|
|
9107
9325
|
}
|
|
@@ -9165,7 +9383,7 @@ function expandIndexTargets(target) {
|
|
|
9165
9383
|
function matchesIndexGlob(pattern, name) {
|
|
9166
9384
|
const normalized = pattern.toLowerCase() === "_all" ? "*" : pattern.toLowerCase();
|
|
9167
9385
|
try {
|
|
9168
|
-
return
|
|
9386
|
+
return globMatches(normalized, name.toLowerCase());
|
|
9169
9387
|
} catch {
|
|
9170
9388
|
return true;
|
|
9171
9389
|
}
|
|
@@ -9220,6 +9438,17 @@ function dedupe2(values) {
|
|
|
9220
9438
|
}
|
|
9221
9439
|
return result;
|
|
9222
9440
|
}
|
|
9441
|
+
function compileGlobRules(rules, fold, table, operation) {
|
|
9442
|
+
const globbed = rules.filter((rule) => /[*?[\\]/.test(rule));
|
|
9443
|
+
if (globbed.length === 0)
|
|
9444
|
+
return [];
|
|
9445
|
+
const { patterns, rejected } = compilePatterns(globbed.map(fold));
|
|
9446
|
+
if (rejected.length > 0) {
|
|
9447
|
+
const detail = rejected.map((r) => `'${r.raw}' (${r.reason})`).join(", ");
|
|
9448
|
+
throw new BlacklistError(`blacklist.columns for '${table}' has entries this matcher cannot read: ${detail}`, table, operation);
|
|
9449
|
+
}
|
|
9450
|
+
return patterns;
|
|
9451
|
+
}
|
|
9223
9452
|
|
|
9224
9453
|
class BlacklistValidator {
|
|
9225
9454
|
manager;
|
|
@@ -9277,7 +9506,27 @@ class BlacklistValidator {
|
|
|
9277
9506
|
if (blacklisted.length === 0 || fields.length === 0) {
|
|
9278
9507
|
return;
|
|
9279
9508
|
}
|
|
9280
|
-
const
|
|
9509
|
+
const protectedPaths = new Set(blacklisted.map((name) => name.toLowerCase()));
|
|
9510
|
+
const globs = compileGlobRules(blacklisted, (value) => value.toLowerCase(), tableName, operation);
|
|
9511
|
+
const conflicts = fields.filter((field) => {
|
|
9512
|
+
const name = field.toLowerCase();
|
|
9513
|
+
if (protectedPaths.has(name))
|
|
9514
|
+
return true;
|
|
9515
|
+
if (matchAny(name, globs))
|
|
9516
|
+
return true;
|
|
9517
|
+
let dot = name.indexOf(".");
|
|
9518
|
+
while (dot >= 0) {
|
|
9519
|
+
if (dot > 0) {
|
|
9520
|
+
const ancestor = name.slice(0, dot);
|
|
9521
|
+
if (protectedPaths.has(ancestor))
|
|
9522
|
+
return true;
|
|
9523
|
+
if (matchAny(ancestor, globs))
|
|
9524
|
+
return true;
|
|
9525
|
+
}
|
|
9526
|
+
dot = name.indexOf(".", dot + 1);
|
|
9527
|
+
}
|
|
9528
|
+
return false;
|
|
9529
|
+
});
|
|
9281
9530
|
if (conflicts.length === 0) {
|
|
9282
9531
|
return;
|
|
9283
9532
|
}
|
|
@@ -9328,11 +9577,20 @@ class BlacklistValidator {
|
|
|
9328
9577
|
}
|
|
9329
9578
|
collect(row);
|
|
9330
9579
|
}
|
|
9331
|
-
const
|
|
9580
|
+
const foldHead = (path) => {
|
|
9581
|
+
const dot = path.indexOf(".");
|
|
9582
|
+
return dot < 0 ? path.toLowerCase() : path.slice(0, dot).toLowerCase() + path.slice(dot);
|
|
9583
|
+
};
|
|
9584
|
+
const protectedPaths = new Set(blacklistedColumns.map(foldHead));
|
|
9585
|
+
const globRules = compileGlobRules(blacklistedColumns, foldHead, tables[0] ?? "unknown", "SELECT");
|
|
9586
|
+
const presentByFolded = new Map;
|
|
9587
|
+
for (const column of presentColumns)
|
|
9588
|
+
presentByFolded.set(foldHead(column), column);
|
|
9332
9589
|
const omitted = new Set;
|
|
9333
9590
|
for (const path of blacklistedColumns) {
|
|
9334
|
-
|
|
9335
|
-
|
|
9591
|
+
const present = presentByFolded.get(foldHead(path));
|
|
9592
|
+
if (present !== undefined) {
|
|
9593
|
+
omitted.add(present);
|
|
9336
9594
|
continue;
|
|
9337
9595
|
}
|
|
9338
9596
|
const dot = path.indexOf(".");
|
|
@@ -9347,13 +9605,30 @@ class BlacklistValidator {
|
|
|
9347
9605
|
for (const column of presentColumns) {
|
|
9348
9606
|
let dot = column.indexOf(".");
|
|
9349
9607
|
while (dot >= 0) {
|
|
9350
|
-
if (dot > 0 && protectedPaths.has(column.slice(0, dot))) {
|
|
9608
|
+
if (dot > 0 && protectedPaths.has(column.slice(0, dot).toLowerCase())) {
|
|
9351
9609
|
omitted.add(column);
|
|
9352
9610
|
break;
|
|
9353
9611
|
}
|
|
9354
9612
|
dot = column.indexOf(".", dot + 1);
|
|
9355
9613
|
}
|
|
9356
9614
|
}
|
|
9615
|
+
if (globRules.length > 0) {
|
|
9616
|
+
for (const column of presentColumns) {
|
|
9617
|
+
const folded = foldHead(column);
|
|
9618
|
+
if (matchAny(folded, globRules)) {
|
|
9619
|
+
omitted.add(column);
|
|
9620
|
+
continue;
|
|
9621
|
+
}
|
|
9622
|
+
let dot = folded.indexOf(".");
|
|
9623
|
+
while (dot >= 0) {
|
|
9624
|
+
if (dot > 0 && matchAny(folded.slice(0, dot), globRules)) {
|
|
9625
|
+
omitted.add(column);
|
|
9626
|
+
break;
|
|
9627
|
+
}
|
|
9628
|
+
dot = folded.indexOf(".", dot + 1);
|
|
9629
|
+
}
|
|
9630
|
+
}
|
|
9631
|
+
}
|
|
9357
9632
|
const omittedColumns = Array.from(omitted);
|
|
9358
9633
|
if (omittedColumns.length === 0) {
|
|
9359
9634
|
return { filteredRows: rows, omittedColumns: [] };
|
package/gemini-extension.json
CHANGED
package/package.json
CHANGED