@carllee1983/dbcli 4.0.0 → 7.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/dist/core.mjs CHANGED
@@ -1323,6 +1323,7 @@ var messages_default = {
1323
1323
  unsupported_lang: "Language '{lang}' not supported, using English",
1324
1324
  invalid_json: "Invalid JSON: {message}",
1325
1325
  table_blacklisted: "Error: Table '{table}' is blacklisted for {operation} operations",
1326
+ redis_target_blacklisted: "Redis command refused by blacklist",
1326
1327
  column_blacklisted_write: "Error: {operation} on table '{table}' touches blacklisted columns: {columns}",
1327
1328
  table_already_blacklisted: "Error: Table '{table}' is already blacklisted",
1328
1329
  table_not_in_blacklist: "Error: Table '{table}' is not in the blacklist",
@@ -1414,7 +1415,8 @@ Reference: {referencePath}`,
1414
1415
  columns_omitted: "Security: {count} column(s) were omitted based on your blacklist"
1415
1416
  },
1416
1417
  warnings: {
1417
- blacklist_override_used: "Warning: Blacklist override enabled (DBCLI_OVERRIDE_BLACKLIST=true). Executing {operation} on blacklisted table '{table}'"
1418
+ blacklist_override_used: "Warning: Blacklist override enabled (DBCLI_OVERRIDE_BLACKLIST=true). Executing {operation} on blacklisted table '{table}'",
1419
+ redis_blacklist_override_active: "Warning: Redis blacklist enforcement disabled by DBCLI_OVERRIDE_BLACKLIST=true"
1418
1420
  },
1419
1421
  version: {
1420
1422
  unsupported_warning: "{system} {version} is below minimum supported version {minVersion}. Some features may not work correctly.",
@@ -1637,6 +1639,7 @@ var messages_default2 = {
1637
1639
  unsupported_lang: "\u4E0D\u652F\u63F4\u7684\u8A9E\u8A00 '{lang}'\uFF0C\u4F7F\u7528\u82F1\u6587",
1638
1640
  invalid_json: "\u7121\u6548\u7684 JSON\uFF1A{message}",
1639
1641
  table_blacklisted: "\u932F\u8AA4: \u8868\u683C '{table}' \u7684 {operation} \u64CD\u4F5C\u5DF2\u88AB\u9ED1\u540D\u55AE",
1642
+ redis_target_blacklisted: "Redis \u6307\u4EE4\u5DF2\u88AB\u9ED1\u540D\u55AE\u62D2\u7D55",
1640
1643
  column_blacklisted_write: "\u932F\u8AA4: \u8868\u683C '{table}' \u7684 {operation} \u64CD\u4F5C\u5305\u542B\u9ED1\u540D\u55AE\u6B04\u4F4D: {columns}",
1641
1644
  table_already_blacklisted: "\u932F\u8AA4: \u8868\u683C '{table}' \u5DF2\u5728\u9ED1\u540D\u55AE\u4E2D",
1642
1645
  table_not_in_blacklist: "\u932F\u8AA4: \u8868\u683C '{table}' \u4E0D\u5728\u9ED1\u540D\u55AE\u4E2D",
@@ -1728,7 +1731,8 @@ var messages_default2 = {
1728
1731
  columns_omitted: "\u5B89\u5168: {count} \u500B\u6B04\u4F4D\u6839\u64DA\u4F60\u7684\u9ED1\u540D\u55AE\u5DF2\u88AB\u96B1\u85CF"
1729
1732
  },
1730
1733
  warnings: {
1731
- blacklist_override_used: "\u8B66\u544A: \u5DF2\u555F\u7528\u9ED1\u540D\u55AE\u8986\u84CB (DBCLI_OVERRIDE_BLACKLIST=true)\u3002\u57F7\u884C {operation} \u5728\u5DF2\u9ED1\u540D\u55AE\u7684\u8868\u683C '{table}' \u4E0A"
1734
+ blacklist_override_used: "\u8B66\u544A: \u5DF2\u555F\u7528\u9ED1\u540D\u55AE\u8986\u84CB (DBCLI_OVERRIDE_BLACKLIST=true)\u3002\u57F7\u884C {operation} \u5728\u5DF2\u9ED1\u540D\u55AE\u7684\u8868\u683C '{table}' \u4E0A",
1735
+ redis_blacklist_override_active: "\u8B66\u544A: DBCLI_OVERRIDE_BLACKLIST=true \u5DF2\u505C\u7528 Redis \u9ED1\u540D\u55AE\u5F37\u5236\u57F7\u884C"
1732
1736
  },
1733
1737
  version: {
1734
1738
  unsupported_warning: "{system} {version} \u4F4E\u65BC\u6700\u4F4E\u652F\u63F4\u7248\u672C {minVersion}\u3002\u90E8\u5206\u529F\u80FD\u53EF\u80FD\u7121\u6CD5\u6B63\u5E38\u904B\u4F5C\u3002",
@@ -2024,6 +2028,263 @@ var messageLoader = MessageLoader.getInstance();
2024
2028
  var t = (key) => messageLoader.t(key);
2025
2029
  var t_vars = (key, vars) => messageLoader.interpolate(key, vars);
2026
2030
 
2031
+ // src/utils/case-fold.ts
2032
+ function foldCase(text) {
2033
+ const lowered = (text.includes("\u0130") ? text.replaceAll("\u0130", "i") : text).toLowerCase();
2034
+ return lowered.includes("\u03C2") ? lowered.replaceAll("\u03C2", "\u03C3") : lowered;
2035
+ }
2036
+
2037
+ // src/utils/glob.ts
2038
+ function findClassEnd(glob, open) {
2039
+ for (let i = open + 1;i < glob.length; i++) {
2040
+ if (glob[i] === "\\") {
2041
+ i++;
2042
+ continue;
2043
+ }
2044
+ if (glob[i] === "]")
2045
+ return i;
2046
+ }
2047
+ return -1;
2048
+ }
2049
+ function isValidCharacterClass(body) {
2050
+ try {
2051
+ new RegExp(body);
2052
+ return true;
2053
+ } catch {
2054
+ return false;
2055
+ }
2056
+ }
2057
+ var parsedGlobs = new Map;
2058
+ var PARSED_GLOB_LIMIT = 4096;
2059
+ function parseGlob(glob, caseInsensitive) {
2060
+ const key = `${caseInsensitive ? "i" : "c"}\x00${glob}`;
2061
+ const memo = parsedGlobs.get(key);
2062
+ if (memo !== undefined)
2063
+ return memo;
2064
+ const parsed = parseGlobUncached(glob, caseInsensitive);
2065
+ if (parsedGlobs.size >= PARSED_GLOB_LIMIT)
2066
+ parsedGlobs.clear();
2067
+ parsedGlobs.set(key, parsed);
2068
+ return parsed;
2069
+ }
2070
+ function parseGlobUncached(glob, caseInsensitive) {
2071
+ const runs = [];
2072
+ let current = [];
2073
+ let leadingStar = false;
2074
+ let trailingStar = false;
2075
+ let sawStar = false;
2076
+ const endRun = () => {
2077
+ if (current.length > 0)
2078
+ runs.push(current);
2079
+ current = [];
2080
+ };
2081
+ for (let i = 0;i < glob.length; i++) {
2082
+ const c = glob[i];
2083
+ if (c === "\\" && i + 1 < glob.length) {
2084
+ trailingStar = false;
2085
+ const escaped = String.fromCodePoint(glob.codePointAt(++i));
2086
+ i += escaped.length - 1;
2087
+ current.push(...literalToken(escaped, caseInsensitive));
2088
+ continue;
2089
+ }
2090
+ if (c === "*") {
2091
+ if (!sawStar && current.length === 0)
2092
+ leadingStar = true;
2093
+ sawStar = true;
2094
+ trailingStar = true;
2095
+ endRun();
2096
+ continue;
2097
+ }
2098
+ trailingStar = false;
2099
+ if (c === "?") {
2100
+ current.push({ kind: "any" });
2101
+ continue;
2102
+ }
2103
+ if (c === "[") {
2104
+ const end = findClassEnd(glob, i);
2105
+ const body = end === -1 ? "" : glob.slice(i, end + 1);
2106
+ if (body !== "" && isValidCharacterClass(body)) {
2107
+ current.push({
2108
+ kind: "class",
2109
+ test: new RegExp(`^${body}$`, caseInsensitive ? "si" : "s")
2110
+ });
2111
+ i = end;
2112
+ continue;
2113
+ }
2114
+ current.push(...literalToken("[", caseInsensitive));
2115
+ continue;
2116
+ }
2117
+ const literal = String.fromCodePoint(glob.codePointAt(i));
2118
+ i += literal.length - 1;
2119
+ current.push(...literalToken(literal, caseInsensitive));
2120
+ }
2121
+ endRun();
2122
+ return { runs, leadingStar, trailingStar, anchored: !sawStar };
2123
+ }
2124
+ function literalToken(codePoint, caseInsensitive) {
2125
+ const folded = caseInsensitive ? foldCase(codePoint) : codePoint;
2126
+ return folded.split("").map((unit) => ({ kind: "literal", char: unit }));
2127
+ }
2128
+ function runMatchesAt(run, text, raw, at) {
2129
+ if (at + run.length > text.length)
2130
+ return false;
2131
+ for (let i = 0;i < run.length; i++) {
2132
+ const token = run[i];
2133
+ if (token.kind === "literal") {
2134
+ if (token.char !== text[at + i])
2135
+ return false;
2136
+ } else if (token.kind === "class") {
2137
+ const rawChar = raw[at + i];
2138
+ if (rawChar === undefined) {
2139
+ throw new Error("globMatches: folded and unfolded subject lengths disagree");
2140
+ }
2141
+ if (!token.test.test(rawChar))
2142
+ return false;
2143
+ }
2144
+ }
2145
+ return true;
2146
+ }
2147
+ function globMatches(glob, subject, options) {
2148
+ const caseInsensitive = options?.caseInsensitive === true;
2149
+ const { runs, leadingStar, trailingStar, anchored } = parseGlob(glob, caseInsensitive);
2150
+ const text = caseInsensitive ? foldCase(subject) : subject;
2151
+ if (anchored) {
2152
+ const run = runs[0] ?? [];
2153
+ return text.length === run.length && runMatchesAt(run, text, subject, 0);
2154
+ }
2155
+ if (runs.length === 0)
2156
+ return true;
2157
+ let cursor = 0;
2158
+ let first = 0;
2159
+ let last = runs.length;
2160
+ if (!leadingStar) {
2161
+ const head = runs[0];
2162
+ if (!runMatchesAt(head, text, subject, 0))
2163
+ return false;
2164
+ cursor = head.length;
2165
+ first = 1;
2166
+ }
2167
+ if (!trailingStar) {
2168
+ const tail = runs[last - 1];
2169
+ const at = text.length - tail.length;
2170
+ if (at < cursor || !runMatchesAt(tail, text, subject, at))
2171
+ return false;
2172
+ last -= 1;
2173
+ }
2174
+ const limit = trailingStar ? text.length : text.length - runs[runs.length - 1].length;
2175
+ for (let r = first;r < last; r++) {
2176
+ const run = runs[r];
2177
+ let found = -1;
2178
+ for (let at = cursor;at + run.length <= limit; at++) {
2179
+ if (runMatchesAt(run, text, subject, at)) {
2180
+ found = at;
2181
+ break;
2182
+ }
2183
+ }
2184
+ if (found === -1)
2185
+ return false;
2186
+ cursor = found + run.length;
2187
+ }
2188
+ return true;
2189
+ }
2190
+ function isGlobPattern(value) {
2191
+ return /[*?[\\]/.test(value);
2192
+ }
2193
+ function globNeverMatches(glob) {
2194
+ for (let i = 0;i < glob.length; i++) {
2195
+ const c = glob[i];
2196
+ if (c === "\\") {
2197
+ i++;
2198
+ continue;
2199
+ }
2200
+ if (c !== "[")
2201
+ continue;
2202
+ const end = findClassEnd(glob, i);
2203
+ if (end === -1) {
2204
+ return "unclosed character class";
2205
+ }
2206
+ const body = glob.slice(i, end + 1);
2207
+ if (body === "[]" || body === "[^]")
2208
+ return "empty character class matches nothing";
2209
+ if (!isValidCharacterClass(body))
2210
+ return "invalid character class";
2211
+ i = end;
2212
+ }
2213
+ return null;
2214
+ }
2215
+
2216
+ // src/core/blacklist-fold.ts
2217
+ function foldFieldPath(path) {
2218
+ return foldCase(path);
2219
+ }
2220
+
2221
+ // src/core/mongo/path-matcher.ts
2222
+ function compilePatterns(raw) {
2223
+ const patterns = [];
2224
+ const rejected = [];
2225
+ for (const entry of raw) {
2226
+ if (typeof entry !== "string" || entry.length === 0) {
2227
+ rejected.push({ raw: String(entry ?? ""), reason: "must be a non-empty string" });
2228
+ continue;
2229
+ }
2230
+ const segments = entry.split(".");
2231
+ if (segments.some((s) => s.length === 0)) {
2232
+ rejected.push({ raw: entry, reason: "empty path segment" });
2233
+ continue;
2234
+ }
2235
+ const globstar = segments.find((seg) => seg.includes("**"));
2236
+ if (globstar !== undefined) {
2237
+ rejected.push({
2238
+ raw: entry,
2239
+ reason: "`**` matches one segment, not a subtree; write `*` or a longer path"
2240
+ });
2241
+ continue;
2242
+ }
2243
+ const dead = segments.map((seg) => globNeverMatches(seg)).find((r) => r !== null);
2244
+ if (dead !== undefined && dead !== null) {
2245
+ rejected.push({ raw: entry, reason: dead });
2246
+ continue;
2247
+ }
2248
+ const wildcardTail = segments.length > 1 && segments[segments.length - 1] === "*";
2249
+ const literal = wildcardTail ? segments.slice(0, -1) : segments;
2250
+ patterns.push({
2251
+ raw: entry,
2252
+ segments: literal,
2253
+ wildcardTail
2254
+ });
2255
+ }
2256
+ return { patterns, rejected };
2257
+ }
2258
+ var FOLD_CASE = { caseInsensitive: true };
2259
+ function matchSegment(pattern, segment) {
2260
+ return globMatches(pattern, segment, FOLD_CASE);
2261
+ }
2262
+ function matchAny(path, patterns) {
2263
+ if (patterns.length === 0)
2264
+ return false;
2265
+ const pathSegments = path.split(".");
2266
+ for (const pat of patterns) {
2267
+ if (pat.wildcardTail) {
2268
+ if (pathSegments.length < pat.segments.length)
2269
+ continue;
2270
+ } else {
2271
+ if (pat.segments.length !== pathSegments.length)
2272
+ continue;
2273
+ }
2274
+ let ok = true;
2275
+ for (let i = 0;i < pat.segments.length; i++) {
2276
+ if (!globMatches(pat.segments[i], pathSegments[i], FOLD_CASE)) {
2277
+ ok = false;
2278
+ break;
2279
+ }
2280
+ }
2281
+ if (ok)
2282
+ return true;
2283
+ }
2284
+ return false;
2285
+ }
2286
+ var compiledRuleSets = new WeakMap;
2287
+
2027
2288
  // src/core/mongo/request-fields.ts
2028
2289
  var WHOLE_DOCUMENT_VARIABLES = new Set(["ROOT", "CURRENT"]);
2029
2290
  var RESHAPES_DOCUMENT = new Set(["$objectToArray", "$replaceRoot", "$replaceWith"]);
@@ -2468,62 +2729,180 @@ var REDIS_COMMAND_TABLE = Object.freeze({
2468
2729
  })
2469
2730
  });
2470
2731
 
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;
2732
+ // src/adapters/redis/returned-key-names.ts
2733
+ var RETURNS_KEY_NAMES = new Set(["SCAN", "KEYS"]);
2734
+
2735
+ // src/adapters/redis/value-masker.ts
2736
+ var MASKABLE = new Set(["GET", "GETRANGE", "HGETALL", "HGET", "HMGET", "HVALS"]);
2737
+
2738
+ // src/core/blacklist-manager.ts
2739
+ function isBlacklistOverrideEnabled(value) {
2740
+ return (value ?? Bun.env.DBCLI_OVERRIDE_BLACKLIST ?? "") === "true";
2741
+ }
2742
+ function normalizeBlacklistEntry(raw) {
2743
+ const trimmed = raw.trim();
2744
+ const quote = trimmed[0];
2745
+ if ((quote === '"' || quote === "`") && trimmed.length > 1 && trimmed.endsWith(quote)) {
2746
+ return trimmed.slice(1, -1).trim();
2747
+ }
2748
+ return trimmed;
2749
+ }
2750
+ function assertNotTableQualified(tableKey, column) {
2751
+ const dot = column.indexOf(".");
2752
+ if (dot <= 0)
2753
+ return;
2754
+ if (foldFieldPath(column.slice(0, dot)) !== foldFieldPath(tableKey))
2755
+ return;
2756
+ 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))}.`);
2757
+ }
2758
+
2759
+ class BlacklistManager {
2760
+ config;
2761
+ state;
2762
+ overrideEnabled;
2763
+ rawTables = [];
2764
+ constructor(config, overrideEnvValue) {
2765
+ this.config = config;
2766
+ this.overrideEnabled = isBlacklistOverrideEnabled(overrideEnvValue);
2767
+ this.state = this.loadBlacklist();
2768
+ this.wildcardTables = this.rawTables.filter(isGlobPattern);
2769
+ }
2770
+ loadBlacklist() {
2771
+ this.rawTables.length = 0;
2772
+ const tables = new Set;
2773
+ const columns = new Map;
2774
+ const blacklistConfig = this.config.blacklist;
2775
+ if (!blacklistConfig) {
2776
+ return { tables, columns };
2479
2777
  }
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 += "\\[";
2778
+ if (Array.isArray(blacklistConfig.tables)) {
2779
+ for (const tableName of blacklistConfig.tables) {
2780
+ if (typeof tableName === "string") {
2781
+ const entry = normalizeBlacklistEntry(tableName);
2782
+ this.rawTables.push(entry);
2783
+ tables.add(foldFieldPath(entry));
2784
+ } else {
2785
+ console.warn(`[BlacklistManager] Invalid table name in blacklist config: ${JSON.stringify(tableName)}`);
2786
+ }
2492
2787
  }
2493
- } else if (".^$+(){}|\\".includes(c)) {
2494
- out += "\\" + c;
2495
- } else {
2496
- out += c;
2788
+ } else if (blacklistConfig.tables !== undefined) {
2789
+ console.warn("[BlacklistManager] blacklist.tables must be an array, ignoring");
2497
2790
  }
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;
2791
+ if (blacklistConfig.columns && typeof blacklistConfig.columns === "object" && !Array.isArray(blacklistConfig.columns)) {
2792
+ for (const [tableName, cols] of Object.entries(blacklistConfig.columns)) {
2793
+ if (typeof tableName !== "string") {
2794
+ console.warn(`[BlacklistManager] Invalid table name key in columns config: ${JSON.stringify(tableName)}`);
2795
+ continue;
2796
+ }
2797
+ if (!Array.isArray(cols)) {
2798
+ console.warn(`[BlacklistManager] blacklist.columns["${tableName}"] must be an array, ignoring`);
2799
+ continue;
2800
+ }
2801
+ const columnSet = new Set;
2802
+ for (const col of cols) {
2803
+ if (typeof col === "string") {
2804
+ const entry = normalizeBlacklistEntry(col);
2805
+ assertNotTableQualified(normalizeBlacklistEntry(tableName), entry);
2806
+ columnSet.add(entry);
2807
+ } else {
2808
+ console.warn(`[BlacklistManager] Invalid column name in blacklist.columns["${tableName}"]: ${JSON.stringify(col)}`);
2809
+ }
2810
+ }
2811
+ if (columnSet.size > 0) {
2812
+ columns.set(foldFieldPath(normalizeBlacklistEntry(tableName)), columnSet);
2813
+ }
2814
+ }
2815
+ } else if (blacklistConfig.columns !== undefined) {
2816
+ console.warn("[BlacklistManager] blacklist.columns must be an object, ignoring");
2507
2817
  }
2508
- if (glob[i] === "]")
2509
- return i;
2818
+ return { tables, columns };
2510
2819
  }
2511
- return -1;
2512
- }
2513
- function isValidCharacterClass(body) {
2514
- try {
2515
- new RegExp(body);
2516
- return true;
2517
- } catch {
2820
+ isTableBlacklisted(tableName) {
2821
+ const name = foldFieldPath(tableName);
2822
+ if (this.state.tables.has(name))
2823
+ return true;
2824
+ for (const pattern of this.wildcardTables) {
2825
+ if (globMatches(pattern, tableName, { caseInsensitive: true }))
2826
+ return true;
2827
+ }
2518
2828
  return false;
2519
2829
  }
2830
+ wildcardTables;
2831
+ columnRulesFor(tableName) {
2832
+ const key = foldFieldPath(normalizeBlacklistEntry(tableName));
2833
+ const direct = this.state.columns.get(key);
2834
+ if (direct)
2835
+ return direct;
2836
+ const dot = key.lastIndexOf(".");
2837
+ if (dot > 0) {
2838
+ const bare = this.state.columns.get(key.slice(dot + 1));
2839
+ if (bare)
2840
+ return bare;
2841
+ }
2842
+ let merged;
2843
+ for (const [ruleKey, columns] of this.state.columns) {
2844
+ const ruleDot = ruleKey.lastIndexOf(".");
2845
+ if (ruleDot <= 0 || ruleKey.slice(ruleDot + 1) !== key)
2846
+ continue;
2847
+ merged ??= new Set;
2848
+ for (const column of columns)
2849
+ merged.add(column);
2850
+ }
2851
+ return merged;
2852
+ }
2853
+ isColumnBlacklisted(tableName, columnName) {
2854
+ const columnSet = this.columnRulesFor(tableName);
2855
+ if (!columnSet) {
2856
+ return false;
2857
+ }
2858
+ const target = foldFieldPath(normalizeBlacklistEntry(columnName));
2859
+ const compiled = this.foldedColumnsFor(columnSet);
2860
+ if (compiled.literals.has(target))
2861
+ return true;
2862
+ return compiled.globs.length > 0 && matchAny(target, compiled.globs);
2863
+ }
2864
+ foldedColumns = new WeakMap;
2865
+ foldedColumnsFor(columnSet) {
2866
+ const memo = this.foldedColumns.get(columnSet);
2867
+ if (memo !== undefined)
2868
+ return memo;
2869
+ const literals = new Set;
2870
+ const globbed = [];
2871
+ for (const rule of columnSet) {
2872
+ if (isGlobPattern(rule))
2873
+ globbed.push(rule);
2874
+ else
2875
+ literals.add(foldFieldPath(rule));
2876
+ }
2877
+ const folded = {
2878
+ literals,
2879
+ globs: globbed.length > 0 ? compilePatterns(globbed).patterns : []
2880
+ };
2881
+ this.foldedColumns.set(columnSet, folded);
2882
+ return folded;
2883
+ }
2884
+ getBlacklistedColumns(tableName) {
2885
+ const columnSet = this.columnRulesFor(tableName);
2886
+ if (!columnSet) {
2887
+ return [];
2888
+ }
2889
+ return Array.from(columnSet);
2890
+ }
2891
+ getAllBlacklistedColumns() {
2892
+ const all = new Set;
2893
+ for (const columnSet of this.state.columns.values()) {
2894
+ for (const column of columnSet)
2895
+ all.add(column);
2896
+ }
2897
+ return Array.from(all);
2898
+ }
2899
+ canOverrideBlacklist() {
2900
+ return this.overrideEnabled;
2901
+ }
2902
+ getState() {
2903
+ return this.state;
2904
+ }
2520
2905
  }
2521
-
2522
- // src/adapters/redis/returned-key-names.ts
2523
- var RETURNS_KEY_NAMES = new Set(["SCAN", "KEYS"]);
2524
-
2525
- // src/adapters/redis/value-masker.ts
2526
- var MASKABLE = new Set(["GET", "GETRANGE", "HGETALL", "HGET", "HMGET", "HVALS"]);
2527
2906
  // src/utils/sql-lexical.ts
2528
2907
  var IDENTIFIER_CONTINUATION = /[A-Za-z0-9_$]|[\u0080-\uFFFF]/;
2529
2908
  var IDENTIFIER_START = /[A-Za-z_]|[\u0080-\uFFFF]/;
@@ -3444,18 +3823,139 @@ function projectRows(rows, selection) {
3444
3823
  const columnNames = collectColumnNames(projectedRows);
3445
3824
  return { rows: normalizeRows(projectedRows, columnNames), columnNames };
3446
3825
  }
3447
- function hasFieldPath(row, segments) {
3448
- return readPath(row, segments).found;
3826
+ function hasFieldPath(row, segments, options) {
3827
+ const caseInsensitive = options?.caseInsensitive === true;
3828
+ const folded = caseInsensitive && options?.alreadyFolded !== true ? segments.map(foldCase) : segments;
3829
+ return readPath(row, folded, caseInsensitive).found;
3830
+ }
3831
+ function narrow(rules, key) {
3832
+ const parts = key.includes(".") ? key.split(".") : [key];
3833
+ const live = [];
3834
+ for (const rule of rules) {
3835
+ const remaining = rule.segments.length;
3836
+ if (remaining < parts.length && !rule.wildcardTail)
3837
+ continue;
3838
+ let matches = true;
3839
+ for (let i = 0;i < Math.min(remaining, parts.length); i++) {
3840
+ if (!matchSegment(rule.segments[i], parts[i])) {
3841
+ matches = false;
3842
+ break;
3843
+ }
3844
+ }
3845
+ if (!matches)
3846
+ continue;
3847
+ if (remaining <= parts.length)
3848
+ return { drop: true, live: [] };
3849
+ live.push({
3850
+ root: rule.root,
3851
+ rootIndex: rule.rootIndex,
3852
+ offset: rule.offset + parts.length,
3853
+ segments: rule.segments.slice(parts.length),
3854
+ wildcardTail: rule.wildcardTail
3855
+ });
3856
+ }
3857
+ return { drop: false, live };
3858
+ }
3859
+ function liveRulesOf(patterns) {
3860
+ return patterns.map((pattern, index) => ({
3861
+ root: pattern,
3862
+ rootIndex: index,
3863
+ offset: 0,
3864
+ segments: pattern.segments,
3865
+ wildcardTail: pattern.wildcardTail
3866
+ }));
3449
3867
  }
3450
- function omitFieldPaths(rows, paths) {
3451
- const topLevel = new Set(paths);
3452
- const dotted = paths.filter((path) => path.includes(".")).map((path) => ({ head: path.slice(0, path.indexOf(".")), segments: path.split(".") }));
3868
+ var NARROW_STATE_LIMIT = 4096;
3869
+
3870
+ class NarrowStates {
3871
+ states = [];
3872
+ ids = new Map;
3873
+ transitions = new Map;
3874
+ constructor(rules) {
3875
+ this.intern(rules);
3876
+ }
3877
+ rules(state) {
3878
+ const rules = this.states[state];
3879
+ if (rules === undefined)
3880
+ throw new Error(`unknown nested-rule state ${state}`);
3881
+ return rules;
3882
+ }
3883
+ step(state, key) {
3884
+ const memoKey = `${state}\x00${key}`;
3885
+ const memo = this.transitions.get(memoKey);
3886
+ if (memo !== undefined)
3887
+ return memo;
3888
+ const { drop, live } = narrow(this.rules(state), key);
3889
+ const step = drop ? { drop: true } : { drop: false, next: this.intern(live) };
3890
+ if (this.transitions.size >= NARROW_STATE_LIMIT)
3891
+ this.transitions = new Map;
3892
+ this.transitions.set(memoKey, step);
3893
+ return step;
3894
+ }
3895
+ intern(rules) {
3896
+ const key = rules.map((rule) => `${rule.rootIndex}:${rule.offset}`).join(",");
3897
+ const existing = this.ids.get(key);
3898
+ if (existing !== undefined)
3899
+ return existing;
3900
+ const id = this.states.length;
3901
+ this.states.push(rules);
3902
+ this.ids.set(key, id);
3903
+ return id;
3904
+ }
3905
+ }
3906
+ function collectNestedMatches(row, states, pending, onMatch) {
3907
+ if (pending.size === 0)
3908
+ return;
3909
+ const walk = (value, state, depth) => {
3910
+ if (pending.size === 0 || states.rules(state).length === 0)
3911
+ return;
3912
+ if (Array.isArray(value)) {
3913
+ for (const item of value)
3914
+ walk(item, state, depth);
3915
+ return;
3916
+ }
3917
+ if (!isPlainRecord(value))
3918
+ return;
3919
+ for (const key of Object.getOwnPropertyNames(value)) {
3920
+ const step = states.step(state, key);
3921
+ if (step.drop) {
3922
+ if (depth > 0) {
3923
+ for (const rule of states.rules(state)) {
3924
+ if (!pending.has(rule.root))
3925
+ continue;
3926
+ if (!narrow([rule], key).drop)
3927
+ continue;
3928
+ pending.delete(rule.root);
3929
+ onMatch(rule.root);
3930
+ }
3931
+ }
3932
+ continue;
3933
+ }
3934
+ walk(value[key], step.next, depth + 1);
3935
+ }
3936
+ };
3937
+ walk(row, 0, 0);
3938
+ }
3939
+ function compileNestedRules(patterns) {
3940
+ return new NarrowStates(liveRulesOf(patterns));
3941
+ }
3942
+ function omitFieldPaths(rows, paths, options) {
3943
+ const caseInsensitive = options?.caseInsensitive === true;
3944
+ const nestedPatterns = options?.patterns ?? [];
3945
+ const nestedStates = nestedPatterns.length > 0 ? compileNestedRules(nestedPatterns) : undefined;
3946
+ const fold = (value) => caseInsensitive ? foldCase(value) : value;
3947
+ const topLevel = new Set(paths.map(fold));
3948
+ const dotted = paths.filter((path) => path.includes(".")).map((path) => ({ head: fold(path.slice(0, path.indexOf("."))), segments: path.split(".") }));
3453
3949
  const maskRecord = (row) => {
3454
- let projected = cloneRecord(row, topLevel);
3950
+ let projected = cloneRecord(row, topLevel, caseInsensitive);
3455
3951
  for (const { head, segments } of dotted) {
3456
- const value = row[head];
3457
- if (value !== null && typeof value === "object")
3458
- projected = omitPath(projected, segments);
3952
+ const value = headValue(row, head, caseInsensitive);
3953
+ if (value !== null && typeof value === "object") {
3954
+ projected = omitPath(projected, segments, caseInsensitive);
3955
+ }
3956
+ }
3957
+ if (nestedStates !== undefined && hasNestedRecord(row)) {
3958
+ projected = omitMatching(projected, nestedStates, 0);
3459
3959
  }
3460
3960
  return projected;
3461
3961
  };
@@ -3475,11 +3975,38 @@ function maskArrayRow(row, maskRecord) {
3475
3975
  });
3476
3976
  return out;
3477
3977
  }
3478
- function readPath(value, segments) {
3978
+ function headValue(row, head, caseInsensitive) {
3979
+ if (!caseInsensitive)
3980
+ return row[head];
3981
+ const key = foldedKeys(row).get(head);
3982
+ return key === undefined ? undefined : row[key];
3983
+ }
3984
+ var foldedKeyIndex = new WeakMap;
3985
+ function foldedKeys(value) {
3986
+ const memo = foldedKeyIndex.get(value);
3987
+ if (memo !== undefined)
3988
+ return memo;
3989
+ const index = new Map;
3990
+ for (const key of Object.getOwnPropertyNames(value)) {
3991
+ const folded = foldCase(key);
3992
+ if (!index.has(folded))
3993
+ index.set(folded, key);
3994
+ }
3995
+ foldedKeyIndex.set(value, index);
3996
+ return index;
3997
+ }
3998
+ function ownKey(value, name, caseInsensitive) {
3999
+ if (Object.prototype.hasOwnProperty.call(value, name))
4000
+ return name;
4001
+ if (!caseInsensitive)
4002
+ return;
4003
+ return foldedKeys(value).get(name);
4004
+ }
4005
+ function readPath(value, segments, caseInsensitive = false) {
3479
4006
  if (segments.length === 0)
3480
4007
  return { found: true, value };
3481
4008
  if (Array.isArray(value)) {
3482
- const results = value.map((item) => readPath(item, segments));
4009
+ const results = value.map((item) => readPath(item, segments, caseInsensitive));
3483
4010
  return {
3484
4011
  found: results.some((result) => result.found),
3485
4012
  value: results.map((result) => result.found ? result.value : undefined)
@@ -3487,41 +4014,68 @@ function readPath(value, segments) {
3487
4014
  }
3488
4015
  if (!isRecord(value))
3489
4016
  return { found: false };
3490
- const exactPath = segments.join(".");
3491
- if (Object.prototype.hasOwnProperty.call(value, exactPath)) {
3492
- return { found: true, value: value[exactPath] };
3493
- }
4017
+ const exact = ownKey(value, segments.join("."), caseInsensitive);
4018
+ if (exact !== undefined)
4019
+ return { found: true, value: value[exact] };
3494
4020
  const [head, ...tail] = segments;
3495
- if (!Object.prototype.hasOwnProperty.call(value, head))
4021
+ const headKey = ownKey(value, head, caseInsensitive);
4022
+ if (headKey === undefined)
3496
4023
  return { found: false };
3497
- return readPath(value[head], tail);
4024
+ return readPath(value[headKey], tail, caseInsensitive);
4025
+ }
4026
+ function omitMatching(value, states, state) {
4027
+ if (states.rules(state).length === 0)
4028
+ return value;
4029
+ if (Array.isArray(value))
4030
+ return value.map((item) => omitMatching(item, states, state));
4031
+ if (!isPlainRecord(value))
4032
+ return value;
4033
+ const out = {};
4034
+ for (const [key, child] of Object.entries(value)) {
4035
+ const step = states.step(state, key);
4036
+ if (step.drop)
4037
+ continue;
4038
+ defineData(out, key, omitMatching(child, states, step.next));
4039
+ }
4040
+ return out;
3498
4041
  }
3499
- function omitPath(value, segments) {
4042
+ function hasNestedRecord(row) {
4043
+ for (const key of Object.getOwnPropertyNames(row)) {
4044
+ const value = row[key];
4045
+ if (Array.isArray(value) || isPlainRecord(value))
4046
+ return true;
4047
+ }
4048
+ return false;
4049
+ }
4050
+ function omitPath(value, segments, caseInsensitive) {
3500
4051
  if (segments.length === 0)
3501
4052
  return value;
3502
4053
  if (Array.isArray(value))
3503
- return value.map((item) => omitPath(item, segments));
4054
+ return value.map((item) => omitPath(item, segments, caseInsensitive));
3504
4055
  if (!isPlainRecord(value))
3505
4056
  return value;
3506
- const exactPath = segments.join(".");
3507
- const [head, ...tail] = segments;
4057
+ const fold = (name) => caseInsensitive ? foldCase(name) : name;
4058
+ const exactPath = fold(segments.join("."));
4059
+ const head = fold(segments[0]);
4060
+ const tail = segments.slice(1);
3508
4061
  const out = {};
3509
4062
  for (const [key, child] of Object.entries(value)) {
3510
- if (key === exactPath)
4063
+ const folded = fold(key);
4064
+ if (folded === exactPath)
3511
4065
  continue;
3512
- if (key === head) {
4066
+ if (folded === head) {
3513
4067
  if (tail.length > 0)
3514
- defineData(out, key, omitPath(child, tail));
4068
+ defineData(out, key, omitPath(child, tail, caseInsensitive));
3515
4069
  continue;
3516
4070
  }
3517
4071
  defineData(out, key, child);
3518
4072
  }
3519
4073
  return out;
3520
4074
  }
3521
- function cloneRecord(value, omittedKeys) {
4075
+ function cloneRecord(value, omittedKeys, caseInsensitive = false) {
3522
4076
  const out = {};
3523
4077
  for (const [key, child] of Object.entries(value)) {
3524
- if (!omittedKeys.has(key))
4078
+ if (!omittedKeys.has(caseInsensitive ? foldCase(key) : key))
3525
4079
  defineData(out, key, child);
3526
4080
  }
3527
4081
  return out;
@@ -3627,12 +4181,12 @@ class QueryExecutor {
3627
4181
  let columnNames = rows.length > 0 && rows[0] ? Object.keys(rows[0]) : [];
3628
4182
  let filteredRows = rows;
3629
4183
  let securityNotification;
3630
- let omittedColumns = [];
4184
+ let reachesOmitted = () => false;
3631
4185
  if (this.blacklistValidator) {
3632
4186
  const filterResult = this.blacklistValidator.filterColumnsForTables(referencedTables, rows, columnNames);
3633
4187
  filteredRows = filterResult.filteredRows;
3634
4188
  if (filterResult.omittedColumns.length > 0) {
3635
- omittedColumns = filterResult.omittedColumns;
4189
+ reachesOmitted = filterResult.reachesOmitted;
3636
4190
  columnNames = columnNames.filter((col) => !filterResult.omittedColumns.includes(col));
3637
4191
  securityNotification = this.blacklistValidator.buildSecurityNotification(referencedTables[0] ?? "", filterResult.omittedColumns);
3638
4192
  }
@@ -3640,7 +4194,7 @@ class QueryExecutor {
3640
4194
  if (options?.fieldSelection) {
3641
4195
  const fieldSelection = options.fieldSelection.mode === "include" ? {
3642
4196
  mode: "include",
3643
- paths: options.fieldSelection.paths.filter((path) => !omittedColumns.some((omitted) => path === omitted || path.startsWith(`${omitted}.`)))
4197
+ paths: options.fieldSelection.paths.filter((path) => !reachesOmitted(path))
3644
4198
  } : options.fieldSelection;
3645
4199
  const projection = projectRows(filteredRows, fieldSelection);
3646
4200
  filteredRows = projection.rows;
@@ -7977,10 +8531,11 @@ var RedisMaskRuleSchema = exports_external.object({
7977
8531
  var RedisConfigSchema = exports_external.object({
7978
8532
  mask: exports_external.array(RedisMaskRuleSchema).default([])
7979
8533
  }).optional();
8534
+ var DEFAULT_AUDIT_ROTATION = { max_bytes: 10485760, max_entries: 1e4 };
7980
8535
  var AuditRotationConfigSchema = exports_external.object({
7981
- max_bytes: exports_external.number().int().positive().default(10485760),
7982
- max_entries: exports_external.number().int().positive().default(1000)
7983
- }).optional().default({ max_bytes: 10485760, max_entries: 1000 });
8536
+ max_bytes: exports_external.number().int().positive().default(DEFAULT_AUDIT_ROTATION.max_bytes),
8537
+ max_entries: exports_external.number().int().positive().default(DEFAULT_AUDIT_ROTATION.max_entries)
8538
+ }).optional().default({ ...DEFAULT_AUDIT_ROTATION });
7984
8539
  var AuditConfigSchema = exports_external.object({
7985
8540
  enabled: exports_external.boolean().default(true),
7986
8541
  strict: exports_external.boolean().default(false),
@@ -7991,7 +8546,7 @@ var AuditConfigSchema = exports_external.object({
7991
8546
  }).optional().default({
7992
8547
  enabled: true,
7993
8548
  strict: false,
7994
- rotation: { max_bytes: 10485760, max_entries: 1000 }
8549
+ rotation: { ...DEFAULT_AUDIT_ROTATION }
7995
8550
  });
7996
8551
  var DbcliConfigSchema = exports_external.object({
7997
8552
  connection: ConnectionConfigSchema,
@@ -8509,7 +9064,7 @@ function migrateV1ToV2(v1) {
8509
9064
  schemas: {},
8510
9065
  metadata: v1.metadata ?? { version: "2.0" },
8511
9066
  blacklist: v1.blacklist ?? { tables: [], columns: {} },
8512
- audit: v1.audit ?? { enabled: true, rotation: { max_bytes: 10485760, max_entries: 1000 } }
9067
+ audit: v1.audit ?? { enabled: true, rotation: { ...DEFAULT_AUDIT_ROTATION } }
8513
9068
  };
8514
9069
  }
8515
9070
  function upsertConnection(config, input) {
@@ -8781,7 +9336,7 @@ var DEFAULT_CONFIG = {
8781
9336
  audit: {
8782
9337
  enabled: true,
8783
9338
  strict: false,
8784
- rotation: { max_bytes: 10485760, max_entries: 1000 }
9339
+ rotation: { ...DEFAULT_AUDIT_ROTATION }
8785
9340
  }
8786
9341
  };
8787
9342
  function isEnvReference(value) {
@@ -9035,93 +9590,6 @@ DBCLI_PASSWORD=${password}
9035
9590
  }
9036
9591
  }
9037
9592
  };
9038
- // src/core/blacklist-manager.ts
9039
- class BlacklistManager {
9040
- config;
9041
- state;
9042
- overrideEnabled;
9043
- constructor(config, overrideEnvValue) {
9044
- this.config = config;
9045
- this.overrideEnabled = (overrideEnvValue ?? Bun.env.DBCLI_OVERRIDE_BLACKLIST ?? "") === "true";
9046
- this.state = this.loadBlacklist();
9047
- }
9048
- loadBlacklist() {
9049
- const tables = new Set;
9050
- const columns = new Map;
9051
- const blacklistConfig = this.config.blacklist;
9052
- if (!blacklistConfig) {
9053
- return { tables, columns };
9054
- }
9055
- if (Array.isArray(blacklistConfig.tables)) {
9056
- for (const tableName of blacklistConfig.tables) {
9057
- if (typeof tableName === "string") {
9058
- tables.add(tableName.toLowerCase());
9059
- } else {
9060
- console.warn(`[BlacklistManager] Invalid table name in blacklist config: ${JSON.stringify(tableName)}`);
9061
- }
9062
- }
9063
- } else if (blacklistConfig.tables !== undefined) {
9064
- console.warn("[BlacklistManager] blacklist.tables must be an array, ignoring");
9065
- }
9066
- if (blacklistConfig.columns && typeof blacklistConfig.columns === "object" && !Array.isArray(blacklistConfig.columns)) {
9067
- for (const [tableName, cols] of Object.entries(blacklistConfig.columns)) {
9068
- if (typeof tableName !== "string") {
9069
- console.warn(`[BlacklistManager] Invalid table name key in columns config: ${JSON.stringify(tableName)}`);
9070
- continue;
9071
- }
9072
- if (!Array.isArray(cols)) {
9073
- console.warn(`[BlacklistManager] blacklist.columns["${tableName}"] must be an array, ignoring`);
9074
- continue;
9075
- }
9076
- const columnSet = new Set;
9077
- for (const col of cols) {
9078
- if (typeof col === "string") {
9079
- columnSet.add(col);
9080
- } else {
9081
- console.warn(`[BlacklistManager] Invalid column name in blacklist.columns["${tableName}"]: ${JSON.stringify(col)}`);
9082
- }
9083
- }
9084
- if (columnSet.size > 0) {
9085
- columns.set(tableName.toLowerCase(), columnSet);
9086
- }
9087
- }
9088
- } else if (blacklistConfig.columns !== undefined) {
9089
- console.warn("[BlacklistManager] blacklist.columns must be an object, ignoring");
9090
- }
9091
- return { tables, columns };
9092
- }
9093
- isTableBlacklisted(tableName) {
9094
- return this.state.tables.has(tableName.toLowerCase());
9095
- }
9096
- isColumnBlacklisted(tableName, columnName) {
9097
- const columnSet = this.state.columns.get(tableName.toLowerCase());
9098
- if (!columnSet) {
9099
- return false;
9100
- }
9101
- return columnSet.has(columnName);
9102
- }
9103
- getBlacklistedColumns(tableName) {
9104
- const columnSet = this.state.columns.get(tableName.toLowerCase());
9105
- if (!columnSet) {
9106
- return [];
9107
- }
9108
- return Array.from(columnSet);
9109
- }
9110
- getAllBlacklistedColumns() {
9111
- const all = new Set;
9112
- for (const columnSet of this.state.columns.values()) {
9113
- for (const column of columnSet)
9114
- all.add(column);
9115
- }
9116
- return Array.from(all);
9117
- }
9118
- canOverrideBlacklist() {
9119
- return this.overrideEnabled;
9120
- }
9121
- getState() {
9122
- return this.state;
9123
- }
9124
- }
9125
9593
  // src/utils/es-index-target.ts
9126
9594
  var MAX_DECODE_PASSES = 4;
9127
9595
  function unwrap(target) {
@@ -9163,16 +9631,16 @@ function expandIndexTargets(target) {
9163
9631
  return { concrete, wildcards };
9164
9632
  }
9165
9633
  function matchesIndexGlob(pattern, name) {
9166
- const normalized = pattern.toLowerCase() === "_all" ? "*" : pattern.toLowerCase();
9634
+ const normalized = pattern.toLowerCase() === "_all" ? "*" : pattern;
9167
9635
  try {
9168
- return globToRegex(normalized).test(name.toLowerCase());
9636
+ return globMatches(normalized, name, { caseInsensitive: true });
9169
9637
  } catch {
9170
9638
  return true;
9171
9639
  }
9172
9640
  }
9173
9641
  function reachesByConvention(name, entry) {
9174
- const lower = name.toLowerCase();
9175
- const target = entry.toLowerCase();
9642
+ const lower = foldFieldPath(name);
9643
+ const target = foldFieldPath(entry);
9176
9644
  return new RegExp(`^\\.ds-${escapeRegExp(target)}-`).test(lower) || new RegExp(`^${escapeRegExp(target)}-\\d+$`).test(lower);
9177
9645
  }
9178
9646
  function escapeRegExp(text) {
@@ -9193,11 +9661,13 @@ function indexExpressionReaches(expression, blacklisted) {
9193
9661
  return false;
9194
9662
  const { concrete, wildcards } = expandIndexTargets(expression);
9195
9663
  const entries = expandBlacklistEntries(blacklisted);
9196
- const reachesName = (name) => entries.names.some((entry) => entry.toLowerCase() === name.toLowerCase() || reachesByConvention(name, entry)) || entries.patterns.some((pattern) => matchesIndexGlob(pattern, name));
9664
+ const reachesName = (name) => entries.names.some((entry) => foldFieldPath(entry) === foldFieldPath(name) || reachesByConvention(name, entry)) || entries.patterns.some((pattern) => matchesIndexGlob(pattern, name));
9197
9665
  return concrete.some(reachesName) || wildcards.some((pattern) => entries.names.some((entry) => matchesIndexGlob(pattern, entry)) || entries.patterns.length > 0);
9198
9666
  }
9199
9667
 
9200
9668
  // src/core/blacklist-validator.ts
9669
+ var FOLD_CASE2 = { caseInsensitive: true };
9670
+ var PRE_FOLDED = { caseInsensitive: true, alreadyFolded: true };
9201
9671
  function flattenArrayRow(row) {
9202
9672
  const records = [];
9203
9673
  for (const item of row) {
@@ -9212,7 +9682,7 @@ function dedupe2(values) {
9212
9682
  const seen = new Set;
9213
9683
  const result = [];
9214
9684
  for (const value of values) {
9215
- const key = value.toLowerCase();
9685
+ const key = foldFieldPath(value);
9216
9686
  if (value.length === 0 || seen.has(key))
9217
9687
  continue;
9218
9688
  seen.add(key);
@@ -9220,6 +9690,20 @@ function dedupe2(values) {
9220
9690
  }
9221
9691
  return result;
9222
9692
  }
9693
+ function omittedByName(omittedColumns, path) {
9694
+ return omittedColumns.some((omitted) => path === omitted || path.startsWith(`${omitted}.`));
9695
+ }
9696
+ function compileGlobRules(rules, table, operation) {
9697
+ const globbed = rules.filter(isGlobPattern);
9698
+ if (globbed.length === 0)
9699
+ return [];
9700
+ const { patterns, rejected } = compilePatterns(globbed);
9701
+ if (rejected.length > 0) {
9702
+ const detail = rejected.map((r) => `'${r.raw}' (${r.reason})`).join(", ");
9703
+ throw new BlacklistError(`blacklist.columns for '${table}' has entries this matcher cannot read: ${detail}`, table, operation);
9704
+ }
9705
+ return patterns;
9706
+ }
9223
9707
 
9224
9708
  class BlacklistValidator {
9225
9709
  manager;
@@ -9277,7 +9761,27 @@ class BlacklistValidator {
9277
9761
  if (blacklisted.length === 0 || fields.length === 0) {
9278
9762
  return;
9279
9763
  }
9280
- const conflicts = fields.filter((f) => blacklisted.includes(f));
9764
+ const protectedPaths = new Set(blacklisted.map(foldFieldPath));
9765
+ const globs = compileGlobRules(blacklisted, tableName, operation);
9766
+ const conflicts = fields.filter((field) => {
9767
+ const name = foldFieldPath(field);
9768
+ if (protectedPaths.has(name))
9769
+ return true;
9770
+ if (matchAny(name, globs))
9771
+ return true;
9772
+ let dot = name.indexOf(".");
9773
+ while (dot >= 0) {
9774
+ if (dot > 0) {
9775
+ const ancestor = name.slice(0, dot);
9776
+ if (protectedPaths.has(ancestor))
9777
+ return true;
9778
+ if (matchAny(ancestor, globs))
9779
+ return true;
9780
+ }
9781
+ dot = name.indexOf(".", dot + 1);
9782
+ }
9783
+ return false;
9784
+ });
9281
9785
  if (conflicts.length === 0) {
9282
9786
  return;
9283
9787
  }
@@ -9303,7 +9807,7 @@ class BlacklistValidator {
9303
9807
  const tables = dedupe2(tableNames);
9304
9808
  const blacklistedColumns = tables.length === 0 ? this.manager.getAllBlacklistedColumns() : Array.from(new Set(tables.flatMap((table) => this.manager.getBlacklistedColumns(table))));
9305
9809
  if (blacklistedColumns.length === 0) {
9306
- return { filteredRows: rows, omittedColumns: [] };
9810
+ return { filteredRows: rows, omittedColumns: [], reachesOmitted: () => false };
9307
9811
  }
9308
9812
  const probeNested = blacklistedColumns.some((path) => path.includes("."));
9309
9813
  const presentColumns = new Set(columnList);
@@ -9315,7 +9819,7 @@ class BlacklistValidator {
9315
9819
  continue;
9316
9820
  const value = record[key];
9317
9821
  if (value !== null && typeof value === "object")
9318
- nestedHeads.add(key);
9822
+ nestedHeads.add(foldFieldPath(key));
9319
9823
  }
9320
9824
  };
9321
9825
  for (const row of rows) {
@@ -9328,38 +9832,103 @@ class BlacklistValidator {
9328
9832
  }
9329
9833
  collect(row);
9330
9834
  }
9331
- const protectedPaths = new Set(blacklistedColumns);
9835
+ const protectedPaths = new Set(blacklistedColumns.map(foldFieldPath));
9836
+ const globRules = compileGlobRules(blacklistedColumns, tables[0] ?? "unknown", "SELECT");
9837
+ const presentByFolded = new Map;
9838
+ for (const column of presentColumns) {
9839
+ const folded = foldFieldPath(column);
9840
+ const names = presentByFolded.get(folded);
9841
+ if (names === undefined)
9842
+ presentByFolded.set(folded, [column]);
9843
+ else
9844
+ names.push(column);
9845
+ }
9332
9846
  const omitted = new Set;
9333
9847
  for (const path of blacklistedColumns) {
9334
- if (presentColumns.has(path)) {
9335
- omitted.add(path);
9848
+ const present = presentByFolded.get(foldFieldPath(path));
9849
+ if (present !== undefined) {
9850
+ for (const name of present)
9851
+ omitted.add(name);
9336
9852
  continue;
9337
9853
  }
9338
9854
  const dot = path.indexOf(".");
9339
9855
  if (dot < 0)
9340
9856
  continue;
9341
- if (!nestedHeads.has(path.slice(0, dot)))
9857
+ if (!nestedHeads.has(foldFieldPath(path.slice(0, dot))))
9342
9858
  continue;
9343
- const segments = path.split(".");
9344
- if (rows.some((row) => hasFieldPath(row, segments)))
9859
+ const segments = path.split(".").map(foldFieldPath);
9860
+ if (rows.some((row) => hasFieldPath(row, segments, PRE_FOLDED)))
9345
9861
  omitted.add(path);
9346
9862
  }
9863
+ const nestedGlobs = globRules.filter((pattern) => pattern.segments.length > 1);
9864
+ const matchedNested = [];
9865
+ if (nestedGlobs.length > 0 && nestedHeads.size > 0) {
9866
+ const pending = new Set(nestedGlobs);
9867
+ const states = compileNestedRules(nestedGlobs);
9868
+ for (const row of rows) {
9869
+ if (pending.size === 0)
9870
+ break;
9871
+ if (row === null || typeof row !== "object")
9872
+ continue;
9873
+ collectNestedMatches(row, states, pending, (pattern) => {
9874
+ matchedNested.push(pattern);
9875
+ omitted.add(pattern.raw);
9876
+ });
9877
+ }
9878
+ }
9347
9879
  for (const column of presentColumns) {
9348
9880
  let dot = column.indexOf(".");
9349
9881
  while (dot >= 0) {
9350
- if (dot > 0 && protectedPaths.has(column.slice(0, dot))) {
9882
+ if (dot > 0 && protectedPaths.has(foldFieldPath(column.slice(0, dot)))) {
9351
9883
  omitted.add(column);
9352
9884
  break;
9353
9885
  }
9354
9886
  dot = column.indexOf(".", dot + 1);
9355
9887
  }
9356
9888
  }
9889
+ if (globRules.length > 0) {
9890
+ for (const column of presentColumns) {
9891
+ const folded = foldFieldPath(column);
9892
+ if (matchAny(folded, globRules)) {
9893
+ omitted.add(column);
9894
+ continue;
9895
+ }
9896
+ let dot = folded.indexOf(".");
9897
+ while (dot >= 0) {
9898
+ if (dot > 0 && matchAny(folded.slice(0, dot), globRules)) {
9899
+ omitted.add(column);
9900
+ break;
9901
+ }
9902
+ dot = folded.indexOf(".", dot + 1);
9903
+ }
9904
+ }
9905
+ }
9357
9906
  const omittedColumns = Array.from(omitted);
9358
9907
  if (omittedColumns.length === 0) {
9359
- return { filteredRows: rows, omittedColumns: [] };
9908
+ return { filteredRows: rows, omittedColumns: [], reachesOmitted: () => false };
9360
9909
  }
9361
- const filteredRows = omitFieldPaths(rows, omittedColumns);
9362
- return { filteredRows, omittedColumns };
9910
+ const removalPaths = matchedNested.length === 0 ? omittedColumns : omittedColumns.filter((path) => !matchedNested.some((p) => p.raw === path));
9911
+ const filteredRows = omitFieldPaths(rows, removalPaths, matchedNested.length > 0 ? { ...FOLD_CASE2, patterns: matchedNested } : FOLD_CASE2);
9912
+ return {
9913
+ filteredRows,
9914
+ omittedColumns,
9915
+ reachesOmitted: (path) => {
9916
+ if (omittedByName(removalPaths, path))
9917
+ return true;
9918
+ if (matchedNested.length === 0)
9919
+ return false;
9920
+ const folded = foldFieldPath(path);
9921
+ if (matchAny(folded, matchedNested))
9922
+ return true;
9923
+ let dot = folded.indexOf(".");
9924
+ while (dot > 0) {
9925
+ if (matchAny(folded.slice(0, dot), matchedNested))
9926
+ return true;
9927
+ dot = folded.indexOf(".", dot + 1);
9928
+ }
9929
+ return false;
9930
+ }
9931
+ };
9363
9932
  }
9364
9933
  buildSecurityNotification(_tableName, omittedColumns) {
9365
9934
  if (omittedColumns.length === 0) {