@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.
@@ -51,7 +51,7 @@ var package_default;
51
51
  var init_package = __esm(() => {
52
52
  package_default = {
53
53
  name: "@carllee1983/dbcli",
54
- version: "4.0.0",
54
+ version: "7.0.0",
55
55
  description: "Database CLI for AI agents",
56
56
  type: "module",
57
57
  publishConfig: {
@@ -183,7 +183,9 @@ var init_package = __esm(() => {
183
183
  },
184
184
  overrides: {
185
185
  "brace-expansion": "^5.0.9",
186
- postcss: "^8.5.26"
186
+ browserslist: "^4.28.7",
187
+ postcss: "^8.5.26",
188
+ "postcss-selector-parser": "^6.1.3"
187
189
  }
188
190
  };
189
191
  });
@@ -4372,7 +4374,7 @@ function validateFormat(value, allowedFormats, commandName) {
4372
4374
  throw new Error(`Invalid format "${value}" for ${commandName}. Allowed: ${allowed}`);
4373
4375
  }
4374
4376
  }
4375
- var EnvRefSchema, StringOrEnvRef, NumberOrEnvRef, OptStringOrEnvRef, OptNumberOrEnvRef, MIN_CONNECTION_TIMEOUT_MS = 100, MAX_CONNECTION_TIMEOUT_MS = 600000, MIN_STATEMENT_TIMEOUT_MS = 0, MAX_STATEMENT_TIMEOUT_MS = 3600000, TimeoutField, MongoDBConnectionConfigSchema, SqlConnectionConfigSchema, RedisConnectionConfigSchema, ElasticsearchConnectionConfigSchema, ConnectionConfigSchema, PermissionSchema, EnvironmentLabelSchema, MetadataSchema, BLACKLIST_KEY_ALIASES, BlacklistConfigSchema, RedisMaskRuleSchema, RedisConfigSchema, AuditRotationConfigSchema, AuditConfigSchema, DbcliConfigSchema, SqlNamedConnectionSchema, MongoDBNamedConnectionSchema, RedisNamedConnectionSchema, ElasticsearchNamedConnectionSchema, NamedConnectionUnion, NamedConnectionSchema, DbcliConfigV2Schema;
4377
+ var EnvRefSchema, StringOrEnvRef, NumberOrEnvRef, OptStringOrEnvRef, OptNumberOrEnvRef, MIN_CONNECTION_TIMEOUT_MS = 100, MAX_CONNECTION_TIMEOUT_MS = 600000, MIN_STATEMENT_TIMEOUT_MS = 0, MAX_STATEMENT_TIMEOUT_MS = 3600000, TimeoutField, MongoDBConnectionConfigSchema, SqlConnectionConfigSchema, RedisConnectionConfigSchema, ElasticsearchConnectionConfigSchema, ConnectionConfigSchema, PermissionSchema, EnvironmentLabelSchema, MetadataSchema, BLACKLIST_KEY_ALIASES, BlacklistConfigSchema, RedisMaskRuleSchema, RedisConfigSchema, DEFAULT_AUDIT_ROTATION, AuditRotationConfigSchema, AuditConfigSchema, DbcliConfigSchema, SqlNamedConnectionSchema, MongoDBNamedConnectionSchema, RedisNamedConnectionSchema, ElasticsearchNamedConnectionSchema, NamedConnectionUnion, NamedConnectionSchema, DbcliConfigV2Schema;
4376
4378
  var init_validation = __esm(() => {
4377
4379
  init_zod();
4378
4380
  EnvRefSchema = exports_external.object({
@@ -4474,10 +4476,11 @@ var init_validation = __esm(() => {
4474
4476
  RedisConfigSchema = exports_external.object({
4475
4477
  mask: exports_external.array(RedisMaskRuleSchema).default([])
4476
4478
  }).optional();
4479
+ DEFAULT_AUDIT_ROTATION = { max_bytes: 10485760, max_entries: 1e4 };
4477
4480
  AuditRotationConfigSchema = exports_external.object({
4478
- max_bytes: exports_external.number().int().positive().default(10485760),
4479
- max_entries: exports_external.number().int().positive().default(1000)
4480
- }).optional().default({ max_bytes: 10485760, max_entries: 1000 });
4481
+ max_bytes: exports_external.number().int().positive().default(DEFAULT_AUDIT_ROTATION.max_bytes),
4482
+ max_entries: exports_external.number().int().positive().default(DEFAULT_AUDIT_ROTATION.max_entries)
4483
+ }).optional().default({ ...DEFAULT_AUDIT_ROTATION });
4481
4484
  AuditConfigSchema = exports_external.object({
4482
4485
  enabled: exports_external.boolean().default(true),
4483
4486
  strict: exports_external.boolean().default(false),
@@ -4488,7 +4491,7 @@ var init_validation = __esm(() => {
4488
4491
  }).optional().default({
4489
4492
  enabled: true,
4490
4493
  strict: false,
4491
- rotation: { max_bytes: 10485760, max_entries: 1000 }
4494
+ rotation: { ...DEFAULT_AUDIT_ROTATION }
4492
4495
  });
4493
4496
  DbcliConfigSchema = exports_external.object({
4494
4497
  connection: ConnectionConfigSchema,
@@ -6321,7 +6324,7 @@ var init_config = __esm(() => {
6321
6324
  audit: {
6322
6325
  enabled: true,
6323
6326
  strict: false,
6324
- rotation: { max_bytes: 10485760, max_entries: 1000 }
6327
+ rotation: { ...DEFAULT_AUDIT_ROTATION }
6325
6328
  }
6326
6329
  };
6327
6330
  configModule = {
@@ -9018,6 +9021,7 @@ var init_messages = __esm(() => {
9018
9021
  unsupported_lang: "Language '{lang}' not supported, using English",
9019
9022
  invalid_json: "Invalid JSON: {message}",
9020
9023
  table_blacklisted: "Error: Table '{table}' is blacklisted for {operation} operations",
9024
+ redis_target_blacklisted: "Redis command refused by blacklist",
9021
9025
  column_blacklisted_write: "Error: {operation} on table '{table}' touches blacklisted columns: {columns}",
9022
9026
  table_already_blacklisted: "Error: Table '{table}' is already blacklisted",
9023
9027
  table_not_in_blacklist: "Error: Table '{table}' is not in the blacklist",
@@ -9109,7 +9113,8 @@ Reference: {referencePath}`,
9109
9113
  columns_omitted: "Security: {count} column(s) were omitted based on your blacklist"
9110
9114
  },
9111
9115
  warnings: {
9112
- blacklist_override_used: "Warning: Blacklist override enabled (DBCLI_OVERRIDE_BLACKLIST=true). Executing {operation} on blacklisted table '{table}'"
9116
+ blacklist_override_used: "Warning: Blacklist override enabled (DBCLI_OVERRIDE_BLACKLIST=true). Executing {operation} on blacklisted table '{table}'",
9117
+ redis_blacklist_override_active: "Warning: Redis blacklist enforcement disabled by DBCLI_OVERRIDE_BLACKLIST=true"
9113
9118
  },
9114
9119
  version: {
9115
9120
  unsupported_warning: "{system} {version} is below minimum supported version {minVersion}. Some features may not work correctly.",
@@ -9336,6 +9341,7 @@ var init_messages2 = __esm(() => {
9336
9341
  unsupported_lang: "\u4E0D\u652F\u63F4\u7684\u8A9E\u8A00 '{lang}'\uFF0C\u4F7F\u7528\u82F1\u6587",
9337
9342
  invalid_json: "\u7121\u6548\u7684 JSON\uFF1A{message}",
9338
9343
  table_blacklisted: "\u932F\u8AA4: \u8868\u683C '{table}' \u7684 {operation} \u64CD\u4F5C\u5DF2\u88AB\u9ED1\u540D\u55AE",
9344
+ redis_target_blacklisted: "Redis \u6307\u4EE4\u5DF2\u88AB\u9ED1\u540D\u55AE\u62D2\u7D55",
9339
9345
  column_blacklisted_write: "\u932F\u8AA4: \u8868\u683C '{table}' \u7684 {operation} \u64CD\u4F5C\u5305\u542B\u9ED1\u540D\u55AE\u6B04\u4F4D: {columns}",
9340
9346
  table_already_blacklisted: "\u932F\u8AA4: \u8868\u683C '{table}' \u5DF2\u5728\u9ED1\u540D\u55AE\u4E2D",
9341
9347
  table_not_in_blacklist: "\u932F\u8AA4: \u8868\u683C '{table}' \u4E0D\u5728\u9ED1\u540D\u55AE\u4E2D",
@@ -9427,7 +9433,8 @@ var init_messages2 = __esm(() => {
9427
9433
  columns_omitted: "\u5B89\u5168: {count} \u500B\u6B04\u4F4D\u6839\u64DA\u4F60\u7684\u9ED1\u540D\u55AE\u5DF2\u88AB\u96B1\u85CF"
9428
9434
  },
9429
9435
  warnings: {
9430
- 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"
9436
+ 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",
9437
+ redis_blacklist_override_active: "\u8B66\u544A: DBCLI_OVERRIDE_BLACKLIST=true \u5DF2\u505C\u7528 Redis \u9ED1\u540D\u55AE\u5F37\u5236\u57F7\u884C"
9431
9438
  },
9432
9439
  version: {
9433
9440
  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",
@@ -11164,31 +11171,349 @@ var init_server_side_script = __esm(() => {
11164
11171
  ES_OPAQUE_BODY_KEYS = ["wrapper"];
11165
11172
  });
11166
11173
 
11167
- // src/core/mongo/request-fields.ts
11168
- function asFieldPath(text2) {
11169
- if (text2.startsWith("$$"))
11170
- return text2.slice(2);
11171
- if (text2.startsWith("$"))
11172
- return text2.slice(1);
11173
- return text2;
11174
+ // src/utils/case-fold.ts
11175
+ function foldCase(text2) {
11176
+ const lowered = (text2.includes("\u0130") ? text2.replaceAll("\u0130", "i") : text2).toLowerCase();
11177
+ return lowered.includes("\u03C2") ? lowered.replaceAll("\u03C2", "\u03C3") : lowered;
11178
+ }
11179
+
11180
+ // src/utils/glob.ts
11181
+ function findClassEnd(glob, open2) {
11182
+ for (let i = open2 + 1;i < glob.length; i++) {
11183
+ if (glob[i] === "\\") {
11184
+ i++;
11185
+ continue;
11186
+ }
11187
+ if (glob[i] === "]")
11188
+ return i;
11189
+ }
11190
+ return -1;
11174
11191
  }
11175
- function reachesProtectedField(path, protectedFields) {
11176
- if (protectedFields.has(path))
11192
+ function isValidCharacterClass(body) {
11193
+ try {
11194
+ new RegExp(body);
11177
11195
  return true;
11178
- const parts = path.split(".");
11179
- if (parts.length === 1)
11196
+ } catch {
11180
11197
  return false;
11181
- for (let start = 0;start < parts.length; start += 1) {
11182
- for (let end = start + 1;end <= parts.length; end += 1) {
11183
- if (protectedFields.has(parts.slice(start, end).join(".")))
11198
+ }
11199
+ }
11200
+ function parseGlob(glob, caseInsensitive) {
11201
+ const key = `${caseInsensitive ? "i" : "c"}\x00${glob}`;
11202
+ const memo = parsedGlobs.get(key);
11203
+ if (memo !== undefined)
11204
+ return memo;
11205
+ const parsed = parseGlobUncached(glob, caseInsensitive);
11206
+ if (parsedGlobs.size >= PARSED_GLOB_LIMIT)
11207
+ parsedGlobs.clear();
11208
+ parsedGlobs.set(key, parsed);
11209
+ return parsed;
11210
+ }
11211
+ function parseGlobUncached(glob, caseInsensitive) {
11212
+ const runs = [];
11213
+ let current = [];
11214
+ let leadingStar = false;
11215
+ let trailingStar = false;
11216
+ let sawStar = false;
11217
+ const endRun = () => {
11218
+ if (current.length > 0)
11219
+ runs.push(current);
11220
+ current = [];
11221
+ };
11222
+ for (let i = 0;i < glob.length; i++) {
11223
+ const c = glob[i];
11224
+ if (c === "\\" && i + 1 < glob.length) {
11225
+ trailingStar = false;
11226
+ const escaped = String.fromCodePoint(glob.codePointAt(++i));
11227
+ i += escaped.length - 1;
11228
+ current.push(...literalToken(escaped, caseInsensitive));
11229
+ continue;
11230
+ }
11231
+ if (c === "*") {
11232
+ if (!sawStar && current.length === 0)
11233
+ leadingStar = true;
11234
+ sawStar = true;
11235
+ trailingStar = true;
11236
+ endRun();
11237
+ continue;
11238
+ }
11239
+ trailingStar = false;
11240
+ if (c === "?") {
11241
+ current.push({ kind: "any" });
11242
+ continue;
11243
+ }
11244
+ if (c === "[") {
11245
+ const end = findClassEnd(glob, i);
11246
+ const body = end === -1 ? "" : glob.slice(i, end + 1);
11247
+ if (body !== "" && isValidCharacterClass(body)) {
11248
+ current.push({
11249
+ kind: "class",
11250
+ test: new RegExp(`^${body}$`, caseInsensitive ? "si" : "s")
11251
+ });
11252
+ i = end;
11253
+ continue;
11254
+ }
11255
+ current.push(...literalToken("[", caseInsensitive));
11256
+ continue;
11257
+ }
11258
+ const literal = String.fromCodePoint(glob.codePointAt(i));
11259
+ i += literal.length - 1;
11260
+ current.push(...literalToken(literal, caseInsensitive));
11261
+ }
11262
+ endRun();
11263
+ return { runs, leadingStar, trailingStar, anchored: !sawStar };
11264
+ }
11265
+ function literalToken(codePoint, caseInsensitive) {
11266
+ const folded = caseInsensitive ? foldCase(codePoint) : codePoint;
11267
+ return folded.split("").map((unit) => ({ kind: "literal", char: unit }));
11268
+ }
11269
+ function runMatchesAt(run, text2, raw, at) {
11270
+ if (at + run.length > text2.length)
11271
+ return false;
11272
+ for (let i = 0;i < run.length; i++) {
11273
+ const token = run[i];
11274
+ if (token.kind === "literal") {
11275
+ if (token.char !== text2[at + i])
11276
+ return false;
11277
+ } else if (token.kind === "class") {
11278
+ const rawChar = raw[at + i];
11279
+ if (rawChar === undefined) {
11280
+ throw new Error("globMatches: folded and unfolded subject lengths disagree");
11281
+ }
11282
+ if (!token.test.test(rawChar))
11283
+ return false;
11284
+ }
11285
+ }
11286
+ return true;
11287
+ }
11288
+ function globMatches(glob, subject, options) {
11289
+ const caseInsensitive = options?.caseInsensitive === true;
11290
+ const { runs, leadingStar, trailingStar, anchored } = parseGlob(glob, caseInsensitive);
11291
+ const text2 = caseInsensitive ? foldCase(subject) : subject;
11292
+ if (anchored) {
11293
+ const run = runs[0] ?? [];
11294
+ return text2.length === run.length && runMatchesAt(run, text2, subject, 0);
11295
+ }
11296
+ if (runs.length === 0)
11297
+ return true;
11298
+ let cursor = 0;
11299
+ let first = 0;
11300
+ let last = runs.length;
11301
+ if (!leadingStar) {
11302
+ const head = runs[0];
11303
+ if (!runMatchesAt(head, text2, subject, 0))
11304
+ return false;
11305
+ cursor = head.length;
11306
+ first = 1;
11307
+ }
11308
+ if (!trailingStar) {
11309
+ const tail = runs[last - 1];
11310
+ const at = text2.length - tail.length;
11311
+ if (at < cursor || !runMatchesAt(tail, text2, subject, at))
11312
+ return false;
11313
+ last -= 1;
11314
+ }
11315
+ const limit = trailingStar ? text2.length : text2.length - runs[runs.length - 1].length;
11316
+ for (let r = first;r < last; r++) {
11317
+ const run = runs[r];
11318
+ let found = -1;
11319
+ for (let at = cursor;at + run.length <= limit; at++) {
11320
+ if (runMatchesAt(run, text2, subject, at)) {
11321
+ found = at;
11322
+ break;
11323
+ }
11324
+ }
11325
+ if (found === -1)
11326
+ return false;
11327
+ cursor = found + run.length;
11328
+ }
11329
+ return true;
11330
+ }
11331
+ function isGlobPattern(value) {
11332
+ return /[*?[\\]/.test(value);
11333
+ }
11334
+ function escapeGlob(literal) {
11335
+ return literal.replace(/[*?[\]\\]/g, "\\$&");
11336
+ }
11337
+ function globNeverMatches(glob) {
11338
+ for (let i = 0;i < glob.length; i++) {
11339
+ const c = glob[i];
11340
+ if (c === "\\") {
11341
+ i++;
11342
+ continue;
11343
+ }
11344
+ if (c !== "[")
11345
+ continue;
11346
+ const end = findClassEnd(glob, i);
11347
+ if (end === -1) {
11348
+ return "unclosed character class";
11349
+ }
11350
+ const body = glob.slice(i, end + 1);
11351
+ if (body === "[]" || body === "[^]")
11352
+ return "empty character class matches nothing";
11353
+ if (!isValidCharacterClass(body))
11354
+ return "invalid character class";
11355
+ i = end;
11356
+ }
11357
+ return null;
11358
+ }
11359
+ var parsedGlobs, PARSED_GLOB_LIMIT = 4096;
11360
+ var init_glob = __esm(() => {
11361
+ parsedGlobs = new Map;
11362
+ });
11363
+
11364
+ // src/core/blacklist-fold.ts
11365
+ function foldFieldPath(path) {
11366
+ return foldCase(path);
11367
+ }
11368
+ var init_blacklist_fold = () => {};
11369
+
11370
+ // src/core/mongo/path-matcher.ts
11371
+ function compilePatterns(raw) {
11372
+ const patterns = [];
11373
+ const rejected = [];
11374
+ for (const entry of raw) {
11375
+ if (typeof entry !== "string" || entry.length === 0) {
11376
+ rejected.push({ raw: String(entry ?? ""), reason: "must be a non-empty string" });
11377
+ continue;
11378
+ }
11379
+ const segments = entry.split(".");
11380
+ if (segments.some((s) => s.length === 0)) {
11381
+ rejected.push({ raw: entry, reason: "empty path segment" });
11382
+ continue;
11383
+ }
11384
+ const globstar = segments.find((seg) => seg.includes("**"));
11385
+ if (globstar !== undefined) {
11386
+ rejected.push({
11387
+ raw: entry,
11388
+ reason: "`**` matches one segment, not a subtree; write `*` or a longer path"
11389
+ });
11390
+ continue;
11391
+ }
11392
+ const dead = segments.map((seg) => globNeverMatches(seg)).find((r) => r !== null);
11393
+ if (dead !== undefined && dead !== null) {
11394
+ rejected.push({ raw: entry, reason: dead });
11395
+ continue;
11396
+ }
11397
+ const wildcardTail = segments.length > 1 && segments[segments.length - 1] === "*";
11398
+ const literal = wildcardTail ? segments.slice(0, -1) : segments;
11399
+ patterns.push({
11400
+ raw: entry,
11401
+ segments: literal,
11402
+ wildcardTail
11403
+ });
11404
+ }
11405
+ return { patterns, rejected };
11406
+ }
11407
+ function matchSegment(pattern, segment) {
11408
+ return globMatches(pattern, segment, FOLD_CASE);
11409
+ }
11410
+ function matchAny(path, patterns) {
11411
+ if (patterns.length === 0)
11412
+ return false;
11413
+ const pathSegments = path.split(".");
11414
+ for (const pat of patterns) {
11415
+ if (pat.wildcardTail) {
11416
+ if (pathSegments.length < pat.segments.length)
11417
+ continue;
11418
+ } else {
11419
+ if (pat.segments.length !== pathSegments.length)
11420
+ continue;
11421
+ }
11422
+ let ok = true;
11423
+ for (let i = 0;i < pat.segments.length; i++) {
11424
+ if (!globMatches(pat.segments[i], pathSegments[i], FOLD_CASE)) {
11425
+ ok = false;
11426
+ break;
11427
+ }
11428
+ }
11429
+ if (ok)
11430
+ return true;
11431
+ }
11432
+ return false;
11433
+ }
11434
+ function contiguousRules(literals, globs) {
11435
+ const widths = new Set;
11436
+ for (const rule of literals)
11437
+ widths.add(rule.split(".").length);
11438
+ return { literals, globs, literalWidths: [...widths].sort((a, b) => a - b) };
11439
+ }
11440
+ function reachesProtectedSegments(segments, rules) {
11441
+ const { literals, globs, literalWidths } = rules;
11442
+ const depth = segments.length;
11443
+ for (let start = 0;start < depth; start++) {
11444
+ for (const width of literalWidths) {
11445
+ const end = start + width;
11446
+ if (end > depth)
11447
+ break;
11448
+ const candidate = width === 1 ? segments[start] : segments.slice(start, end).join(".");
11449
+ if (literals.has(candidate))
11450
+ return true;
11451
+ }
11452
+ for (const pattern of globs) {
11453
+ const width = pattern.segments.length;
11454
+ if (start + width > depth)
11455
+ continue;
11456
+ let matched = true;
11457
+ for (let i = 0;i < width; i++) {
11458
+ if (!matchSegment(pattern.segments[i], segments[start + i])) {
11459
+ matched = false;
11460
+ break;
11461
+ }
11462
+ }
11463
+ if (matched)
11184
11464
  return true;
11185
11465
  }
11186
11466
  }
11187
11467
  return false;
11188
11468
  }
11469
+ function reachesProtectedPath(path, rules) {
11470
+ return reachesProtectedSegments(path.split("."), rules);
11471
+ }
11472
+ function contiguousRulesFor(protectedFields) {
11473
+ const memo = compiledRuleSets.get(protectedFields);
11474
+ if (memo !== undefined)
11475
+ return memo;
11476
+ const globbed = [];
11477
+ const literals = new Set;
11478
+ for (const rule of protectedFields) {
11479
+ if (isGlobPattern(rule))
11480
+ globbed.push(rule);
11481
+ else
11482
+ literals.add(foldFieldPath(rule));
11483
+ }
11484
+ let globs = [];
11485
+ if (globbed.length > 0) {
11486
+ const compiled = compilePatterns(globbed);
11487
+ if (compiled.rejected.length > 0) {
11488
+ const detail = compiled.rejected.map((r) => `'${r.raw}' (${r.reason})`).join(", ");
11489
+ throw new Error(`BlacklistRejection: blacklist entries this matcher cannot read: ${detail}`);
11490
+ }
11491
+ globs = compiled.patterns;
11492
+ }
11493
+ const rules = contiguousRules(literals, globs);
11494
+ compiledRuleSets.set(protectedFields, rules);
11495
+ return rules;
11496
+ }
11497
+ var FOLD_CASE, compiledRuleSets;
11498
+ var init_path_matcher = __esm(() => {
11499
+ init_glob();
11500
+ init_blacklist_fold();
11501
+ FOLD_CASE = { caseInsensitive: true };
11502
+ compiledRuleSets = new WeakMap;
11503
+ });
11504
+
11505
+ // src/core/mongo/request-fields.ts
11506
+ function asFieldPath(text2) {
11507
+ if (text2.startsWith("$$"))
11508
+ return text2.slice(2);
11509
+ if (text2.startsWith("$"))
11510
+ return text2.slice(1);
11511
+ return text2;
11512
+ }
11189
11513
  function findProtectedFieldReference(request, protectedFields) {
11190
11514
  if (protectedFields.size === 0)
11191
11515
  return;
11516
+ const rules = contiguousRulesFor(protectedFields);
11192
11517
  const candidates = [];
11193
11518
  let movesWholeDocument = false;
11194
11519
  const walk = (node) => {
@@ -11222,7 +11547,7 @@ function findProtectedFieldReference(request, protectedFields) {
11222
11547
  }
11223
11548
  };
11224
11549
  walk(request);
11225
- const named = candidates.find((path) => path.length > 0 && reachesProtectedField(path, protectedFields));
11550
+ const named = candidates.find((path) => path.length > 0 && reachesProtectedPath(foldFieldPath(path), rules));
11226
11551
  if (named !== undefined)
11227
11552
  return named;
11228
11553
  return movesWholeDocument ? [...protectedFields][0] : undefined;
@@ -11245,10 +11570,12 @@ function protectedFieldsForRequest(request, collection, columns) {
11245
11570
  collectStrings(value);
11246
11571
  };
11247
11572
  collectStrings(request);
11248
- const namedLower = new Set([...named].map((value) => value.toLowerCase()));
11573
+ const namedFolded = new Set([...named].map(foldFieldPath));
11574
+ const collectionFolded = foldFieldPath(collection);
11249
11575
  const fields = new Set;
11250
11576
  for (const [name, rules] of Object.entries(columns)) {
11251
- const applies = name.toLowerCase() === collection.toLowerCase() || namedLower.has(name.toLowerCase());
11577
+ const folded = foldFieldPath(name);
11578
+ const applies = folded === collectionFolded || namedFolded.has(folded);
11252
11579
  if (!applies)
11253
11580
  continue;
11254
11581
  for (const rule of rules) {
@@ -11261,6 +11588,8 @@ function protectedFieldsForRequest(request, collection, columns) {
11261
11588
  }
11262
11589
  var WHOLE_DOCUMENT_VARIABLES, RESHAPES_DOCUMENT, NAMES_A_FIELD_DYNAMICALLY = "$getField";
11263
11590
  var init_request_fields = __esm(() => {
11591
+ init_path_matcher();
11592
+ init_blacklist_fold();
11264
11593
  WHOLE_DOCUMENT_VARIABLES = new Set(["ROOT", "CURRENT"]);
11265
11594
  RESHAPES_DOCUMENT = new Set(["$objectToArray", "$replaceRoot", "$replaceWith"]);
11266
11595
  });
@@ -12215,66 +12544,13 @@ var init_size_guard = __esm(() => {
12215
12544
  init_types3();
12216
12545
  });
12217
12546
 
12218
- // src/utils/glob.ts
12219
- function globToRegex(glob) {
12220
- let out = "^";
12221
- for (let i = 0;i < glob.length; i++) {
12222
- const c = glob[i];
12223
- if (c === "\\" && i + 1 < glob.length) {
12224
- out += glob[++i].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12225
- continue;
12226
- }
12227
- if (c === "*")
12228
- out += ".*";
12229
- else if (c === "?")
12230
- out += ".";
12231
- else if (c === "[") {
12232
- const end = findClassEnd(glob, i);
12233
- const body = end === -1 ? "" : glob.slice(i, end + 1);
12234
- if (body !== "" && isValidCharacterClass(body)) {
12235
- out += body;
12236
- i = end;
12237
- } else {
12238
- out += "\\[";
12239
- }
12240
- } else if (".^$+(){}|\\".includes(c)) {
12241
- out += "\\" + c;
12242
- } else {
12243
- out += c;
12244
- }
12245
- }
12246
- out += "$";
12247
- return new RegExp(out);
12248
- }
12249
- function findClassEnd(glob, open2) {
12250
- for (let i = open2 + 1;i < glob.length; i++) {
12251
- if (glob[i] === "\\") {
12252
- i++;
12253
- continue;
12254
- }
12255
- if (glob[i] === "]")
12256
- return i;
12257
- }
12258
- return -1;
12259
- }
12260
- function isValidCharacterClass(body) {
12261
- try {
12262
- new RegExp(body);
12263
- return true;
12264
- } catch {
12265
- return false;
12266
- }
12267
- }
12268
-
12269
12547
  // src/adapters/redis/blacklist-enforcer.ts
12270
12548
  function patternsOverlap(a, b) {
12271
- const ra = globToRegex(a);
12272
- const rb = globToRegex(b);
12273
12549
  const probeA = sampleFromGlob(a);
12274
12550
  const probeB = sampleFromGlob(b);
12275
- if (probeA !== null && rb.test(probeA))
12551
+ if (probeA !== null && globMatches(b, probeA))
12276
12552
  return true;
12277
- if (probeB !== null && ra.test(probeB))
12553
+ if (probeB !== null && globMatches(a, probeB))
12278
12554
  return true;
12279
12555
  return false;
12280
12556
  }
@@ -12300,6 +12576,15 @@ function sampleFromGlob(glob) {
12300
12576
  }
12301
12577
  return s;
12302
12578
  }
12579
+ function redisCommandTargets(command, args) {
12580
+ const spec = getCommandSpec(command);
12581
+ if (!spec)
12582
+ return [];
12583
+ return [
12584
+ ...expandKeyArity(spec.keyArity, args, args.length).map((index) => args[index]).filter((key) => key !== undefined),
12585
+ ...userPatterns(spec.keyArity, args)
12586
+ ];
12587
+ }
12303
12588
  function checkKeyArgs(command, args, rules) {
12304
12589
  if (rules.length === 0)
12305
12590
  return { ok: true };
@@ -12317,7 +12602,7 @@ function checkKeyArgs(command, args, rules) {
12317
12602
  if (key === undefined)
12318
12603
  continue;
12319
12604
  for (const pat of rules) {
12320
- if (globToRegex(pat).test(key)) {
12605
+ if (globMatches(pat, key)) {
12321
12606
  return { ok: false, matchedKey: key, matchedPattern: pat };
12322
12607
  }
12323
12608
  }
@@ -12374,6 +12659,7 @@ function expandKeyArity(arity, args, argCount) {
12374
12659
  }
12375
12660
  var init_blacklist_enforcer = __esm(() => {
12376
12661
  init_command_metadata();
12662
+ init_glob();
12377
12663
  });
12378
12664
 
12379
12665
  // src/adapters/redis/returned-key-names.ts
@@ -12385,8 +12671,7 @@ function filterReturnedKeyNames(command, reply, rules) {
12385
12671
  return reply;
12386
12672
  if (!returnsKeyNames(command))
12387
12673
  return reply;
12388
- const regexes = rules.map((pattern) => globToRegex(pattern));
12389
- const permitted = (key) => typeof key === "string" && regexes.every((regex) => !regex.test(key));
12674
+ const permitted = (key) => typeof key === "string" && rules.every((rule) => !globMatches(rule, key));
12390
12675
  if (Array.isArray(reply) && reply.length === 2 && Array.isArray(reply[1])) {
12391
12676
  return [reply[0], reply[1].filter(permitted)];
12392
12677
  }
@@ -12396,12 +12681,13 @@ function filterReturnedKeyNames(command, reply, rules) {
12396
12681
  }
12397
12682
  var RETURNS_KEY_NAMES;
12398
12683
  var init_returned_key_names = __esm(() => {
12684
+ init_glob();
12399
12685
  RETURNS_KEY_NAMES = new Set(["SCAN", "KEYS"]);
12400
12686
  });
12401
12687
 
12402
12688
  // src/adapters/redis/value-masker.ts
12403
12689
  function planFor(key, rules) {
12404
- const matched = rules.filter((r) => globToRegex(r.keyPattern).test(key));
12690
+ const matched = rules.filter((r) => globMatches(r.keyPattern, key));
12405
12691
  if (matched.length === 0)
12406
12692
  return null;
12407
12693
  const wholeValue = matched.some((r) => !r.fields || r.fields.length === 0);
@@ -12456,7 +12742,7 @@ function maskRedisRows(command, args, rows, rules) {
12456
12742
  }
12457
12743
  var REDACTED = "[REDACTED]", MASKABLE;
12458
12744
  var init_value_masker = __esm(() => {
12459
- init_blacklist_enforcer();
12745
+ init_glob();
12460
12746
  MASKABLE = new Set(["GET", "GETRANGE", "HGETALL", "HGET", "HMGET", "HVALS"]);
12461
12747
  });
12462
12748
 
@@ -12547,8 +12833,7 @@ class RedisAdapter {
12547
12833
  async sampleKeyNames(limit) {
12548
12834
  const client = this.requireClient();
12549
12835
  const rules = this.blacklistRules;
12550
- const regexes = rules.map((p) => globToRegex(p));
12551
- const { keys, scanned } = await scanAllKeys(client, "*", limit, limit, (key) => regexes.every((r) => !r.test(key)));
12836
+ const { keys, scanned } = await scanAllKeys(client, "*", limit, limit, (key) => rules.every((rule) => !globMatches(rule, key)));
12552
12837
  return { names: keys, truncated: scanned >= limit };
12553
12838
  }
12554
12839
  async getDbSize() {
@@ -12927,6 +13212,7 @@ var init_redis_adapter = __esm(() => {
12927
13212
  init_types2();
12928
13213
  init_size_guard();
12929
13214
  init_blacklist_enforcer();
13215
+ init_glob();
12930
13216
  init_returned_key_names();
12931
13217
  init_types3();
12932
13218
  init_command_metadata();
@@ -13227,6 +13513,180 @@ var init_elasticsearch_adapter = __esm(() => {
13227
13513
  init_server_side_script();
13228
13514
  });
13229
13515
 
13516
+ // src/core/blacklist-manager.ts
13517
+ function isBlacklistOverrideEnabled(value) {
13518
+ return (value ?? Bun.env.DBCLI_OVERRIDE_BLACKLIST ?? "") === "true";
13519
+ }
13520
+ function normalizeBlacklistEntry(raw) {
13521
+ const trimmed = raw.trim();
13522
+ const quote = trimmed[0];
13523
+ if ((quote === '"' || quote === "`") && trimmed.length > 1 && trimmed.endsWith(quote)) {
13524
+ return trimmed.slice(1, -1).trim();
13525
+ }
13526
+ return trimmed;
13527
+ }
13528
+ function assertNotTableQualified(tableKey, column) {
13529
+ const dot = column.indexOf(".");
13530
+ if (dot <= 0)
13531
+ return;
13532
+ if (foldFieldPath(column.slice(0, dot)) !== foldFieldPath(tableKey))
13533
+ return;
13534
+ 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))}.`);
13535
+ }
13536
+
13537
+ class BlacklistManager {
13538
+ config;
13539
+ state;
13540
+ overrideEnabled;
13541
+ rawTables = [];
13542
+ constructor(config, overrideEnvValue) {
13543
+ this.config = config;
13544
+ this.overrideEnabled = isBlacklistOverrideEnabled(overrideEnvValue);
13545
+ this.state = this.loadBlacklist();
13546
+ this.wildcardTables = this.rawTables.filter(isGlobPattern);
13547
+ }
13548
+ loadBlacklist() {
13549
+ this.rawTables.length = 0;
13550
+ const tables = new Set;
13551
+ const columns = new Map;
13552
+ const blacklistConfig = this.config.blacklist;
13553
+ if (!blacklistConfig) {
13554
+ return { tables, columns };
13555
+ }
13556
+ if (Array.isArray(blacklistConfig.tables)) {
13557
+ for (const tableName of blacklistConfig.tables) {
13558
+ if (typeof tableName === "string") {
13559
+ const entry = normalizeBlacklistEntry(tableName);
13560
+ this.rawTables.push(entry);
13561
+ tables.add(foldFieldPath(entry));
13562
+ } else {
13563
+ console.warn(`[BlacklistManager] Invalid table name in blacklist config: ${JSON.stringify(tableName)}`);
13564
+ }
13565
+ }
13566
+ } else if (blacklistConfig.tables !== undefined) {
13567
+ console.warn("[BlacklistManager] blacklist.tables must be an array, ignoring");
13568
+ }
13569
+ if (blacklistConfig.columns && typeof blacklistConfig.columns === "object" && !Array.isArray(blacklistConfig.columns)) {
13570
+ for (const [tableName, cols] of Object.entries(blacklistConfig.columns)) {
13571
+ if (typeof tableName !== "string") {
13572
+ console.warn(`[BlacklistManager] Invalid table name key in columns config: ${JSON.stringify(tableName)}`);
13573
+ continue;
13574
+ }
13575
+ if (!Array.isArray(cols)) {
13576
+ console.warn(`[BlacklistManager] blacklist.columns["${tableName}"] must be an array, ignoring`);
13577
+ continue;
13578
+ }
13579
+ const columnSet = new Set;
13580
+ for (const col of cols) {
13581
+ if (typeof col === "string") {
13582
+ const entry = normalizeBlacklistEntry(col);
13583
+ assertNotTableQualified(normalizeBlacklistEntry(tableName), entry);
13584
+ columnSet.add(entry);
13585
+ } else {
13586
+ console.warn(`[BlacklistManager] Invalid column name in blacklist.columns["${tableName}"]: ${JSON.stringify(col)}`);
13587
+ }
13588
+ }
13589
+ if (columnSet.size > 0) {
13590
+ columns.set(foldFieldPath(normalizeBlacklistEntry(tableName)), columnSet);
13591
+ }
13592
+ }
13593
+ } else if (blacklistConfig.columns !== undefined) {
13594
+ console.warn("[BlacklistManager] blacklist.columns must be an object, ignoring");
13595
+ }
13596
+ return { tables, columns };
13597
+ }
13598
+ isTableBlacklisted(tableName) {
13599
+ const name = foldFieldPath(tableName);
13600
+ if (this.state.tables.has(name))
13601
+ return true;
13602
+ for (const pattern of this.wildcardTables) {
13603
+ if (globMatches(pattern, tableName, { caseInsensitive: true }))
13604
+ return true;
13605
+ }
13606
+ return false;
13607
+ }
13608
+ wildcardTables;
13609
+ columnRulesFor(tableName) {
13610
+ const key = foldFieldPath(normalizeBlacklistEntry(tableName));
13611
+ const direct = this.state.columns.get(key);
13612
+ if (direct)
13613
+ return direct;
13614
+ const dot = key.lastIndexOf(".");
13615
+ if (dot > 0) {
13616
+ const bare = this.state.columns.get(key.slice(dot + 1));
13617
+ if (bare)
13618
+ return bare;
13619
+ }
13620
+ let merged;
13621
+ for (const [ruleKey, columns] of this.state.columns) {
13622
+ const ruleDot = ruleKey.lastIndexOf(".");
13623
+ if (ruleDot <= 0 || ruleKey.slice(ruleDot + 1) !== key)
13624
+ continue;
13625
+ merged ??= new Set;
13626
+ for (const column of columns)
13627
+ merged.add(column);
13628
+ }
13629
+ return merged;
13630
+ }
13631
+ isColumnBlacklisted(tableName, columnName) {
13632
+ const columnSet = this.columnRulesFor(tableName);
13633
+ if (!columnSet) {
13634
+ return false;
13635
+ }
13636
+ const target = foldFieldPath(normalizeBlacklistEntry(columnName));
13637
+ const compiled = this.foldedColumnsFor(columnSet);
13638
+ if (compiled.literals.has(target))
13639
+ return true;
13640
+ return compiled.globs.length > 0 && matchAny(target, compiled.globs);
13641
+ }
13642
+ foldedColumns = new WeakMap;
13643
+ foldedColumnsFor(columnSet) {
13644
+ const memo = this.foldedColumns.get(columnSet);
13645
+ if (memo !== undefined)
13646
+ return memo;
13647
+ const literals = new Set;
13648
+ const globbed = [];
13649
+ for (const rule of columnSet) {
13650
+ if (isGlobPattern(rule))
13651
+ globbed.push(rule);
13652
+ else
13653
+ literals.add(foldFieldPath(rule));
13654
+ }
13655
+ const folded = {
13656
+ literals,
13657
+ globs: globbed.length > 0 ? compilePatterns(globbed).patterns : []
13658
+ };
13659
+ this.foldedColumns.set(columnSet, folded);
13660
+ return folded;
13661
+ }
13662
+ getBlacklistedColumns(tableName) {
13663
+ const columnSet = this.columnRulesFor(tableName);
13664
+ if (!columnSet) {
13665
+ return [];
13666
+ }
13667
+ return Array.from(columnSet);
13668
+ }
13669
+ getAllBlacklistedColumns() {
13670
+ const all = new Set;
13671
+ for (const columnSet of this.state.columns.values()) {
13672
+ for (const column of columnSet)
13673
+ all.add(column);
13674
+ }
13675
+ return Array.from(all);
13676
+ }
13677
+ canOverrideBlacklist() {
13678
+ return this.overrideEnabled;
13679
+ }
13680
+ getState() {
13681
+ return this.state;
13682
+ }
13683
+ }
13684
+ var init_blacklist_manager = __esm(() => {
13685
+ init_glob();
13686
+ init_blacklist_fold();
13687
+ init_path_matcher();
13688
+ });
13689
+
13230
13690
  // src/adapters/factory.ts
13231
13691
  class AdapterFactory {
13232
13692
  static createSqlAdapter(rawOptions) {
@@ -13241,7 +13701,7 @@ class AdapterFactory {
13241
13701
  throw new Error(`createSqlAdapter requires a SQL system, got: ${options.system}`);
13242
13702
  }
13243
13703
  }
13244
- static createQueryableAdapter(rawOptions) {
13704
+ static createQueryableAdapterWithoutRules(rawOptions) {
13245
13705
  const options = withResolvedTimeout(rawOptions);
13246
13706
  switch (options.system) {
13247
13707
  case "mongodb":
@@ -13251,10 +13711,27 @@ class AdapterFactory {
13251
13711
  case "elasticsearch":
13252
13712
  return new ElasticsearchAdapter(options);
13253
13713
  default:
13254
- throw new Error(`createQueryableAdapter requires a non-SQL queryable system, got: ${options.system}`);
13714
+ throw new Error(`createQueryableAdapterWithoutRules requires a non-SQL queryable system, got: ${options.system}`);
13255
13715
  }
13256
13716
  }
13257
- static createAdapter(options) {
13717
+ static createAdapter(config) {
13718
+ const options = config.connection;
13719
+ switch (options.system) {
13720
+ case "postgresql":
13721
+ case "mysql":
13722
+ case "mariadb":
13723
+ return AdapterFactory.createSqlAdapter(options);
13724
+ case "redis":
13725
+ return AdapterFactory.createRedisAdapter(config);
13726
+ case "mongodb":
13727
+ return AdapterFactory.createMongoDBAdapter(config);
13728
+ case "elasticsearch":
13729
+ return AdapterFactory.createElasticsearchAdapter(options);
13730
+ default:
13731
+ throw new Error(`Unsupported database system: ${options.system}`);
13732
+ }
13733
+ }
13734
+ static createAdapterWithoutRules(options) {
13258
13735
  switch (options.system) {
13259
13736
  case "postgresql":
13260
13737
  case "mysql":
@@ -13263,7 +13740,7 @@ class AdapterFactory {
13263
13740
  case "mongodb":
13264
13741
  case "redis":
13265
13742
  case "elasticsearch":
13266
- return AdapterFactory.createQueryableAdapter(options);
13743
+ return AdapterFactory.createQueryableAdapterWithoutRules(options);
13267
13744
  default:
13268
13745
  throw new Error(`Unsupported database system: ${options.system}`);
13269
13746
  }
@@ -13273,7 +13750,7 @@ class AdapterFactory {
13273
13750
  if (options.system !== "mongodb") {
13274
13751
  throw new Error("createMongoDBAdapter requires system: mongodb");
13275
13752
  }
13276
- const adapter = AdapterFactory.createQueryableAdapter(options);
13753
+ const adapter = AdapterFactory.createQueryableAdapterWithoutRules(options);
13277
13754
  adapter.setBlacklistColumns(config.blacklist?.columns ?? {});
13278
13755
  return adapter;
13279
13756
  }
@@ -13284,7 +13761,12 @@ class AdapterFactory {
13284
13761
  }
13285
13762
  const options = withResolvedTimeout(rawOptions);
13286
13763
  const adapter = new RedisAdapter(options);
13287
- adapter.setBlacklistRules(config.blacklist?.tables ?? []);
13764
+ const blacklistRules = config.blacklist?.tables ?? [];
13765
+ const overrideEnabled = isBlacklistOverrideEnabled();
13766
+ if (overrideEnabled && blacklistRules.length > 0) {
13767
+ console.error(t("warnings.redis_blacklist_override_active"));
13768
+ }
13769
+ adapter.setBlacklistRules(overrideEnabled ? [] : blacklistRules);
13288
13770
  adapter.setMaskRules(config.redis?.mask ?? []);
13289
13771
  return adapter;
13290
13772
  }
@@ -13292,7 +13774,7 @@ class AdapterFactory {
13292
13774
  if (options.system !== "elasticsearch") {
13293
13775
  throw new Error("createElasticsearchAdapter requires system: elasticsearch");
13294
13776
  }
13295
- return AdapterFactory.createQueryableAdapter(options);
13777
+ return AdapterFactory.createQueryableAdapterWithoutRules(options);
13296
13778
  }
13297
13779
  }
13298
13780
  var init_factory = __esm(() => {
@@ -13301,6 +13783,8 @@ var init_factory = __esm(() => {
13301
13783
  init_mongodb_adapter();
13302
13784
  init_redis_adapter();
13303
13785
  init_elasticsearch_adapter();
13786
+ init_blacklist_manager();
13787
+ init_message_loader();
13304
13788
  });
13305
13789
 
13306
13790
  // src/adapters/index.ts
@@ -13449,7 +13933,7 @@ async function writeV2InitConfig(configPath, connectionName, connection, permiss
13449
13933
  audit: {
13450
13934
  enabled: true,
13451
13935
  strict: false,
13452
- rotation: { max_bytes: 10485760, max_entries: 1000 }
13936
+ rotation: { ...DEFAULT_AUDIT_ROTATION }
13453
13937
  }
13454
13938
  };
13455
13939
  await writeV2Config(storagePath, v2Config);
@@ -13464,6 +13948,7 @@ var init_init_shared = __esm(() => {
13464
13948
  init_config();
13465
13949
  init_config_v2();
13466
13950
  init_prompts();
13951
+ init_validation();
13467
13952
  init_config_binding();
13468
13953
  });
13469
13954
 
@@ -13914,7 +14399,7 @@ async function initCommandHandler(options, command) {
13914
14399
  password: String(resolveValue(newConfig.connection.password, "password")) || "",
13915
14400
  database: String(resolveValue(newConfig.connection.database, "database"))
13916
14401
  };
13917
- const adapter = AdapterFactory.createAdapter(testConnection);
14402
+ const adapter = AdapterFactory.createAdapterWithoutRules(testConnection);
13918
14403
  try {
13919
14404
  await adapter.connect();
13920
14405
  const isHealthy = await adapter.testConnection();
@@ -16456,94 +16941,6 @@ class ColumnIndexBuilder {
16456
16941
  };
16457
16942
  }
16458
16943
  }
16459
- // src/core/blacklist-manager.ts
16460
- class BlacklistManager {
16461
- config;
16462
- state;
16463
- overrideEnabled;
16464
- constructor(config, overrideEnvValue) {
16465
- this.config = config;
16466
- this.overrideEnabled = (overrideEnvValue ?? Bun.env.DBCLI_OVERRIDE_BLACKLIST ?? "") === "true";
16467
- this.state = this.loadBlacklist();
16468
- }
16469
- loadBlacklist() {
16470
- const tables = new Set;
16471
- const columns = new Map;
16472
- const blacklistConfig = this.config.blacklist;
16473
- if (!blacklistConfig) {
16474
- return { tables, columns };
16475
- }
16476
- if (Array.isArray(blacklistConfig.tables)) {
16477
- for (const tableName of blacklistConfig.tables) {
16478
- if (typeof tableName === "string") {
16479
- tables.add(tableName.toLowerCase());
16480
- } else {
16481
- console.warn(`[BlacklistManager] Invalid table name in blacklist config: ${JSON.stringify(tableName)}`);
16482
- }
16483
- }
16484
- } else if (blacklistConfig.tables !== undefined) {
16485
- console.warn("[BlacklistManager] blacklist.tables must be an array, ignoring");
16486
- }
16487
- if (blacklistConfig.columns && typeof blacklistConfig.columns === "object" && !Array.isArray(blacklistConfig.columns)) {
16488
- for (const [tableName, cols] of Object.entries(blacklistConfig.columns)) {
16489
- if (typeof tableName !== "string") {
16490
- console.warn(`[BlacklistManager] Invalid table name key in columns config: ${JSON.stringify(tableName)}`);
16491
- continue;
16492
- }
16493
- if (!Array.isArray(cols)) {
16494
- console.warn(`[BlacklistManager] blacklist.columns["${tableName}"] must be an array, ignoring`);
16495
- continue;
16496
- }
16497
- const columnSet = new Set;
16498
- for (const col of cols) {
16499
- if (typeof col === "string") {
16500
- columnSet.add(col);
16501
- } else {
16502
- console.warn(`[BlacklistManager] Invalid column name in blacklist.columns["${tableName}"]: ${JSON.stringify(col)}`);
16503
- }
16504
- }
16505
- if (columnSet.size > 0) {
16506
- columns.set(tableName.toLowerCase(), columnSet);
16507
- }
16508
- }
16509
- } else if (blacklistConfig.columns !== undefined) {
16510
- console.warn("[BlacklistManager] blacklist.columns must be an object, ignoring");
16511
- }
16512
- return { tables, columns };
16513
- }
16514
- isTableBlacklisted(tableName) {
16515
- return this.state.tables.has(tableName.toLowerCase());
16516
- }
16517
- isColumnBlacklisted(tableName, columnName) {
16518
- const columnSet = this.state.columns.get(tableName.toLowerCase());
16519
- if (!columnSet) {
16520
- return false;
16521
- }
16522
- return columnSet.has(columnName);
16523
- }
16524
- getBlacklistedColumns(tableName) {
16525
- const columnSet = this.state.columns.get(tableName.toLowerCase());
16526
- if (!columnSet) {
16527
- return [];
16528
- }
16529
- return Array.from(columnSet);
16530
- }
16531
- getAllBlacklistedColumns() {
16532
- const all = new Set;
16533
- for (const columnSet of this.state.columns.values()) {
16534
- for (const column of columnSet)
16535
- all.add(column);
16536
- }
16537
- return Array.from(all);
16538
- }
16539
- canOverrideBlacklist() {
16540
- return this.overrideEnabled;
16541
- }
16542
- getState() {
16543
- return this.state;
16544
- }
16545
- }
16546
-
16547
16944
  // src/types/blacklist.ts
16548
16945
  var BlacklistError;
16549
16946
  var init_blacklist = __esm(() => {
@@ -16607,18 +17004,138 @@ function projectRows(rows, selection) {
16607
17004
  const columnNames = collectColumnNames(projectedRows);
16608
17005
  return { rows: normalizeRows(projectedRows, columnNames), columnNames };
16609
17006
  }
16610
- function hasFieldPath(row, segments) {
16611
- return readPath(row, segments).found;
17007
+ function hasFieldPath(row, segments, options) {
17008
+ const caseInsensitive = options?.caseInsensitive === true;
17009
+ const folded = caseInsensitive && options?.alreadyFolded !== true ? segments.map(foldCase) : segments;
17010
+ return readPath(row, folded, caseInsensitive).found;
17011
+ }
17012
+ function narrow(rules, key) {
17013
+ const parts = key.includes(".") ? key.split(".") : [key];
17014
+ const live = [];
17015
+ for (const rule of rules) {
17016
+ const remaining = rule.segments.length;
17017
+ if (remaining < parts.length && !rule.wildcardTail)
17018
+ continue;
17019
+ let matches = true;
17020
+ for (let i = 0;i < Math.min(remaining, parts.length); i++) {
17021
+ if (!matchSegment(rule.segments[i], parts[i])) {
17022
+ matches = false;
17023
+ break;
17024
+ }
17025
+ }
17026
+ if (!matches)
17027
+ continue;
17028
+ if (remaining <= parts.length)
17029
+ return { drop: true, live: [] };
17030
+ live.push({
17031
+ root: rule.root,
17032
+ rootIndex: rule.rootIndex,
17033
+ offset: rule.offset + parts.length,
17034
+ segments: rule.segments.slice(parts.length),
17035
+ wildcardTail: rule.wildcardTail
17036
+ });
17037
+ }
17038
+ return { drop: false, live };
17039
+ }
17040
+ function liveRulesOf(patterns) {
17041
+ return patterns.map((pattern, index) => ({
17042
+ root: pattern,
17043
+ rootIndex: index,
17044
+ offset: 0,
17045
+ segments: pattern.segments,
17046
+ wildcardTail: pattern.wildcardTail
17047
+ }));
17048
+ }
17049
+
17050
+ class NarrowStates {
17051
+ states = [];
17052
+ ids = new Map;
17053
+ transitions = new Map;
17054
+ constructor(rules) {
17055
+ this.intern(rules);
17056
+ }
17057
+ rules(state) {
17058
+ const rules = this.states[state];
17059
+ if (rules === undefined)
17060
+ throw new Error(`unknown nested-rule state ${state}`);
17061
+ return rules;
17062
+ }
17063
+ step(state, key) {
17064
+ const memoKey = `${state}\x00${key}`;
17065
+ const memo = this.transitions.get(memoKey);
17066
+ if (memo !== undefined)
17067
+ return memo;
17068
+ const { drop, live } = narrow(this.rules(state), key);
17069
+ const step = drop ? { drop: true } : { drop: false, next: this.intern(live) };
17070
+ if (this.transitions.size >= NARROW_STATE_LIMIT)
17071
+ this.transitions = new Map;
17072
+ this.transitions.set(memoKey, step);
17073
+ return step;
17074
+ }
17075
+ intern(rules) {
17076
+ const key = rules.map((rule) => `${rule.rootIndex}:${rule.offset}`).join(",");
17077
+ const existing = this.ids.get(key);
17078
+ if (existing !== undefined)
17079
+ return existing;
17080
+ const id = this.states.length;
17081
+ this.states.push(rules);
17082
+ this.ids.set(key, id);
17083
+ return id;
17084
+ }
16612
17085
  }
16613
- function omitFieldPaths(rows, paths) {
16614
- const topLevel = new Set(paths);
16615
- const dotted = paths.filter((path) => path.includes(".")).map((path) => ({ head: path.slice(0, path.indexOf(".")), segments: path.split(".") }));
17086
+ function collectNestedMatches(row, states, pending, onMatch) {
17087
+ if (pending.size === 0)
17088
+ return;
17089
+ const walk = (value, state, depth) => {
17090
+ if (pending.size === 0 || states.rules(state).length === 0)
17091
+ return;
17092
+ if (Array.isArray(value)) {
17093
+ for (const item of value)
17094
+ walk(item, state, depth);
17095
+ return;
17096
+ }
17097
+ if (!isPlainRecord(value))
17098
+ return;
17099
+ for (const key of Object.getOwnPropertyNames(value)) {
17100
+ const step = states.step(state, key);
17101
+ if (step.drop) {
17102
+ if (depth > 0) {
17103
+ for (const rule of states.rules(state)) {
17104
+ if (!pending.has(rule.root))
17105
+ continue;
17106
+ if (!narrow([rule], key).drop)
17107
+ continue;
17108
+ pending.delete(rule.root);
17109
+ onMatch(rule.root);
17110
+ }
17111
+ }
17112
+ continue;
17113
+ }
17114
+ walk(value[key], step.next, depth + 1);
17115
+ }
17116
+ };
17117
+ walk(row, 0, 0);
17118
+ }
17119
+ function compileNestedRules(patterns) {
17120
+ return new NarrowStates(liveRulesOf(patterns));
17121
+ }
17122
+ function omitFieldPaths(rows, paths, options) {
17123
+ const caseInsensitive = options?.caseInsensitive === true;
17124
+ const nestedPatterns = options?.patterns ?? [];
17125
+ const nestedStates = nestedPatterns.length > 0 ? compileNestedRules(nestedPatterns) : undefined;
17126
+ const fold = (value) => caseInsensitive ? foldCase(value) : value;
17127
+ const topLevel = new Set(paths.map(fold));
17128
+ const dotted = paths.filter((path) => path.includes(".")).map((path) => ({ head: fold(path.slice(0, path.indexOf("."))), segments: path.split(".") }));
16616
17129
  const maskRecord = (row) => {
16617
- let projected = cloneRecord(row, topLevel);
17130
+ let projected = cloneRecord(row, topLevel, caseInsensitive);
16618
17131
  for (const { head, segments } of dotted) {
16619
- const value = row[head];
16620
- if (value !== null && typeof value === "object")
16621
- projected = omitPath(projected, segments);
17132
+ const value = headValue(row, head, caseInsensitive);
17133
+ if (value !== null && typeof value === "object") {
17134
+ projected = omitPath(projected, segments, caseInsensitive);
17135
+ }
17136
+ }
17137
+ if (nestedStates !== undefined && hasNestedRecord(row)) {
17138
+ projected = omitMatching(projected, nestedStates, 0);
16622
17139
  }
16623
17140
  return projected;
16624
17141
  };
@@ -16657,11 +17174,37 @@ function validatePath(path) {
16657
17174
  if (unsafe)
16658
17175
  throw new Error(`--fields contains an unsafe field segment: ${unsafe}`);
16659
17176
  }
16660
- function readPath(value, segments) {
17177
+ function headValue(row, head, caseInsensitive) {
17178
+ if (!caseInsensitive)
17179
+ return row[head];
17180
+ const key = foldedKeys(row).get(head);
17181
+ return key === undefined ? undefined : row[key];
17182
+ }
17183
+ function foldedKeys(value) {
17184
+ const memo = foldedKeyIndex.get(value);
17185
+ if (memo !== undefined)
17186
+ return memo;
17187
+ const index = new Map;
17188
+ for (const key of Object.getOwnPropertyNames(value)) {
17189
+ const folded = foldCase(key);
17190
+ if (!index.has(folded))
17191
+ index.set(folded, key);
17192
+ }
17193
+ foldedKeyIndex.set(value, index);
17194
+ return index;
17195
+ }
17196
+ function ownKey(value, name, caseInsensitive) {
17197
+ if (Object.prototype.hasOwnProperty.call(value, name))
17198
+ return name;
17199
+ if (!caseInsensitive)
17200
+ return;
17201
+ return foldedKeys(value).get(name);
17202
+ }
17203
+ function readPath(value, segments, caseInsensitive = false) {
16661
17204
  if (segments.length === 0)
16662
17205
  return { found: true, value };
16663
17206
  if (Array.isArray(value)) {
16664
- const results = value.map((item) => readPath(item, segments));
17207
+ const results = value.map((item) => readPath(item, segments, caseInsensitive));
16665
17208
  return {
16666
17209
  found: results.some((result) => result.found),
16667
17210
  value: results.map((result) => result.found ? result.value : undefined)
@@ -16669,41 +17212,68 @@ function readPath(value, segments) {
16669
17212
  }
16670
17213
  if (!isRecord(value))
16671
17214
  return { found: false };
16672
- const exactPath = segments.join(".");
16673
- if (Object.prototype.hasOwnProperty.call(value, exactPath)) {
16674
- return { found: true, value: value[exactPath] };
16675
- }
17215
+ const exact = ownKey(value, segments.join("."), caseInsensitive);
17216
+ if (exact !== undefined)
17217
+ return { found: true, value: value[exact] };
16676
17218
  const [head, ...tail] = segments;
16677
- if (!Object.prototype.hasOwnProperty.call(value, head))
17219
+ const headKey = ownKey(value, head, caseInsensitive);
17220
+ if (headKey === undefined)
16678
17221
  return { found: false };
16679
- return readPath(value[head], tail);
17222
+ return readPath(value[headKey], tail, caseInsensitive);
17223
+ }
17224
+ function omitMatching(value, states, state) {
17225
+ if (states.rules(state).length === 0)
17226
+ return value;
17227
+ if (Array.isArray(value))
17228
+ return value.map((item) => omitMatching(item, states, state));
17229
+ if (!isPlainRecord(value))
17230
+ return value;
17231
+ const out = {};
17232
+ for (const [key, child] of Object.entries(value)) {
17233
+ const step = states.step(state, key);
17234
+ if (step.drop)
17235
+ continue;
17236
+ defineData(out, key, omitMatching(child, states, step.next));
17237
+ }
17238
+ return out;
17239
+ }
17240
+ function hasNestedRecord(row) {
17241
+ for (const key of Object.getOwnPropertyNames(row)) {
17242
+ const value = row[key];
17243
+ if (Array.isArray(value) || isPlainRecord(value))
17244
+ return true;
17245
+ }
17246
+ return false;
16680
17247
  }
16681
- function omitPath(value, segments) {
17248
+ function omitPath(value, segments, caseInsensitive) {
16682
17249
  if (segments.length === 0)
16683
17250
  return value;
16684
17251
  if (Array.isArray(value))
16685
- return value.map((item) => omitPath(item, segments));
17252
+ return value.map((item) => omitPath(item, segments, caseInsensitive));
16686
17253
  if (!isPlainRecord(value))
16687
17254
  return value;
16688
- const exactPath = segments.join(".");
16689
- const [head, ...tail] = segments;
17255
+ const fold = (name) => caseInsensitive ? foldCase(name) : name;
17256
+ const exactPath = fold(segments.join("."));
17257
+ const head = fold(segments[0]);
17258
+ const tail = segments.slice(1);
16690
17259
  const out = {};
16691
17260
  for (const [key, child] of Object.entries(value)) {
16692
- if (key === exactPath)
17261
+ const folded = fold(key);
17262
+ if (folded === exactPath)
16693
17263
  continue;
16694
- if (key === head) {
17264
+ if (folded === head) {
16695
17265
  if (tail.length > 0)
16696
- defineData(out, key, omitPath(child, tail));
17266
+ defineData(out, key, omitPath(child, tail, caseInsensitive));
16697
17267
  continue;
16698
17268
  }
16699
17269
  defineData(out, key, child);
16700
17270
  }
16701
17271
  return out;
16702
17272
  }
16703
- function cloneRecord(value, omittedKeys) {
17273
+ function cloneRecord(value, omittedKeys, caseInsensitive = false) {
16704
17274
  const out = {};
16705
17275
  for (const [key, child] of Object.entries(value)) {
16706
- if (!omittedKeys.has(key))
17276
+ if (!omittedKeys.has(caseInsensitive ? foldCase(key) : key))
16707
17277
  defineData(out, key, child);
16708
17278
  }
16709
17279
  return out;
@@ -16748,9 +17318,11 @@ function defineData(target, key, value) {
16748
17318
  writable: true
16749
17319
  });
16750
17320
  }
16751
- var UNSAFE_SEGMENTS;
17321
+ var UNSAFE_SEGMENTS, NARROW_STATE_LIMIT = 4096, foldedKeyIndex;
16752
17322
  var init_field_projection = __esm(() => {
17323
+ init_path_matcher();
16753
17324
  UNSAFE_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
17325
+ foldedKeyIndex = new WeakMap;
16754
17326
  });
16755
17327
 
16756
17328
  // src/utils/es-index-target.ts
@@ -16817,16 +17389,16 @@ function expandIndexTargets(target) {
16817
17389
  return { concrete, wildcards };
16818
17390
  }
16819
17391
  function matchesIndexGlob(pattern, name) {
16820
- const normalized = pattern.toLowerCase() === "_all" ? "*" : pattern.toLowerCase();
17392
+ const normalized = pattern.toLowerCase() === "_all" ? "*" : pattern;
16821
17393
  try {
16822
- return globToRegex(normalized).test(name.toLowerCase());
17394
+ return globMatches(normalized, name, { caseInsensitive: true });
16823
17395
  } catch {
16824
17396
  return true;
16825
17397
  }
16826
17398
  }
16827
17399
  function reachesByConvention(name, entry) {
16828
- const lower = name.toLowerCase();
16829
- const target = entry.toLowerCase();
17400
+ const lower = foldFieldPath(name);
17401
+ const target = foldFieldPath(entry);
16830
17402
  return new RegExp(`^\\.ds-${escapeRegExp(target)}-`).test(lower) || new RegExp(`^${escapeRegExp(target)}-\\d+$`).test(lower);
16831
17403
  }
16832
17404
  function escapeRegExp(text2) {
@@ -16847,11 +17419,14 @@ function indexExpressionReaches(expression, blacklisted) {
16847
17419
  return false;
16848
17420
  const { concrete, wildcards } = expandIndexTargets(expression);
16849
17421
  const entries = expandBlacklistEntries(blacklisted);
16850
- const reachesName = (name) => entries.names.some((entry) => entry.toLowerCase() === name.toLowerCase() || reachesByConvention(name, entry)) || entries.patterns.some((pattern) => matchesIndexGlob(pattern, name));
17422
+ const reachesName = (name) => entries.names.some((entry) => foldFieldPath(entry) === foldFieldPath(name) || reachesByConvention(name, entry)) || entries.patterns.some((pattern) => matchesIndexGlob(pattern, name));
16851
17423
  return concrete.some(reachesName) || wildcards.some((pattern) => entries.names.some((entry) => matchesIndexGlob(pattern, entry)) || entries.patterns.length > 0);
16852
17424
  }
16853
17425
  var MAX_DECODE_PASSES = 4;
16854
- var init_es_index_target = () => {};
17426
+ var init_es_index_target = __esm(() => {
17427
+ init_blacklist_fold();
17428
+ init_glob();
17429
+ });
16855
17430
 
16856
17431
  // src/core/blacklist-validator.ts
16857
17432
  function flattenArrayRow(row) {
@@ -16868,7 +17443,7 @@ function dedupe2(values) {
16868
17443
  const seen = new Set;
16869
17444
  const result = [];
16870
17445
  for (const value of values) {
16871
- const key = value.toLowerCase();
17446
+ const key = foldFieldPath(value);
16872
17447
  if (value.length === 0 || seen.has(key))
16873
17448
  continue;
16874
17449
  seen.add(key);
@@ -16876,6 +17451,20 @@ function dedupe2(values) {
16876
17451
  }
16877
17452
  return result;
16878
17453
  }
17454
+ function omittedByName(omittedColumns, path) {
17455
+ return omittedColumns.some((omitted) => path === omitted || path.startsWith(`${omitted}.`));
17456
+ }
17457
+ function compileGlobRules(rules, table, operation) {
17458
+ const globbed = rules.filter(isGlobPattern);
17459
+ if (globbed.length === 0)
17460
+ return [];
17461
+ const { patterns, rejected } = compilePatterns(globbed);
17462
+ if (rejected.length > 0) {
17463
+ const detail = rejected.map((r) => `'${r.raw}' (${r.reason})`).join(", ");
17464
+ throw new BlacklistError(`blacklist.columns for '${table}' has entries this matcher cannot read: ${detail}`, table, operation);
17465
+ }
17466
+ return patterns;
17467
+ }
16879
17468
 
16880
17469
  class BlacklistValidator {
16881
17470
  manager;
@@ -16933,7 +17522,27 @@ class BlacklistValidator {
16933
17522
  if (blacklisted.length === 0 || fields.length === 0) {
16934
17523
  return;
16935
17524
  }
16936
- const conflicts = fields.filter((f) => blacklisted.includes(f));
17525
+ const protectedPaths = new Set(blacklisted.map(foldFieldPath));
17526
+ const globs = compileGlobRules(blacklisted, tableName, operation);
17527
+ const conflicts = fields.filter((field) => {
17528
+ const name = foldFieldPath(field);
17529
+ if (protectedPaths.has(name))
17530
+ return true;
17531
+ if (matchAny(name, globs))
17532
+ return true;
17533
+ let dot = name.indexOf(".");
17534
+ while (dot >= 0) {
17535
+ if (dot > 0) {
17536
+ const ancestor = name.slice(0, dot);
17537
+ if (protectedPaths.has(ancestor))
17538
+ return true;
17539
+ if (matchAny(ancestor, globs))
17540
+ return true;
17541
+ }
17542
+ dot = name.indexOf(".", dot + 1);
17543
+ }
17544
+ return false;
17545
+ });
16937
17546
  if (conflicts.length === 0) {
16938
17547
  return;
16939
17548
  }
@@ -16959,7 +17568,7 @@ class BlacklistValidator {
16959
17568
  const tables = dedupe2(tableNames);
16960
17569
  const blacklistedColumns = tables.length === 0 ? this.manager.getAllBlacklistedColumns() : Array.from(new Set(tables.flatMap((table) => this.manager.getBlacklistedColumns(table))));
16961
17570
  if (blacklistedColumns.length === 0) {
16962
- return { filteredRows: rows, omittedColumns: [] };
17571
+ return { filteredRows: rows, omittedColumns: [], reachesOmitted: () => false };
16963
17572
  }
16964
17573
  const probeNested = blacklistedColumns.some((path) => path.includes("."));
16965
17574
  const presentColumns = new Set(columnList);
@@ -16971,7 +17580,7 @@ class BlacklistValidator {
16971
17580
  continue;
16972
17581
  const value = record[key];
16973
17582
  if (value !== null && typeof value === "object")
16974
- nestedHeads.add(key);
17583
+ nestedHeads.add(foldFieldPath(key));
16975
17584
  }
16976
17585
  };
16977
17586
  for (const row of rows) {
@@ -16984,38 +17593,103 @@ class BlacklistValidator {
16984
17593
  }
16985
17594
  collect(row);
16986
17595
  }
16987
- const protectedPaths = new Set(blacklistedColumns);
17596
+ const protectedPaths = new Set(blacklistedColumns.map(foldFieldPath));
17597
+ const globRules = compileGlobRules(blacklistedColumns, tables[0] ?? "unknown", "SELECT");
17598
+ const presentByFolded = new Map;
17599
+ for (const column of presentColumns) {
17600
+ const folded = foldFieldPath(column);
17601
+ const names = presentByFolded.get(folded);
17602
+ if (names === undefined)
17603
+ presentByFolded.set(folded, [column]);
17604
+ else
17605
+ names.push(column);
17606
+ }
16988
17607
  const omitted = new Set;
16989
17608
  for (const path of blacklistedColumns) {
16990
- if (presentColumns.has(path)) {
16991
- omitted.add(path);
17609
+ const present = presentByFolded.get(foldFieldPath(path));
17610
+ if (present !== undefined) {
17611
+ for (const name of present)
17612
+ omitted.add(name);
16992
17613
  continue;
16993
17614
  }
16994
17615
  const dot = path.indexOf(".");
16995
17616
  if (dot < 0)
16996
17617
  continue;
16997
- if (!nestedHeads.has(path.slice(0, dot)))
17618
+ if (!nestedHeads.has(foldFieldPath(path.slice(0, dot))))
16998
17619
  continue;
16999
- const segments = path.split(".");
17000
- if (rows.some((row) => hasFieldPath(row, segments)))
17620
+ const segments = path.split(".").map(foldFieldPath);
17621
+ if (rows.some((row) => hasFieldPath(row, segments, PRE_FOLDED)))
17001
17622
  omitted.add(path);
17002
17623
  }
17624
+ const nestedGlobs = globRules.filter((pattern) => pattern.segments.length > 1);
17625
+ const matchedNested = [];
17626
+ if (nestedGlobs.length > 0 && nestedHeads.size > 0) {
17627
+ const pending = new Set(nestedGlobs);
17628
+ const states = compileNestedRules(nestedGlobs);
17629
+ for (const row of rows) {
17630
+ if (pending.size === 0)
17631
+ break;
17632
+ if (row === null || typeof row !== "object")
17633
+ continue;
17634
+ collectNestedMatches(row, states, pending, (pattern) => {
17635
+ matchedNested.push(pattern);
17636
+ omitted.add(pattern.raw);
17637
+ });
17638
+ }
17639
+ }
17003
17640
  for (const column of presentColumns) {
17004
17641
  let dot = column.indexOf(".");
17005
17642
  while (dot >= 0) {
17006
- if (dot > 0 && protectedPaths.has(column.slice(0, dot))) {
17643
+ if (dot > 0 && protectedPaths.has(foldFieldPath(column.slice(0, dot)))) {
17007
17644
  omitted.add(column);
17008
17645
  break;
17009
17646
  }
17010
17647
  dot = column.indexOf(".", dot + 1);
17011
17648
  }
17012
17649
  }
17650
+ if (globRules.length > 0) {
17651
+ for (const column of presentColumns) {
17652
+ const folded = foldFieldPath(column);
17653
+ if (matchAny(folded, globRules)) {
17654
+ omitted.add(column);
17655
+ continue;
17656
+ }
17657
+ let dot = folded.indexOf(".");
17658
+ while (dot >= 0) {
17659
+ if (dot > 0 && matchAny(folded.slice(0, dot), globRules)) {
17660
+ omitted.add(column);
17661
+ break;
17662
+ }
17663
+ dot = folded.indexOf(".", dot + 1);
17664
+ }
17665
+ }
17666
+ }
17013
17667
  const omittedColumns = Array.from(omitted);
17014
17668
  if (omittedColumns.length === 0) {
17015
- return { filteredRows: rows, omittedColumns: [] };
17669
+ return { filteredRows: rows, omittedColumns: [], reachesOmitted: () => false };
17016
17670
  }
17017
- const filteredRows = omitFieldPaths(rows, omittedColumns);
17018
- return { filteredRows, omittedColumns };
17671
+ const removalPaths = matchedNested.length === 0 ? omittedColumns : omittedColumns.filter((path) => !matchedNested.some((p) => p.raw === path));
17672
+ const filteredRows = omitFieldPaths(rows, removalPaths, matchedNested.length > 0 ? { ...FOLD_CASE2, patterns: matchedNested } : FOLD_CASE2);
17673
+ return {
17674
+ filteredRows,
17675
+ omittedColumns,
17676
+ reachesOmitted: (path) => {
17677
+ if (omittedByName(removalPaths, path))
17678
+ return true;
17679
+ if (matchedNested.length === 0)
17680
+ return false;
17681
+ const folded = foldFieldPath(path);
17682
+ if (matchAny(folded, matchedNested))
17683
+ return true;
17684
+ let dot = folded.indexOf(".");
17685
+ while (dot > 0) {
17686
+ if (matchAny(folded.slice(0, dot), matchedNested))
17687
+ return true;
17688
+ dot = folded.indexOf(".", dot + 1);
17689
+ }
17690
+ return false;
17691
+ }
17692
+ };
17019
17693
  }
17020
17694
  buildSecurityNotification(_tableName, omittedColumns) {
17021
17695
  if (omittedColumns.length === 0) {
@@ -17026,11 +17700,17 @@ class BlacklistValidator {
17026
17700
  });
17027
17701
  }
17028
17702
  }
17703
+ var FOLD_CASE2, PRE_FOLDED;
17029
17704
  var init_blacklist_validator = __esm(() => {
17705
+ init_glob();
17030
17706
  init_blacklist();
17031
17707
  init_message_loader();
17032
17708
  init_field_projection();
17709
+ init_path_matcher();
17710
+ init_blacklist_fold();
17033
17711
  init_es_index_target();
17712
+ FOLD_CASE2 = { caseInsensitive: true };
17713
+ PRE_FOLDED = { caseInsensitive: true, alreadyFolded: true };
17034
17714
  });
17035
17715
 
17036
17716
  // src/core/verification/types.ts
@@ -18040,6 +18720,7 @@ var init_core = __esm(() => {
18040
18720
  init_schema_updater();
18041
18721
  init_concurrent_lock();
18042
18722
  init_error_recovery();
18723
+ init_blacklist_manager();
18043
18724
  init_blacklist_validator();
18044
18725
  init_size_category();
18045
18726
  init_health_checker();
@@ -18418,6 +19099,355 @@ var init_sql_lexical = __esm(() => {
18418
19099
  DOLLAR_QUOTE_DELIMITER = /^\$(?:(?:[A-Za-z_]|[\u0080-\uFFFF])(?:[A-Za-z0-9_]|[\u0080-\uFFFF])*)?\$/;
18419
19100
  });
18420
19101
 
19102
+ // src/utils/sql-tables.ts
19103
+ var exports_sql_tables = {};
19104
+ __export(exports_sql_tables, {
19105
+ extractTableReferences: () => extractTableReferences
19106
+ });
19107
+ function tokenize(sql, dialect, backslashEscapes) {
19108
+ const tokens = [];
19109
+ const mysqlDialect = dialect === "mysql" || dialect === "mariadb";
19110
+ let i = 0;
19111
+ while (i < sql.length) {
19112
+ const char = sql[i];
19113
+ const dashFollowerCode = sql.charCodeAt(i + 2);
19114
+ if (char === "-" && sql[i + 1] === "-" && (!mysqlDialect || sql[i + 2] === undefined || dashFollowerCode <= 32 || dashFollowerCode === 127)) {
19115
+ while (i < sql.length && sql[i] !== `
19116
+ `)
19117
+ i++;
19118
+ continue;
19119
+ }
19120
+ if (mysqlDialect && char === "#") {
19121
+ while (i < sql.length && sql[i] !== `
19122
+ `)
19123
+ i++;
19124
+ continue;
19125
+ }
19126
+ if (char === "/" && sql[i + 1] === "*") {
19127
+ if (mysqlDialect && (sql.startsWith("/*!", i) || sql.startsWith("/*M!", i))) {
19128
+ const prefixLength = sql.startsWith("/*M!", i) ? 4 : 3;
19129
+ const closingIndex = sql.indexOf("*/", i + prefixLength);
19130
+ const bodyEnd = closingIndex === -1 ? sql.length : closingIndex;
19131
+ const body = sql.slice(i + prefixLength, bodyEnd).replace(/^\d+/, " ");
19132
+ tokens.push(...tokenize(body, dialect, backslashEscapes));
19133
+ i = closingIndex === -1 ? sql.length : closingIndex + 2;
19134
+ continue;
19135
+ }
19136
+ const nests = dialect === "postgresql";
19137
+ let depth = 1;
19138
+ i += 2;
19139
+ while (i < sql.length && depth > 0) {
19140
+ if (nests && sql[i] === "/" && sql[i + 1] === "*") {
19141
+ depth++;
19142
+ i += 2;
19143
+ continue;
19144
+ }
19145
+ if (sql[i] === "*" && sql[i + 1] === "/") {
19146
+ depth--;
19147
+ i += 2;
19148
+ continue;
19149
+ }
19150
+ i++;
19151
+ }
19152
+ continue;
19153
+ }
19154
+ if (dialect === "postgresql" && char === "$") {
19155
+ const delimiter = dollarQuoteDelimiterAt(sql, i);
19156
+ if (delimiter) {
19157
+ i += delimiter.length;
19158
+ const closingIndex = sql.indexOf(delimiter, i);
19159
+ i = closingIndex === -1 ? sql.length : closingIndex + delimiter.length;
19160
+ continue;
19161
+ }
19162
+ }
19163
+ if (char === "'") {
19164
+ i++;
19165
+ while (i < sql.length) {
19166
+ if (sql[i] === "'") {
19167
+ if (sql[i + 1] === "'") {
19168
+ i += 2;
19169
+ continue;
19170
+ }
19171
+ i++;
19172
+ break;
19173
+ }
19174
+ if (backslashEscapes && sql[i] === "\\") {
19175
+ i += 2;
19176
+ continue;
19177
+ }
19178
+ i++;
19179
+ }
19180
+ continue;
19181
+ }
19182
+ if (char === '"' || char === "`") {
19183
+ const quote = char;
19184
+ i++;
19185
+ let value = "";
19186
+ while (i < sql.length) {
19187
+ if (sql[i] === quote) {
19188
+ if (sql[i + 1] === quote) {
19189
+ value += quote;
19190
+ i += 2;
19191
+ continue;
19192
+ }
19193
+ i++;
19194
+ break;
19195
+ }
19196
+ if (backslashEscapes && sql[i] === "\\") {
19197
+ value += sql[i + 1] ?? "";
19198
+ i += 2;
19199
+ continue;
19200
+ }
19201
+ value += sql[i];
19202
+ i++;
19203
+ }
19204
+ tokens.push({ value, kind: "identifier", quoted: true });
19205
+ continue;
19206
+ }
19207
+ if (IDENTIFIER_START2.test(char)) {
19208
+ let value = "";
19209
+ while (i < sql.length && IDENTIFIER_PART.test(sql[i])) {
19210
+ value += sql[i];
19211
+ i++;
19212
+ }
19213
+ tokens.push({ value, kind: "identifier", quoted: false });
19214
+ continue;
19215
+ }
19216
+ if (char === "." || char === "," || char === "(" || char === ")" || char === ";") {
19217
+ tokens.push({ value: char, kind: "punctuation", quoted: false });
19218
+ i++;
19219
+ continue;
19220
+ }
19221
+ i++;
19222
+ }
19223
+ return tokens;
19224
+ }
19225
+ function isKeyword(token, keywords) {
19226
+ if (!token || token.kind !== "identifier" || token.quoted)
19227
+ return false;
19228
+ return keywords.has(token.value.toUpperCase());
19229
+ }
19230
+ function isPunctuation(token, value) {
19231
+ return token?.kind === "punctuation" && token.value === value;
19232
+ }
19233
+ function readQualifiedName(tokens, index) {
19234
+ const first = tokens[index];
19235
+ if (!first || first.kind !== "identifier")
19236
+ return null;
19237
+ const parts = [first.value];
19238
+ let cursor = index + 1;
19239
+ while (isPunctuation(tokens[cursor], ".") && tokens[cursor + 1]?.kind === "identifier") {
19240
+ parts.push(tokens[cursor + 1].value);
19241
+ cursor += 2;
19242
+ }
19243
+ return { parts, next: cursor };
19244
+ }
19245
+ function decodedVariants(value) {
19246
+ const decoded = value.replace(ANY_ESCAPE_SEQUENCE, (whole, _escape, long, short) => fromCodePointOrRaw(long ?? short, whole));
19247
+ return decoded === value ? [] : [decoded];
19248
+ }
19249
+ function fromCodePointOrRaw(hex, whole) {
19250
+ const codePoint = parseInt(hex, 16);
19251
+ return codePoint <= 1114111 ? String.fromCodePoint(codePoint) : whole;
19252
+ }
19253
+ function extractTableReferences(sql, options = {}) {
19254
+ const seen = new Set;
19255
+ const references = [];
19256
+ const record = (name) => {
19257
+ const key = name.toLowerCase();
19258
+ if (name.length === 0 || seen.has(key))
19259
+ return;
19260
+ seen.add(key);
19261
+ references.push(name);
19262
+ };
19263
+ const dialects = options.dialect ? [options.dialect] : ["postgresql", "mysql", undefined];
19264
+ for (const dialect of dialects) {
19265
+ for (const backslashEscapes of [false, true]) {
19266
+ collectReferences(tokenize(sql, dialect, backslashEscapes), record);
19267
+ }
19268
+ }
19269
+ return references;
19270
+ }
19271
+ function collectReferences(tokens, record) {
19272
+ const recordName = (parts) => {
19273
+ const bare = parts[parts.length - 1];
19274
+ record(bare);
19275
+ for (const variant of decodedVariants(bare))
19276
+ record(variant);
19277
+ if (parts.length > 1)
19278
+ record(parts.join("."));
19279
+ };
19280
+ let i = 0;
19281
+ while (i < tokens.length) {
19282
+ if (!isKeyword(tokens[i], TABLE_INTRODUCERS)) {
19283
+ i++;
19284
+ continue;
19285
+ }
19286
+ const introducer = tokens[i].value.toUpperCase();
19287
+ const parenMeansFunction = introducer === "FROM" || introducer === "JOIN";
19288
+ let cursor = i + 1;
19289
+ while (isKeyword(tokens[cursor], PRE_TABLE_NOISE))
19290
+ cursor++;
19291
+ while (parenMeansFunction && isPunctuation(tokens[cursor], "(") && !isKeyword(tokens[cursor + 1], SUBQUERY_OPENERS) && tokens[cursor + 1]?.kind === "identifier") {
19292
+ cursor++;
19293
+ }
19294
+ let expectTable = true;
19295
+ while (expectTable) {
19296
+ expectTable = false;
19297
+ const name = readQualifiedName(tokens, cursor);
19298
+ if (!name)
19299
+ break;
19300
+ const isFunctionCall = parenMeansFunction && isPunctuation(tokens[name.next], "(");
19301
+ if (!isFunctionCall)
19302
+ recordName(name.parts);
19303
+ cursor = name.next;
19304
+ if (isFunctionCall)
19305
+ break;
19306
+ if (isKeyword(tokens[cursor], AS_KEYWORD))
19307
+ cursor++;
19308
+ if (tokens[cursor]?.kind === "identifier" && !isKeyword(tokens[cursor], POST_TABLE_KEYWORDS)) {
19309
+ cursor++;
19310
+ }
19311
+ while (isPunctuation(tokens[cursor], "(")) {
19312
+ let depth = 0;
19313
+ do {
19314
+ if (isPunctuation(tokens[cursor], "("))
19315
+ depth++;
19316
+ else if (isPunctuation(tokens[cursor], ")"))
19317
+ depth--;
19318
+ cursor++;
19319
+ } while (depth > 0 && cursor < tokens.length);
19320
+ if (tokens[cursor]?.kind === "identifier" && !isKeyword(tokens[cursor], POST_TABLE_KEYWORDS))
19321
+ cursor++;
19322
+ }
19323
+ if (isPunctuation(tokens[cursor], ",")) {
19324
+ cursor++;
19325
+ expectTable = true;
19326
+ }
19327
+ }
19328
+ i = Math.max(cursor, i + 1);
19329
+ }
19330
+ let index = 0;
19331
+ while (index < tokens.length) {
19332
+ const token = tokens[index];
19333
+ if (!token || token.kind !== "identifier") {
19334
+ index++;
19335
+ continue;
19336
+ }
19337
+ const name = readQualifiedName(tokens, index);
19338
+ for (let part = 0;part < name.parts.length; part++) {
19339
+ const value = name.parts[part];
19340
+ const isQuoted = tokens[index + part * 2]?.quoted === true;
19341
+ if (isQuoted || !RESERVED_KEYWORDS.has(value.toUpperCase())) {
19342
+ record(value);
19343
+ for (const variant of decodedVariants(value))
19344
+ record(variant);
19345
+ }
19346
+ }
19347
+ if (name.parts.length > 1)
19348
+ record(name.parts.join("."));
19349
+ index = name.next;
19350
+ }
19351
+ }
19352
+ var TABLE_INTRODUCERS, PRE_TABLE_NOISE, SUBQUERY_OPENERS, AS_KEYWORD, POST_TABLE_KEYWORDS, RESERVED_KEYWORDS, IDENTIFIER_START2, IDENTIFIER_PART, ANY_ESCAPE_SEQUENCE;
19353
+ var init_sql_tables = __esm(() => {
19354
+ init_sql_lexical();
19355
+ TABLE_INTRODUCERS = new Set([
19356
+ "FROM",
19357
+ "JOIN",
19358
+ "INTO",
19359
+ "UPDATE",
19360
+ "TABLE",
19361
+ "TRUNCATE",
19362
+ "COPY",
19363
+ "USING",
19364
+ "STRAIGHT_JOIN"
19365
+ ]);
19366
+ PRE_TABLE_NOISE = new Set(["ONLY", "LATERAL", "TABLE"]);
19367
+ SUBQUERY_OPENERS = new Set(["SELECT", "WITH", "VALUES", "TABLE"]);
19368
+ AS_KEYWORD = new Set(["AS"]);
19369
+ POST_TABLE_KEYWORDS = new Set([
19370
+ "AS",
19371
+ "ON",
19372
+ "USING",
19373
+ "WHERE",
19374
+ "GROUP",
19375
+ "ORDER",
19376
+ "HAVING",
19377
+ "LIMIT",
19378
+ "OFFSET",
19379
+ "FETCH",
19380
+ "WINDOW",
19381
+ "UNION",
19382
+ "INTERSECT",
19383
+ "EXCEPT",
19384
+ "JOIN",
19385
+ "INNER",
19386
+ "LEFT",
19387
+ "RIGHT",
19388
+ "FULL",
19389
+ "OUTER",
19390
+ "CROSS",
19391
+ "NATURAL",
19392
+ "STRAIGHT_JOIN",
19393
+ "SET",
19394
+ "VALUES",
19395
+ "SELECT",
19396
+ "RETURNING",
19397
+ "FOR",
19398
+ "INTO",
19399
+ "PARTITION",
19400
+ "WITH",
19401
+ "TABLESAMPLE",
19402
+ "FORCE",
19403
+ "IGNORE",
19404
+ "USE"
19405
+ ]);
19406
+ RESERVED_KEYWORDS = new Set([
19407
+ "ALL",
19408
+ "AND",
19409
+ "AS",
19410
+ "ASC",
19411
+ "CASE",
19412
+ "CROSS",
19413
+ "DESC",
19414
+ "DISTINCT",
19415
+ "ELSE",
19416
+ "FALSE",
19417
+ "FOR",
19418
+ "FROM",
19419
+ "GROUP",
19420
+ "HAVING",
19421
+ "IN",
19422
+ "INNER",
19423
+ "INTO",
19424
+ "IS",
19425
+ "JOIN",
19426
+ "LEFT",
19427
+ "LIKE",
19428
+ "LIMIT",
19429
+ "NOT",
19430
+ "NULL",
19431
+ "ON",
19432
+ "OR",
19433
+ "ORDER",
19434
+ "OUTER",
19435
+ "RIGHT",
19436
+ "SELECT",
19437
+ "THEN",
19438
+ "TRUE",
19439
+ "UNION",
19440
+ "USING",
19441
+ "VALUES",
19442
+ "WHEN",
19443
+ "WHERE",
19444
+ "WITH"
19445
+ ]);
19446
+ IDENTIFIER_START2 = /[A-Za-z_\u0080-\uFFFF]/;
19447
+ IDENTIFIER_PART = /[A-Za-z0-9_$\u0080-\uFFFF]/;
19448
+ ANY_ESCAPE_SEQUENCE = /([^0-9a-fA-F+'"\s])(?:\+([0-9a-fA-F]{6})|([0-9a-fA-F]{4}))/g;
19449
+ });
19450
+
18421
19451
  // src/core/permission/sql-analysis.ts
18422
19452
  function normalizeSQL(sql) {
18423
19453
  return sql.replace(/--[^\n]*\n/g, `
@@ -19010,6 +20040,9 @@ var init_engine_hints = __esm(() => {
19010
20040
  });
19011
20041
 
19012
20042
  // src/core/audit/integration-helper.ts
20043
+ function sqlDialectFor(engine) {
20044
+ return SQL_AUDIT_DIALECTS.includes(engine) ? engine : null;
20045
+ }
19013
20046
  async function getAuditLogger(config, configPath, connectionName) {
19014
20047
  const storagePath = await resolveConfigStoragePath(configPath);
19015
20048
  const connName = connectionName || config.effectiveConnectionName || getGlobalConnectionName() || "default";
@@ -19051,6 +20084,8 @@ async function writeAuditEntryResult(config, commandName, options, outcome) {
19051
20084
  const logger = await getAuditLogger(config, options.config || ".dbcli", connectionName);
19052
20085
  const engine = config.connection?.system || "postgresql";
19053
20086
  const target = outcome.target || getOperationTarget(engine, commandName, options, outcome.sql);
20087
+ const auditDialect = sqlDialectFor(engine);
20088
+ const blacklistChecked = auditDialect ? extractTableReferences(outcome.sql ?? "", { dialect: auditDialect }) : [];
19054
20089
  let tier = outcome.sideEffectTier ?? getEngineCapability(engine, commandName).tier;
19055
20090
  if (options.dryRun || options.plan) {
19056
20091
  tier = "dry-run";
@@ -19075,6 +20110,7 @@ async function writeAuditEntryResult(config, commandName, options, outcome) {
19075
20110
  ...outcome.recovery_ref && { recovery_ref: outcome.recovery_ref },
19076
20111
  metadata: {
19077
20112
  ...outcome.metadata ?? {},
20113
+ ...blacklistChecked.length > 0 && { blacklist_checked: blacklistChecked },
19078
20114
  connection_name: connectionName,
19079
20115
  environment: config.effectiveEnvironment ?? null
19080
20116
  }
@@ -19087,15 +20123,17 @@ async function writeAuditEntryResult(config, commandName, options, outcome) {
19087
20123
  };
19088
20124
  }
19089
20125
  }
19090
- var _sessionIdService = null, _loggers, AuditRequiredError;
20126
+ var SQL_AUDIT_DIALECTS, _sessionIdService = null, _loggers, AuditRequiredError;
19091
20127
  var init_integration_helper = __esm(() => {
19092
20128
  init_logger2();
19093
20129
  init_session_id();
19094
20130
  init_config_binding();
19095
20131
  init_config();
19096
20132
  init_capabilities();
20133
+ init_sql_tables();
19097
20134
  init_redaction();
19098
20135
  init_engine_hints();
20136
+ SQL_AUDIT_DIALECTS = ["postgresql", "mysql", "mariadb"];
19099
20137
  _loggers = new Map;
19100
20138
  AuditRequiredError = class AuditRequiredError extends Error {
19101
20139
  constructor(detail) {
@@ -19170,73 +20208,6 @@ async function getAllTablesFromAdapter(adapter) {
19170
20208
  }
19171
20209
  var init_error_suggester = () => {};
19172
20210
 
19173
- // src/core/mongo/path-matcher.ts
19174
- function compilePatterns(raw) {
19175
- const patterns = [];
19176
- const rejected = [];
19177
- for (const entry of raw) {
19178
- if (typeof entry !== "string" || entry.length === 0) {
19179
- rejected.push({ raw: String(entry ?? ""), reason: "must be a non-empty string" });
19180
- continue;
19181
- }
19182
- const segments = entry.split(".");
19183
- if (segments.length === 0 || segments.some((s) => s.length === 0)) {
19184
- rejected.push({ raw: entry, reason: "empty path segment" });
19185
- continue;
19186
- }
19187
- const wildcardIndices = segments.map((s, i) => s.includes("*") ? i : -1).filter((i) => i >= 0);
19188
- if (wildcardIndices.length === 0) {
19189
- patterns.push({ raw: entry, segments, wildcardTail: false });
19190
- continue;
19191
- }
19192
- const lastIndex = segments.length - 1;
19193
- const onlyTail = wildcardIndices.length === 1 && wildcardIndices[0] === lastIndex && segments[lastIndex] === "*";
19194
- if (!onlyTail) {
19195
- rejected.push({ raw: entry, reason: "wildcard must be the final segment" });
19196
- continue;
19197
- }
19198
- if (segments.length === 1) {
19199
- rejected.push({ raw: entry, reason: "wildcard must have a parent path" });
19200
- continue;
19201
- }
19202
- patterns.push({ raw: entry, segments: segments.slice(0, -1), wildcardTail: true });
19203
- }
19204
- return { patterns, rejected };
19205
- }
19206
- function matchAny(path, patterns) {
19207
- if (patterns.length === 0)
19208
- return false;
19209
- const pathSegments = path.split(".");
19210
- for (const pat of patterns) {
19211
- if (pat.wildcardTail) {
19212
- if (pathSegments.length < pat.segments.length)
19213
- continue;
19214
- let ok = true;
19215
- for (let i = 0;i < pat.segments.length; i++) {
19216
- if (pat.segments[i] !== pathSegments[i]) {
19217
- ok = false;
19218
- break;
19219
- }
19220
- }
19221
- if (ok)
19222
- return true;
19223
- } else {
19224
- if (pat.segments.length !== pathSegments.length)
19225
- continue;
19226
- let ok = true;
19227
- for (let i = 0;i < pat.segments.length; i++) {
19228
- if (pat.segments[i] !== pathSegments[i]) {
19229
- ok = false;
19230
- break;
19231
- }
19232
- }
19233
- if (ok)
19234
- return true;
19235
- }
19236
- }
19237
- return false;
19238
- }
19239
-
19240
20211
  // src/utils/bounded-parallel.ts
19241
20212
  async function mapWithConcurrency(items, limit, worker) {
19242
20213
  if (items.length === 0)
@@ -21322,7 +22293,11 @@ function markRedactedColumns(cols, collection, blacklist) {
21322
22293
  const raw = (blacklist.columns ?? {})[collection];
21323
22294
  if (!raw || raw.length === 0)
21324
22295
  return cols;
21325
- const { patterns } = compilePatterns(raw);
22296
+ const { patterns, rejected } = compilePatterns(raw);
22297
+ if (rejected.length > 0) {
22298
+ const detail = rejected.map((r) => `'${r.raw}' (${r.reason})`).join(", ");
22299
+ throw new Error(`blacklist.columns for '${collection}' has entries this matcher cannot read: ${detail}`);
22300
+ }
21326
22301
  if (patterns.length === 0)
21327
22302
  return cols;
21328
22303
  return cols.map((c) => matchAny(c.name, patterns) ? { ...c, redacted: true } : c);
@@ -21452,7 +22427,7 @@ Key: ${schema.name}`);
21452
22427
  await esAdapter.disconnect();
21453
22428
  }
21454
22429
  }
21455
- const adapter = AdapterFactory.createAdapter(config.connection);
22430
+ const adapter = AdapterFactory.createAdapter(config);
21456
22431
  await adapter.connect();
21457
22432
  const mongoMeta = config.connection.system === "mongodb" ? {
21458
22433
  blacklist: config.blacklist ?? { tables: [], columns: {} },
@@ -21770,6 +22745,7 @@ var init_schema = __esm(() => {
21770
22745
  init_validation();
21771
22746
  init_connection_selector();
21772
22747
  init_error_suggester();
22748
+ init_path_matcher();
21773
22749
  init_config_path();
21774
22750
  ALLOWED_FORMATS2 = ["table", "json"];
21775
22751
  schemaCommand = new Command().name("schema").description("Display table schema, scan database schema, or refresh existing schema with detected changes").argument("[table]", "Optional: table name to inspect (if omitted, scans all tables)").option("--format <format>", "Output format: table (default) or json", "table").option("--config <path>", "Path to .dbcli config file", ".dbcli").addOption(createConnectionSelectorOption()).option("--refresh", "Refresh schema by detecting changes from database", false).option("--reset", "Clear all existing schema data and re-fetch from database", false).option("--force", "Skip confirmation when updating schema data", false).option("--sample-size <n>", "MongoDB only: number of documents to sample for schema inference (default 100, max 1000). Ignored on SQL connections.").option("--sample-method <method>", 'MongoDB only: "random" (default, uses $sample) or "natural" (uses find().limit()). Ignored on SQL connections.', "random").option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(schemaAction);
@@ -21874,355 +22850,6 @@ async function openInBrowser(target) {
21874
22850
  }
21875
22851
  var init_opener = () => {};
21876
22852
 
21877
- // src/utils/sql-tables.ts
21878
- var exports_sql_tables = {};
21879
- __export(exports_sql_tables, {
21880
- extractTableReferences: () => extractTableReferences
21881
- });
21882
- function tokenize(sql, dialect, backslashEscapes) {
21883
- const tokens = [];
21884
- const mysqlDialect = dialect === "mysql" || dialect === "mariadb";
21885
- let i = 0;
21886
- while (i < sql.length) {
21887
- const char = sql[i];
21888
- const dashFollowerCode = sql.charCodeAt(i + 2);
21889
- if (char === "-" && sql[i + 1] === "-" && (!mysqlDialect || sql[i + 2] === undefined || dashFollowerCode <= 32 || dashFollowerCode === 127)) {
21890
- while (i < sql.length && sql[i] !== `
21891
- `)
21892
- i++;
21893
- continue;
21894
- }
21895
- if (mysqlDialect && char === "#") {
21896
- while (i < sql.length && sql[i] !== `
21897
- `)
21898
- i++;
21899
- continue;
21900
- }
21901
- if (char === "/" && sql[i + 1] === "*") {
21902
- if (mysqlDialect && (sql.startsWith("/*!", i) || sql.startsWith("/*M!", i))) {
21903
- const prefixLength = sql.startsWith("/*M!", i) ? 4 : 3;
21904
- const closingIndex = sql.indexOf("*/", i + prefixLength);
21905
- const bodyEnd = closingIndex === -1 ? sql.length : closingIndex;
21906
- const body = sql.slice(i + prefixLength, bodyEnd).replace(/^\d+/, " ");
21907
- tokens.push(...tokenize(body, dialect, backslashEscapes));
21908
- i = closingIndex === -1 ? sql.length : closingIndex + 2;
21909
- continue;
21910
- }
21911
- const nests = dialect === "postgresql";
21912
- let depth = 1;
21913
- i += 2;
21914
- while (i < sql.length && depth > 0) {
21915
- if (nests && sql[i] === "/" && sql[i + 1] === "*") {
21916
- depth++;
21917
- i += 2;
21918
- continue;
21919
- }
21920
- if (sql[i] === "*" && sql[i + 1] === "/") {
21921
- depth--;
21922
- i += 2;
21923
- continue;
21924
- }
21925
- i++;
21926
- }
21927
- continue;
21928
- }
21929
- if (dialect === "postgresql" && char === "$") {
21930
- const delimiter = dollarQuoteDelimiterAt(sql, i);
21931
- if (delimiter) {
21932
- i += delimiter.length;
21933
- const closingIndex = sql.indexOf(delimiter, i);
21934
- i = closingIndex === -1 ? sql.length : closingIndex + delimiter.length;
21935
- continue;
21936
- }
21937
- }
21938
- if (char === "'") {
21939
- i++;
21940
- while (i < sql.length) {
21941
- if (sql[i] === "'") {
21942
- if (sql[i + 1] === "'") {
21943
- i += 2;
21944
- continue;
21945
- }
21946
- i++;
21947
- break;
21948
- }
21949
- if (backslashEscapes && sql[i] === "\\") {
21950
- i += 2;
21951
- continue;
21952
- }
21953
- i++;
21954
- }
21955
- continue;
21956
- }
21957
- if (char === '"' || char === "`") {
21958
- const quote = char;
21959
- i++;
21960
- let value = "";
21961
- while (i < sql.length) {
21962
- if (sql[i] === quote) {
21963
- if (sql[i + 1] === quote) {
21964
- value += quote;
21965
- i += 2;
21966
- continue;
21967
- }
21968
- i++;
21969
- break;
21970
- }
21971
- if (backslashEscapes && sql[i] === "\\") {
21972
- value += sql[i + 1] ?? "";
21973
- i += 2;
21974
- continue;
21975
- }
21976
- value += sql[i];
21977
- i++;
21978
- }
21979
- tokens.push({ value, kind: "identifier", quoted: true });
21980
- continue;
21981
- }
21982
- if (IDENTIFIER_START2.test(char)) {
21983
- let value = "";
21984
- while (i < sql.length && IDENTIFIER_PART.test(sql[i])) {
21985
- value += sql[i];
21986
- i++;
21987
- }
21988
- tokens.push({ value, kind: "identifier", quoted: false });
21989
- continue;
21990
- }
21991
- if (char === "." || char === "," || char === "(" || char === ")" || char === ";") {
21992
- tokens.push({ value: char, kind: "punctuation", quoted: false });
21993
- i++;
21994
- continue;
21995
- }
21996
- i++;
21997
- }
21998
- return tokens;
21999
- }
22000
- function isKeyword(token, keywords) {
22001
- if (!token || token.kind !== "identifier" || token.quoted)
22002
- return false;
22003
- return keywords.has(token.value.toUpperCase());
22004
- }
22005
- function isPunctuation(token, value) {
22006
- return token?.kind === "punctuation" && token.value === value;
22007
- }
22008
- function readQualifiedName(tokens, index) {
22009
- const first = tokens[index];
22010
- if (!first || first.kind !== "identifier")
22011
- return null;
22012
- const parts = [first.value];
22013
- let cursor = index + 1;
22014
- while (isPunctuation(tokens[cursor], ".") && tokens[cursor + 1]?.kind === "identifier") {
22015
- parts.push(tokens[cursor + 1].value);
22016
- cursor += 2;
22017
- }
22018
- return { parts, next: cursor };
22019
- }
22020
- function decodedVariants(value) {
22021
- const decoded = value.replace(ANY_ESCAPE_SEQUENCE, (whole, _escape, long, short) => fromCodePointOrRaw(long ?? short, whole));
22022
- return decoded === value ? [] : [decoded];
22023
- }
22024
- function fromCodePointOrRaw(hex, whole) {
22025
- const codePoint = parseInt(hex, 16);
22026
- return codePoint <= 1114111 ? String.fromCodePoint(codePoint) : whole;
22027
- }
22028
- function extractTableReferences(sql, options = {}) {
22029
- const seen = new Set;
22030
- const references = [];
22031
- const record = (name) => {
22032
- const key = name.toLowerCase();
22033
- if (name.length === 0 || seen.has(key))
22034
- return;
22035
- seen.add(key);
22036
- references.push(name);
22037
- };
22038
- const dialects = options.dialect ? [options.dialect] : ["postgresql", "mysql", undefined];
22039
- for (const dialect of dialects) {
22040
- for (const backslashEscapes of [false, true]) {
22041
- collectReferences(tokenize(sql, dialect, backslashEscapes), record);
22042
- }
22043
- }
22044
- return references;
22045
- }
22046
- function collectReferences(tokens, record) {
22047
- const recordName = (parts) => {
22048
- const bare = parts[parts.length - 1];
22049
- record(bare);
22050
- for (const variant of decodedVariants(bare))
22051
- record(variant);
22052
- if (parts.length > 1)
22053
- record(parts.join("."));
22054
- };
22055
- let i = 0;
22056
- while (i < tokens.length) {
22057
- if (!isKeyword(tokens[i], TABLE_INTRODUCERS)) {
22058
- i++;
22059
- continue;
22060
- }
22061
- const introducer = tokens[i].value.toUpperCase();
22062
- const parenMeansFunction = introducer === "FROM" || introducer === "JOIN";
22063
- let cursor = i + 1;
22064
- while (isKeyword(tokens[cursor], PRE_TABLE_NOISE))
22065
- cursor++;
22066
- while (parenMeansFunction && isPunctuation(tokens[cursor], "(") && !isKeyword(tokens[cursor + 1], SUBQUERY_OPENERS) && tokens[cursor + 1]?.kind === "identifier") {
22067
- cursor++;
22068
- }
22069
- let expectTable = true;
22070
- while (expectTable) {
22071
- expectTable = false;
22072
- const name = readQualifiedName(tokens, cursor);
22073
- if (!name)
22074
- break;
22075
- const isFunctionCall = parenMeansFunction && isPunctuation(tokens[name.next], "(");
22076
- if (!isFunctionCall)
22077
- recordName(name.parts);
22078
- cursor = name.next;
22079
- if (isFunctionCall)
22080
- break;
22081
- if (isKeyword(tokens[cursor], AS_KEYWORD))
22082
- cursor++;
22083
- if (tokens[cursor]?.kind === "identifier" && !isKeyword(tokens[cursor], POST_TABLE_KEYWORDS)) {
22084
- cursor++;
22085
- }
22086
- while (isPunctuation(tokens[cursor], "(")) {
22087
- let depth = 0;
22088
- do {
22089
- if (isPunctuation(tokens[cursor], "("))
22090
- depth++;
22091
- else if (isPunctuation(tokens[cursor], ")"))
22092
- depth--;
22093
- cursor++;
22094
- } while (depth > 0 && cursor < tokens.length);
22095
- if (tokens[cursor]?.kind === "identifier" && !isKeyword(tokens[cursor], POST_TABLE_KEYWORDS))
22096
- cursor++;
22097
- }
22098
- if (isPunctuation(tokens[cursor], ",")) {
22099
- cursor++;
22100
- expectTable = true;
22101
- }
22102
- }
22103
- i = Math.max(cursor, i + 1);
22104
- }
22105
- let index = 0;
22106
- while (index < tokens.length) {
22107
- const token = tokens[index];
22108
- if (!token || token.kind !== "identifier") {
22109
- index++;
22110
- continue;
22111
- }
22112
- const name = readQualifiedName(tokens, index);
22113
- for (let part = 0;part < name.parts.length; part++) {
22114
- const value = name.parts[part];
22115
- const isQuoted = tokens[index + part * 2]?.quoted === true;
22116
- if (isQuoted || !RESERVED_KEYWORDS.has(value.toUpperCase())) {
22117
- record(value);
22118
- for (const variant of decodedVariants(value))
22119
- record(variant);
22120
- }
22121
- }
22122
- if (name.parts.length > 1)
22123
- record(name.parts.join("."));
22124
- index = name.next;
22125
- }
22126
- }
22127
- var TABLE_INTRODUCERS, PRE_TABLE_NOISE, SUBQUERY_OPENERS, AS_KEYWORD, POST_TABLE_KEYWORDS, RESERVED_KEYWORDS, IDENTIFIER_START2, IDENTIFIER_PART, ANY_ESCAPE_SEQUENCE;
22128
- var init_sql_tables = __esm(() => {
22129
- init_sql_lexical();
22130
- TABLE_INTRODUCERS = new Set([
22131
- "FROM",
22132
- "JOIN",
22133
- "INTO",
22134
- "UPDATE",
22135
- "TABLE",
22136
- "TRUNCATE",
22137
- "COPY",
22138
- "USING",
22139
- "STRAIGHT_JOIN"
22140
- ]);
22141
- PRE_TABLE_NOISE = new Set(["ONLY", "LATERAL", "TABLE"]);
22142
- SUBQUERY_OPENERS = new Set(["SELECT", "WITH", "VALUES", "TABLE"]);
22143
- AS_KEYWORD = new Set(["AS"]);
22144
- POST_TABLE_KEYWORDS = new Set([
22145
- "AS",
22146
- "ON",
22147
- "USING",
22148
- "WHERE",
22149
- "GROUP",
22150
- "ORDER",
22151
- "HAVING",
22152
- "LIMIT",
22153
- "OFFSET",
22154
- "FETCH",
22155
- "WINDOW",
22156
- "UNION",
22157
- "INTERSECT",
22158
- "EXCEPT",
22159
- "JOIN",
22160
- "INNER",
22161
- "LEFT",
22162
- "RIGHT",
22163
- "FULL",
22164
- "OUTER",
22165
- "CROSS",
22166
- "NATURAL",
22167
- "STRAIGHT_JOIN",
22168
- "SET",
22169
- "VALUES",
22170
- "SELECT",
22171
- "RETURNING",
22172
- "FOR",
22173
- "INTO",
22174
- "PARTITION",
22175
- "WITH",
22176
- "TABLESAMPLE",
22177
- "FORCE",
22178
- "IGNORE",
22179
- "USE"
22180
- ]);
22181
- RESERVED_KEYWORDS = new Set([
22182
- "ALL",
22183
- "AND",
22184
- "AS",
22185
- "ASC",
22186
- "CASE",
22187
- "CROSS",
22188
- "DESC",
22189
- "DISTINCT",
22190
- "ELSE",
22191
- "FALSE",
22192
- "FOR",
22193
- "FROM",
22194
- "GROUP",
22195
- "HAVING",
22196
- "IN",
22197
- "INNER",
22198
- "INTO",
22199
- "IS",
22200
- "JOIN",
22201
- "LEFT",
22202
- "LIKE",
22203
- "LIMIT",
22204
- "NOT",
22205
- "NULL",
22206
- "ON",
22207
- "OR",
22208
- "ORDER",
22209
- "OUTER",
22210
- "RIGHT",
22211
- "SELECT",
22212
- "THEN",
22213
- "TRUE",
22214
- "UNION",
22215
- "USING",
22216
- "VALUES",
22217
- "WHEN",
22218
- "WHERE",
22219
- "WITH"
22220
- ]);
22221
- IDENTIFIER_START2 = /[A-Za-z_\u0080-\uFFFF]/;
22222
- IDENTIFIER_PART = /[A-Za-z0-9_$\u0080-\uFFFF]/;
22223
- ANY_ESCAPE_SEQUENCE = /([^0-9a-fA-F+'"\s])(?:\+([0-9a-fA-F]{6})|([0-9a-fA-F]{4}))/g;
22224
- });
22225
-
22226
22853
  // src/core/limits.ts
22227
22854
  var DEFAULT_QUERY_ONLY_LIMIT = 1000;
22228
22855
 
@@ -22289,12 +22916,12 @@ class QueryExecutor {
22289
22916
  let columnNames = rows.length > 0 && rows[0] ? Object.keys(rows[0]) : [];
22290
22917
  let filteredRows = rows;
22291
22918
  let securityNotification;
22292
- let omittedColumns = [];
22919
+ let reachesOmitted = () => false;
22293
22920
  if (this.blacklistValidator) {
22294
22921
  const filterResult = this.blacklistValidator.filterColumnsForTables(referencedTables, rows, columnNames);
22295
22922
  filteredRows = filterResult.filteredRows;
22296
22923
  if (filterResult.omittedColumns.length > 0) {
22297
- omittedColumns = filterResult.omittedColumns;
22924
+ reachesOmitted = filterResult.reachesOmitted;
22298
22925
  columnNames = columnNames.filter((col) => !filterResult.omittedColumns.includes(col));
22299
22926
  securityNotification = this.blacklistValidator.buildSecurityNotification(referencedTables[0] ?? "", filterResult.omittedColumns);
22300
22927
  }
@@ -22302,7 +22929,7 @@ class QueryExecutor {
22302
22929
  if (options?.fieldSelection) {
22303
22930
  const fieldSelection = options.fieldSelection.mode === "include" ? {
22304
22931
  mode: "include",
22305
- paths: options.fieldSelection.paths.filter((path2) => !omittedColumns.some((omitted) => path2 === omitted || path2.startsWith(`${omitted}.`)))
22932
+ paths: options.fieldSelection.paths.filter((path2) => !reachesOmitted(path2))
22306
22933
  } : options.fieldSelection;
22307
22934
  const projection = projectRows(filteredRows, fieldSelection);
22308
22935
  filteredRows = projection.rows;
@@ -22402,7 +23029,10 @@ function maskMongoRowsForCollections(rows, collections, blacklist) {
22402
23029
  const prefixKey = `\x00${scope.collection}@${scope.prefix}`;
22403
23030
  return maskMongoRows(atTopLevel, prefixKey, {
22404
23031
  ...blacklist,
22405
- columns: { ...columns, [prefixKey]: rules.map((rule) => `${scope.prefix}.${rule}`) }
23032
+ columns: {
23033
+ ...columns,
23034
+ [prefixKey]: rules.map((rule) => `${escapeGlob(scope.prefix)}.${rule}`)
23035
+ }
22406
23036
  });
22407
23037
  }, rows);
22408
23038
  }
@@ -22411,10 +23041,14 @@ function maskMongoRows(rows, collection, blacklist) {
22411
23041
  const raw = columns[collection] ?? findCaseInsensitive(columns, collection);
22412
23042
  if (!raw || raw.length === 0)
22413
23043
  return rows;
22414
- const { patterns } = compilePatterns(raw);
23044
+ const { patterns, rejected } = compilePatterns(raw);
23045
+ if (rejected.length > 0) {
23046
+ const detail = rejected.map((r) => `'${r.raw}' (${r.reason})`).join(", ");
23047
+ throw new BlacklistError(`blacklist.columns for '${collection}' has entries this matcher cannot read: ${detail}`, collection, "READ");
23048
+ }
22415
23049
  if (patterns.length === 0)
22416
23050
  return rows;
22417
- const idAffected = patterns.some((p) => p.segments.length === 1 && p.segments[0] === "_id" && !p.wildcardTail);
23051
+ const idAffected = patterns.some((p) => p.segments.length === 1 && !p.wildcardTail && globMatches(p.segments[0], "_id", { caseInsensitive: true }));
22418
23052
  if (idAffected) {
22419
23053
  console.error(`[blacklist] collection '${collection}' blacklists '_id'; read paths still expose _id to preserve document references.`);
22420
23054
  }
@@ -22440,7 +23074,7 @@ function maskValue(value, path2, patterns) {
22440
23074
  if (value === null || value === undefined)
22441
23075
  return value;
22442
23076
  if (Array.isArray(value)) {
22443
- return value.map((item) => item && typeof item === "object" && !Array.isArray(item) ? maskRecord(item, path2, patterns) : item);
23077
+ return value.map((item) => maskValue(item, path2, patterns));
22444
23078
  }
22445
23079
  if (typeof value === "object") {
22446
23080
  return maskRecord(value, path2, patterns);
@@ -22448,15 +23082,21 @@ function maskValue(value, path2, patterns) {
22448
23082
  return value;
22449
23083
  }
22450
23084
  function findCaseInsensitive(columns, name) {
22451
- const target = name.toLowerCase();
23085
+ const target = foldFieldPath(name);
22452
23086
  for (const [k, v] of Object.entries(columns)) {
22453
- if (k.toLowerCase() === target)
23087
+ if (foldFieldPath(k) === target)
22454
23088
  return v;
22455
23089
  }
22456
23090
  return;
22457
23091
  }
22458
23092
  var REDACTED2 = "[REDACTED]";
22459
- var init_field_masker = () => {};
23093
+ var init_field_masker = __esm(() => {
23094
+ init_blacklist();
23095
+ init_path_matcher();
23096
+ init_blacklist_fold();
23097
+ init_glob();
23098
+ init_glob();
23099
+ });
22460
23100
 
22461
23101
  // src/core/mongo/collection-references.ts
22462
23102
  function recordTarget(value, found) {
@@ -24224,6 +24864,7 @@ var init_query = __esm(() => {
24224
24864
  init_opener();
24225
24865
  init_query_executor();
24226
24866
  init_config();
24867
+ init_blacklist_manager();
24227
24868
  init_blacklist_validator();
24228
24869
  init_config_path();
24229
24870
  init_validation();
@@ -24570,9 +25211,9 @@ function applySelectPatternRules(facts, factors) {
24570
25211
  }
24571
25212
  }
24572
25213
  function applyBlacklistRules(facts, blacklist, factors) {
24573
- const blacklistedTables = new Set((blacklist.tables ?? []).map((table) => table.toLowerCase()));
25214
+ const blacklistedTables = new Set((blacklist.tables ?? []).map(foldFieldPath));
24574
25215
  for (const table of facts.targetTables) {
24575
- if (blacklistedTables.has(table.toLowerCase())) {
25216
+ if (blacklistedTables.has(foldFieldPath(table))) {
24576
25217
  pushFactor(factors, "table_blacklisted", "block", `Target table ${table} is blacklisted.`);
24577
25218
  }
24578
25219
  }
@@ -24588,7 +25229,7 @@ function applyBlacklistRules(facts, blacklist, factors) {
24588
25229
  function findBlacklistedColumnsForTable(blacklist, table) {
24589
25230
  const columns = blacklist.columns ?? {};
24590
25231
  for (const [tableName2, columnNames] of Object.entries(columns)) {
24591
- if (tableName2.toLowerCase() === table.toLowerCase()) {
25232
+ if (foldFieldPath(tableName2) === foldFieldPath(table)) {
24592
25233
  return new Set(columnNames);
24593
25234
  }
24594
25235
  }
@@ -24679,6 +25320,7 @@ function buildSuggestedCommands(facts, gap) {
24679
25320
  }
24680
25321
  var READ_LIKE_WORDS, WRITE_OR_DDL_LIKE_WORDS;
24681
25322
  var init_query_risk_analyzer = __esm(() => {
25323
+ init_blacklist_fold();
24682
25324
  init_permission_guard();
24683
25325
  init_size_category();
24684
25326
  READ_LIKE_WORDS = ["SELECT", "SHOW", "DESCRIBE", "EXPLAIN", "WITH"];
@@ -26161,6 +26803,7 @@ async function qMongoBranch(snippet, prepared, options, config) {
26161
26803
  }
26162
26804
  var init_q_mongo = __esm(() => {
26163
26805
  init_adapters();
26806
+ init_blacklist_manager();
26164
26807
  init_blacklist_validator();
26165
26808
  init_field_masker();
26166
26809
  init_collection_references();
@@ -26234,6 +26877,19 @@ async function qCommand(name, options, command) {
26234
26877
  const family = engineFamily(engine);
26235
26878
  const sqlDialect = SQL_DIALECTS.find((dialect) => dialect === connectionSystem);
26236
26879
  const classification = family === "sql" ? enforcePermission(prepared.rewrittenSql, config.permission, sqlDialect) : undefined;
26880
+ const blacklistManager = new BlacklistManager(config);
26881
+ const blacklistValidator = new BlacklistValidator(blacklistManager);
26882
+ const redisTokens = family === "redis" ? parseRedisCommand(prepared.rewrittenSql) : [];
26883
+ const targets = family === "sql" ? extractTableReferences(prepared.rewrittenSql, {
26884
+ ...sqlDialect ? { dialect: sqlDialect } : {}
26885
+ }) : family === "es" ? [prepared.execHints?.index ?? ""] : family === "redis" && redisTokens.length > 0 ? redisCommandTargets(redisTokens[0], redisTokens.slice(1)) : [];
26886
+ const targetName = targets[0] ?? "";
26887
+ targetNameForAudit = family === "redis" ? name : targetName || name;
26888
+ if (family === "es") {
26889
+ blacklistValidator.checkIndexBlacklist("SELECT", targetName);
26890
+ } else if ((classification || family === "redis") && targets.length > 0) {
26891
+ blacklistValidator.checkTablesBlacklist(classification?.type ?? redisTokens[0] ?? "READ", targets);
26892
+ }
26237
26893
  if (options.dryRun) {
26238
26894
  console.log(formatDryRun({
26239
26895
  family,
@@ -26255,19 +26911,7 @@ async function qCommand(name, options, command) {
26255
26911
  await qMongoBranch2(snippet, prepared, options, config);
26256
26912
  return;
26257
26913
  }
26258
- const blacklistManager = new BlacklistManager(config);
26259
- const blacklistValidator = new BlacklistValidator(blacklistManager);
26260
- const targets = family === "sql" ? extractTableReferences(prepared.rewrittenSql, {
26261
- ...sqlDialect ? { dialect: sqlDialect } : {}
26262
- }) : family === "es" ? [prepared.execHints?.index ?? ""] : [];
26263
- const targetName = targets[0] ?? "";
26264
- targetNameForAudit = targetName || name;
26265
- if (family === "es") {
26266
- blacklistValidator.checkIndexBlacklist("SELECT", targetName);
26267
- } else if (classification && targets.length > 0) {
26268
- blacklistValidator.checkTablesBlacklist(classification.type, targets);
26269
- }
26270
- const adapter = AdapterFactory.createAdapter(config.connection);
26914
+ const adapter = AdapterFactory.createAdapter(config);
26271
26915
  await adapter.connect();
26272
26916
  try {
26273
26917
  if (!options.ui && options.format !== "html") {
@@ -26388,6 +27032,8 @@ async function readParamFile(path2) {
26388
27032
  return parsed;
26389
27033
  }
26390
27034
  async function handleQError(error, snippetName, options, config) {
27035
+ const redisBlacklistRejection = config?.connection?.system === "redis" && (error instanceof BlacklistError || error instanceof BlacklistRejection);
27036
+ const reportedError = redisBlacklistRejection ? new BlacklistError(t("errors.redis_target_blacklisted"), "[REDACTED]", error instanceof BlacklistError ? error.operation : error.command) : error;
26391
27037
  let auditId = null;
26392
27038
  let envelopeId;
26393
27039
  if (options.recovery === true) {
@@ -26397,21 +27043,22 @@ async function handleQError(error, snippetName, options, config) {
26397
27043
  auditId = await writeAuditEntry(config, "q", options, {
26398
27044
  success: false,
26399
27045
  target: snippetName,
26400
- error,
27046
+ error: reportedError,
26401
27047
  ...envelopeId && { recovery_ref: envelopeId }
26402
27048
  });
26403
27049
  }
26404
27050
  if (envelopeId !== undefined) {
26405
27051
  const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
26406
- emitRecoveryEnvelope2(error, { operation: "q", snippet: snippetName }, { envelopeId, auditRef: auditId ?? undefined });
27052
+ emitRecoveryEnvelope2(reportedError, { operation: "q", snippet: snippetName }, { envelopeId, auditRef: auditId ?? undefined });
26407
27053
  }
26408
27054
  if (error instanceof SavedQueryError) {
26409
27055
  printLocalizedCliError(error.message, error);
26410
27056
  process.exit(1);
26411
27057
  }
26412
- if (error instanceof BlacklistError) {
26413
- printLocalizedCliError(error.message, error);
27058
+ if (error instanceof BlacklistError || error instanceof BlacklistRejection) {
27059
+ printLocalizedCliError(reportedError.message, reportedError);
26414
27060
  process.exit(1);
27061
+ return;
26415
27062
  }
26416
27063
  if (error instanceof PermissionError) {
26417
27064
  printLocalizedCliError(t_vars("errors.permission_denied", { required: error.requiredPermission }), error);
@@ -26522,8 +27169,12 @@ function evaluateExpectation(row, expects) {
26522
27169
  var init_q = __esm(() => {
26523
27170
  init_message_loader();
26524
27171
  init_adapters();
27172
+ init_redis_adapter();
27173
+ init_blacklist_enforcer();
27174
+ init_types3();
26525
27175
  init_config();
26526
27176
  init_config_path();
27177
+ init_blacklist_manager();
26527
27178
  init_blacklist_validator();
26528
27179
  init_blacklist();
26529
27180
  init_permission_guard();
@@ -26792,6 +27443,366 @@ function previewDelete(collection, filter) {
26792
27443
  }
26793
27444
  var INDENT = 2;
26794
27445
 
27446
+ // src/utils/where-parser.ts
27447
+ function parseWhereClause(whereClause) {
27448
+ if (!whereClause || whereClause.trim() === "") {
27449
+ throw new Error("WHERE clause cannot be empty");
27450
+ }
27451
+ const conditions = {};
27452
+ const andParts = whereClause.split(/\s+AND\s+/i);
27453
+ for (const part of andParts) {
27454
+ const match = part.match(/^(\w+)\s*=\s*(.+)$/);
27455
+ if (!match) {
27456
+ throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
27457
+ }
27458
+ const column = match[1];
27459
+ const valueStr = match[2];
27460
+ if (valueStr === undefined || column === undefined) {
27461
+ throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
27462
+ }
27463
+ const trimmed = valueStr.trim();
27464
+ const isFullyQuoted = trimmed.length >= 2 && (trimmed.startsWith("'") && trimmed.endsWith("'") || trimmed.startsWith('"') && trimmed.endsWith('"'));
27465
+ if (!isFullyQuoted && /\s+AND\s*$/i.test(trimmed)) {
27466
+ throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
27467
+ }
27468
+ const stripped = isFullyQuoted ? trimmed.slice(1, -1) : trimmed;
27469
+ let value = stripped;
27470
+ if (stripped !== "" && !isNaN(Number(stripped))) {
27471
+ value = Number(stripped);
27472
+ }
27473
+ if (stripped === "true")
27474
+ value = true;
27475
+ else if (stripped === "false")
27476
+ value = false;
27477
+ else if (stripped === "null")
27478
+ value = null;
27479
+ conditions[column] = value;
27480
+ }
27481
+ return conditions;
27482
+ }
27483
+
27484
+ // src/core/mongo/dml-plan.ts
27485
+ function permissionSql(operation) {
27486
+ if (operation === "insert")
27487
+ return "INSERT INTO dummy";
27488
+ if (operation === "update")
27489
+ return "UPDATE dummy";
27490
+ return "DELETE FROM dummy";
27491
+ }
27492
+ function broadOperation(operation) {
27493
+ if (operation === "insert")
27494
+ return "INSERT";
27495
+ if (operation === "update")
27496
+ return "UPDATE";
27497
+ return "DELETE";
27498
+ }
27499
+ function pushFactor2(factors, code, severity, message) {
27500
+ if (factors.some((f) => f.code === code && f.message === message))
27501
+ return;
27502
+ factors.push({ code, severity, message });
27503
+ }
27504
+ function decide2(factors) {
27505
+ if (factors.some((f) => f.severity === "block"))
27506
+ return "BLOCK";
27507
+ if (factors.some((f) => f.severity === "warn"))
27508
+ return "WARN";
27509
+ return "ALLOW";
27510
+ }
27511
+ function classifyMongoUpdate(setDoc) {
27512
+ const fields = new Set;
27513
+ const tierFactors = [];
27514
+ let hasBlock = false;
27515
+ const hasAnyOperator = Object.keys(setDoc).some((k) => k.startsWith("$"));
27516
+ if (!hasAnyOperator) {
27517
+ for (const k of Object.keys(setDoc))
27518
+ fields.add(k);
27519
+ return { fields: Array.from(fields), tierFactors, hasBlock };
27520
+ }
27521
+ const seenTiers = new Map;
27522
+ for (const [op, payload] of Object.entries(setDoc)) {
27523
+ if (!op.startsWith("$")) {
27524
+ fields.add(op);
27525
+ continue;
27526
+ }
27527
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) {
27528
+ for (const k of Object.keys(payload))
27529
+ fields.add(k);
27530
+ }
27531
+ const tier = MONGO_OPERATOR_TIER[op];
27532
+ if (tier === undefined) {
27533
+ hasBlock = true;
27534
+ tierFactors.push({
27535
+ code: "mongo_unknown_operator",
27536
+ severity: "block",
27537
+ message: `Update uses unknown operator '${op}'. Reject by default; add to tier table if intentional.`
27538
+ });
27539
+ continue;
27540
+ }
27541
+ if (tier === "BLOCK") {
27542
+ hasBlock = true;
27543
+ tierFactors.push({
27544
+ code: "mongo_unknown_operator",
27545
+ severity: "block",
27546
+ message: `Update uses '${op}' which executes server-side code. Operation rejected.`
27547
+ });
27548
+ continue;
27549
+ }
27550
+ if (tier === "SAFE")
27551
+ continue;
27552
+ const bucket = seenTiers.get(tier) ?? [];
27553
+ bucket.push(op);
27554
+ seenTiers.set(tier, bucket);
27555
+ }
27556
+ for (const [tier, ops] of seenTiers) {
27557
+ if (tier === "RENAME") {
27558
+ tierFactors.push({
27559
+ code: "mongo_rename_operator",
27560
+ severity: "warn",
27561
+ message: `Update uses ${ops.join(", ")}; a renamed field keeps its value under a name the read mask does not know.`
27562
+ });
27563
+ } else if (tier === "ARITHMETIC") {
27564
+ tierFactors.push({
27565
+ code: "mongo_arithmetic_operator",
27566
+ severity: "warn",
27567
+ message: `Update uses ${ops.join(", ")}; numeric mutation may compound silently.`
27568
+ });
27569
+ } else if (tier === "ARRAY") {
27570
+ tierFactors.push({
27571
+ code: "mongo_array_operator",
27572
+ severity: "warn",
27573
+ message: `Update uses ${ops.join(", ")}; array mutation can grow unboundedly without a size guard.`
27574
+ });
27575
+ } else if (tier === "BITWISE") {
27576
+ tierFactors.push({
27577
+ code: "mongo_bitwise_operator",
27578
+ severity: "warn",
27579
+ message: `Update uses ${ops.join(", ")}; bitwise updates skip type promotion checks.`
27580
+ });
27581
+ }
27582
+ }
27583
+ return { fields: Array.from(fields), tierFactors, hasBlock };
27584
+ }
27585
+ function hasIdEquality(where) {
27586
+ if (!("_id" in where))
27587
+ return false;
27588
+ const value = where._id;
27589
+ if (value === null || value === undefined)
27590
+ return false;
27591
+ if (typeof value === "object")
27592
+ return false;
27593
+ return true;
27594
+ }
27595
+ function hasNonIdEquality(where) {
27596
+ for (const [key, value] of Object.entries(where)) {
27597
+ if (key === "_id")
27598
+ continue;
27599
+ if (key.startsWith("$"))
27600
+ continue;
27601
+ if (value === null)
27602
+ continue;
27603
+ if (typeof value === "object")
27604
+ continue;
27605
+ return true;
27606
+ }
27607
+ return false;
27608
+ }
27609
+ function hasBroadFilter(where) {
27610
+ for (const [key, value] of Object.entries(where)) {
27611
+ if (MONGO_BROAD_FILTER_OPERATORS.has(key))
27612
+ return true;
27613
+ if (value && typeof value === "object" && !Array.isArray(value)) {
27614
+ for (const inner of Object.keys(value)) {
27615
+ if (MONGO_BROAD_FILTER_OPERATORS.has(inner))
27616
+ return true;
27617
+ }
27618
+ }
27619
+ }
27620
+ return false;
27621
+ }
27622
+ function applyPermission(operation, context, factors) {
27623
+ const permResult = checkPermission(permissionSql(operation), context.permission);
27624
+ if (!permResult.allowed) {
27625
+ pushFactor2(factors, "permission_denied", "block", permResult.reason);
27626
+ }
27627
+ }
27628
+ function applyTableBlacklist(target, context, factors) {
27629
+ const blacklisted = (context.blacklist.tables ?? []).map(foldFieldPath);
27630
+ if (blacklisted.includes(foldFieldPath(target))) {
27631
+ pushFactor2(factors, "table_blacklisted", "block", `Target collection ${target} is blacklisted.`);
27632
+ }
27633
+ }
27634
+ function applyColumnBlacklist(target, fields, context, factors) {
27635
+ const columns = context.blacklist.columns ?? {};
27636
+ const lower = foldFieldPath(target);
27637
+ let raw = [];
27638
+ for (const [t2, cols] of Object.entries(columns)) {
27639
+ if (foldFieldPath(t2) === lower) {
27640
+ raw = cols;
27641
+ break;
27642
+ }
27643
+ }
27644
+ if (raw.length === 0)
27645
+ return;
27646
+ const { patterns, rejected } = compilePatterns(raw);
27647
+ if (rejected.length > 0) {
27648
+ const detail = rejected.map((r) => `'${r.raw}' (${r.reason})`).join(", ");
27649
+ pushFactor2(factors, "blacklist_unreadable", "block", `blacklist.columns for '${target}' has entries this matcher cannot read: ${detail}`);
27650
+ return;
27651
+ }
27652
+ if (patterns.length === 0)
27653
+ return;
27654
+ for (const field of fields) {
27655
+ if (matchAny(field, patterns)) {
27656
+ pushFactor2(factors, "blacklisted_column", "block", `MongoDB write would touch blacklisted path ${target}.${field}.`);
27657
+ }
27658
+ }
27659
+ }
27660
+ function applySchemaCoverage(target, context, factors) {
27661
+ const schemaTables = Object.keys(context.schema);
27662
+ if (schemaTables.length === 0) {
27663
+ pushFactor2(factors, "schema_cache_missing", "warn", "Schema cache is missing for the selected connection.");
27664
+ return;
27665
+ }
27666
+ const known = schemaTables.some((t2) => t2.toLowerCase() === target.toLowerCase());
27667
+ if (!known) {
27668
+ pushFactor2(factors, "schema_table_unknown", "warn", `Target collection ${target} is missing from schema cache.`);
27669
+ }
27670
+ }
27671
+ function buildRecommendations2(factors) {
27672
+ const out = new Set;
27673
+ const codes = new Set(factors.map((f) => f.code));
27674
+ if (codes.has("nonsql_filter_empty")) {
27675
+ out.add("Add an _id equality filter before executing this MongoDB write.");
27676
+ }
27677
+ if (codes.has("nonsql_missing_id")) {
27678
+ out.add("Prefer an _id equality filter before executing this MongoDB write.");
27679
+ }
27680
+ if (codes.has("nonsql_filter_broad")) {
27681
+ out.add("Narrow the filter to an _id equality or a tightly bounded condition.");
27682
+ }
27683
+ if (codes.has("nonsql_unsupported_operator")) {
27684
+ out.add("Restrict the update document to $set / $unset for planner-safe writes.");
27685
+ }
27686
+ if (codes.has("permission_denied")) {
27687
+ out.add("Switch to a connection with sufficient permission only if the operation is intended.");
27688
+ }
27689
+ if (codes.has("table_blacklisted") || codes.has("blacklisted_column")) {
27690
+ out.add("Review blacklist rules before accessing sensitive data.");
27691
+ }
27692
+ if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
27693
+ out.add("Refresh schema cache for the target collection before executing.");
27694
+ }
27695
+ out.add("Use --dry-run on the actual write command.");
27696
+ return Array.from(out);
27697
+ }
27698
+ function buildSuggestedCommands2(target, factors) {
27699
+ const codes = new Set(factors.map((f) => f.code));
27700
+ if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
27701
+ return [`dbcli schema ${target} --format json`];
27702
+ }
27703
+ return [];
27704
+ }
27705
+ function flattenInsertPaths(data, prefix = "") {
27706
+ const out = [];
27707
+ for (const [k, v] of Object.entries(data)) {
27708
+ const path2 = prefix === "" ? k : `${prefix}.${k}`;
27709
+ out.push(path2);
27710
+ if (v && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date)) {
27711
+ const ctor = v.constructor?.name;
27712
+ if (ctor === "Object") {
27713
+ out.push(...flattenInsertPaths(v, path2));
27714
+ }
27715
+ }
27716
+ }
27717
+ return out;
27718
+ }
27719
+ var MONGO_OPERATOR_TIER, MONGO_BROAD_FILTER_OPERATORS, analyzeMongoDmlRisk = (intent, context) => {
27720
+ const factors = [];
27721
+ applyPermission(intent.operation, context, factors);
27722
+ applyTableBlacklist(intent.target, context, factors);
27723
+ if (intent.operation === "insert") {
27724
+ applyColumnBlacklist(intent.target, flattenInsertPaths(intent.data), context, factors);
27725
+ } else if (intent.operation === "update") {
27726
+ const cls = classifyMongoUpdate(intent.set);
27727
+ for (const f of cls.tierFactors)
27728
+ pushFactor2(factors, f.code, f.severity, f.message);
27729
+ applyColumnBlacklist(intent.target, cls.fields, context, factors);
27730
+ const where = intent.where ?? {};
27731
+ if (Object.keys(where).length === 0) {
27732
+ pushFactor2(factors, "nonsql_filter_empty", "block", "MongoDB update filter is empty and would match every document.");
27733
+ } else if (!hasIdEquality(where)) {
27734
+ if (hasBroadFilter(where)) {
27735
+ pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB update filter uses a broad operator and may match multiple documents.");
27736
+ } else if (hasNonIdEquality(where)) {
27737
+ pushFactor2(factors, "nonsql_missing_id", "warn", "MongoDB update filter does not use _id and may match multiple documents.");
27738
+ } else {
27739
+ pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB update filter is not an _id equality and may match multiple documents.");
27740
+ }
27741
+ }
27742
+ } else {
27743
+ const where = intent.where ?? {};
27744
+ if (Object.keys(where).length === 0) {
27745
+ pushFactor2(factors, "nonsql_filter_empty", "block", "MongoDB delete filter is empty and would match every document.");
27746
+ } else if (!hasIdEquality(where)) {
27747
+ if (hasBroadFilter(where)) {
27748
+ pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB delete filter uses a broad operator and may match multiple documents.");
27749
+ } else if (hasNonIdEquality(where)) {
27750
+ pushFactor2(factors, "nonsql_missing_id", "warn", "MongoDB delete filter does not use _id and may match multiple documents.");
27751
+ } else {
27752
+ pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB delete filter is not an _id equality and may match multiple documents.");
27753
+ }
27754
+ }
27755
+ }
27756
+ applySchemaCoverage(intent.target, context, factors);
27757
+ return {
27758
+ decision: decide2(factors),
27759
+ operation: broadOperation(intent.operation),
27760
+ targetTables: [intent.target],
27761
+ riskFactors: factors,
27762
+ recommendations: buildRecommendations2(factors),
27763
+ suggestedCommands: buildSuggestedCommands2(intent.target, factors)
27764
+ };
27765
+ };
27766
+ var init_dml_plan = __esm(() => {
27767
+ init_blacklist_fold();
27768
+ init_path_matcher();
27769
+ init_permission_guard();
27770
+ MONGO_OPERATOR_TIER = {
27771
+ $set: "SAFE",
27772
+ $unset: "SAFE",
27773
+ $rename: "RENAME",
27774
+ $inc: "ARITHMETIC",
27775
+ $mul: "ARITHMETIC",
27776
+ $min: "ARITHMETIC",
27777
+ $max: "ARITHMETIC",
27778
+ $currentDate: "ARITHMETIC",
27779
+ $push: "ARRAY",
27780
+ $pull: "ARRAY",
27781
+ $pullAll: "ARRAY",
27782
+ $pop: "ARRAY",
27783
+ $addToSet: "ARRAY",
27784
+ $bit: "BITWISE",
27785
+ $where: "BLOCK"
27786
+ };
27787
+ MONGO_BROAD_FILTER_OPERATORS = new Set([
27788
+ "$regex",
27789
+ "$in",
27790
+ "$nin",
27791
+ "$gt",
27792
+ "$gte",
27793
+ "$lt",
27794
+ "$lte",
27795
+ "$ne",
27796
+ "$exists",
27797
+ "$or",
27798
+ "$and",
27799
+ "$nor",
27800
+ "$not",
27801
+ "$expr",
27802
+ "$text"
27803
+ ]);
27804
+ });
27805
+
26795
27806
  // src/core/dml-plan-sql.ts
26796
27807
  function assertIdentifier(value, role) {
26797
27808
  if (!value || !IDENTIFIER_RE.test(value)) {
@@ -26851,359 +27862,6 @@ var init_dml_plan_sql = __esm(() => {
26851
27862
  IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
26852
27863
  });
26853
27864
 
26854
- // src/utils/where-parser.ts
26855
- function parseWhereClause(whereClause) {
26856
- if (!whereClause || whereClause.trim() === "") {
26857
- throw new Error("WHERE clause cannot be empty");
26858
- }
26859
- const conditions = {};
26860
- const andParts = whereClause.split(/\s+AND\s+/i);
26861
- for (const part of andParts) {
26862
- const match = part.match(/^(\w+)\s*=\s*(.+)$/);
26863
- if (!match) {
26864
- throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
26865
- }
26866
- const column = match[1];
26867
- const valueStr = match[2];
26868
- if (valueStr === undefined || column === undefined) {
26869
- throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
26870
- }
26871
- const trimmed = valueStr.trim();
26872
- const isFullyQuoted = trimmed.length >= 2 && (trimmed.startsWith("'") && trimmed.endsWith("'") || trimmed.startsWith('"') && trimmed.endsWith('"'));
26873
- if (!isFullyQuoted && /\s+AND\s*$/i.test(trimmed)) {
26874
- throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
26875
- }
26876
- const stripped = isFullyQuoted ? trimmed.slice(1, -1) : trimmed;
26877
- let value = stripped;
26878
- if (stripped !== "" && !isNaN(Number(stripped))) {
26879
- value = Number(stripped);
26880
- }
26881
- if (stripped === "true")
26882
- value = true;
26883
- else if (stripped === "false")
26884
- value = false;
26885
- else if (stripped === "null")
26886
- value = null;
26887
- conditions[column] = value;
26888
- }
26889
- return conditions;
26890
- }
26891
-
26892
- // src/core/mongo/dml-plan.ts
26893
- function permissionSql(operation) {
26894
- if (operation === "insert")
26895
- return "INSERT INTO dummy";
26896
- if (operation === "update")
26897
- return "UPDATE dummy";
26898
- return "DELETE FROM dummy";
26899
- }
26900
- function broadOperation(operation) {
26901
- if (operation === "insert")
26902
- return "INSERT";
26903
- if (operation === "update")
26904
- return "UPDATE";
26905
- return "DELETE";
26906
- }
26907
- function pushFactor2(factors, code, severity, message) {
26908
- if (factors.some((f) => f.code === code && f.message === message))
26909
- return;
26910
- factors.push({ code, severity, message });
26911
- }
26912
- function decide2(factors) {
26913
- if (factors.some((f) => f.severity === "block"))
26914
- return "BLOCK";
26915
- if (factors.some((f) => f.severity === "warn"))
26916
- return "WARN";
26917
- return "ALLOW";
26918
- }
26919
- function classifyMongoUpdate(setDoc) {
26920
- const fields = new Set;
26921
- const tierFactors = [];
26922
- let hasBlock = false;
26923
- const hasAnyOperator = Object.keys(setDoc).some((k) => k.startsWith("$"));
26924
- if (!hasAnyOperator) {
26925
- for (const k of Object.keys(setDoc))
26926
- fields.add(k);
26927
- return { fields: Array.from(fields), tierFactors, hasBlock };
26928
- }
26929
- const seenTiers = new Map;
26930
- for (const [op, payload] of Object.entries(setDoc)) {
26931
- if (!op.startsWith("$")) {
26932
- fields.add(op);
26933
- continue;
26934
- }
26935
- if (payload && typeof payload === "object" && !Array.isArray(payload)) {
26936
- for (const k of Object.keys(payload))
26937
- fields.add(k);
26938
- }
26939
- const tier = MONGO_OPERATOR_TIER[op];
26940
- if (tier === undefined) {
26941
- hasBlock = true;
26942
- tierFactors.push({
26943
- code: "mongo_unknown_operator",
26944
- severity: "block",
26945
- message: `Update uses unknown operator '${op}'. Reject by default; add to tier table if intentional.`
26946
- });
26947
- continue;
26948
- }
26949
- if (tier === "BLOCK") {
26950
- hasBlock = true;
26951
- tierFactors.push({
26952
- code: "mongo_unknown_operator",
26953
- severity: "block",
26954
- message: `Update uses '${op}' which executes server-side code. Operation rejected.`
26955
- });
26956
- continue;
26957
- }
26958
- if (tier === "SAFE")
26959
- continue;
26960
- const bucket = seenTiers.get(tier) ?? [];
26961
- bucket.push(op);
26962
- seenTiers.set(tier, bucket);
26963
- }
26964
- for (const [tier, ops] of seenTiers) {
26965
- if (tier === "RENAME") {
26966
- tierFactors.push({
26967
- code: "mongo_rename_operator",
26968
- severity: "warn",
26969
- message: `Update uses ${ops.join(", ")}; field rename does not exfiltrate data but can break readers.`
26970
- });
26971
- } else if (tier === "ARITHMETIC") {
26972
- tierFactors.push({
26973
- code: "mongo_arithmetic_operator",
26974
- severity: "warn",
26975
- message: `Update uses ${ops.join(", ")}; numeric mutation may compound silently.`
26976
- });
26977
- } else if (tier === "ARRAY") {
26978
- tierFactors.push({
26979
- code: "mongo_array_operator",
26980
- severity: "warn",
26981
- message: `Update uses ${ops.join(", ")}; array mutation can grow unboundedly without a size guard.`
26982
- });
26983
- } else if (tier === "BITWISE") {
26984
- tierFactors.push({
26985
- code: "mongo_bitwise_operator",
26986
- severity: "warn",
26987
- message: `Update uses ${ops.join(", ")}; bitwise updates skip type promotion checks.`
26988
- });
26989
- }
26990
- }
26991
- return { fields: Array.from(fields), tierFactors, hasBlock };
26992
- }
26993
- function hasIdEquality(where) {
26994
- if (!("_id" in where))
26995
- return false;
26996
- const value = where._id;
26997
- if (value === null || value === undefined)
26998
- return false;
26999
- if (typeof value === "object")
27000
- return false;
27001
- return true;
27002
- }
27003
- function hasNonIdEquality(where) {
27004
- for (const [key, value] of Object.entries(where)) {
27005
- if (key === "_id")
27006
- continue;
27007
- if (key.startsWith("$"))
27008
- continue;
27009
- if (value === null)
27010
- continue;
27011
- if (typeof value === "object")
27012
- continue;
27013
- return true;
27014
- }
27015
- return false;
27016
- }
27017
- function hasBroadFilter(where) {
27018
- for (const [key, value] of Object.entries(where)) {
27019
- if (MONGO_BROAD_FILTER_OPERATORS.has(key))
27020
- return true;
27021
- if (value && typeof value === "object" && !Array.isArray(value)) {
27022
- for (const inner of Object.keys(value)) {
27023
- if (MONGO_BROAD_FILTER_OPERATORS.has(inner))
27024
- return true;
27025
- }
27026
- }
27027
- }
27028
- return false;
27029
- }
27030
- function applyPermission(operation, context, factors) {
27031
- const permResult = checkPermission(permissionSql(operation), context.permission);
27032
- if (!permResult.allowed) {
27033
- pushFactor2(factors, "permission_denied", "block", permResult.reason);
27034
- }
27035
- }
27036
- function applyTableBlacklist(target, context, factors) {
27037
- const blacklisted = (context.blacklist.tables ?? []).map((t2) => t2.toLowerCase());
27038
- if (blacklisted.includes(target.toLowerCase())) {
27039
- pushFactor2(factors, "table_blacklisted", "block", `Target collection ${target} is blacklisted.`);
27040
- }
27041
- }
27042
- function applyColumnBlacklist(target, fields, context, factors) {
27043
- const columns = context.blacklist.columns ?? {};
27044
- const lower = target.toLowerCase();
27045
- let raw = [];
27046
- for (const [t2, cols] of Object.entries(columns)) {
27047
- if (t2.toLowerCase() === lower) {
27048
- raw = cols;
27049
- break;
27050
- }
27051
- }
27052
- if (raw.length === 0)
27053
- return;
27054
- const { patterns } = compilePatterns(raw);
27055
- if (patterns.length === 0)
27056
- return;
27057
- for (const field of fields) {
27058
- if (matchAny(field, patterns)) {
27059
- pushFactor2(factors, "blacklisted_column", "block", `MongoDB write would touch blacklisted path ${target}.${field}.`);
27060
- }
27061
- }
27062
- }
27063
- function applySchemaCoverage(target, context, factors) {
27064
- const schemaTables = Object.keys(context.schema);
27065
- if (schemaTables.length === 0) {
27066
- pushFactor2(factors, "schema_cache_missing", "warn", "Schema cache is missing for the selected connection.");
27067
- return;
27068
- }
27069
- const known = schemaTables.some((t2) => t2.toLowerCase() === target.toLowerCase());
27070
- if (!known) {
27071
- pushFactor2(factors, "schema_table_unknown", "warn", `Target collection ${target} is missing from schema cache.`);
27072
- }
27073
- }
27074
- function buildRecommendations2(factors) {
27075
- const out = new Set;
27076
- const codes = new Set(factors.map((f) => f.code));
27077
- if (codes.has("nonsql_filter_empty")) {
27078
- out.add("Add an _id equality filter before executing this MongoDB write.");
27079
- }
27080
- if (codes.has("nonsql_missing_id")) {
27081
- out.add("Prefer an _id equality filter before executing this MongoDB write.");
27082
- }
27083
- if (codes.has("nonsql_filter_broad")) {
27084
- out.add("Narrow the filter to an _id equality or a tightly bounded condition.");
27085
- }
27086
- if (codes.has("nonsql_unsupported_operator")) {
27087
- out.add("Restrict the update document to $set / $unset for planner-safe writes.");
27088
- }
27089
- if (codes.has("permission_denied")) {
27090
- out.add("Switch to a connection with sufficient permission only if the operation is intended.");
27091
- }
27092
- if (codes.has("table_blacklisted") || codes.has("blacklisted_column")) {
27093
- out.add("Review blacklist rules before accessing sensitive data.");
27094
- }
27095
- if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
27096
- out.add("Refresh schema cache for the target collection before executing.");
27097
- }
27098
- out.add("Use --dry-run on the actual write command.");
27099
- return Array.from(out);
27100
- }
27101
- function buildSuggestedCommands2(target, factors) {
27102
- const codes = new Set(factors.map((f) => f.code));
27103
- if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
27104
- return [`dbcli schema ${target} --format json`];
27105
- }
27106
- return [];
27107
- }
27108
- function flattenInsertPaths(data, prefix = "") {
27109
- const out = [];
27110
- for (const [k, v] of Object.entries(data)) {
27111
- const path2 = prefix === "" ? k : `${prefix}.${k}`;
27112
- out.push(path2);
27113
- if (v && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date)) {
27114
- const ctor = v.constructor?.name;
27115
- if (ctor === "Object") {
27116
- out.push(...flattenInsertPaths(v, path2));
27117
- }
27118
- }
27119
- }
27120
- return out;
27121
- }
27122
- var MONGO_OPERATOR_TIER, MONGO_BROAD_FILTER_OPERATORS, analyzeMongoDmlRisk = (intent, context) => {
27123
- const factors = [];
27124
- applyPermission(intent.operation, context, factors);
27125
- applyTableBlacklist(intent.target, context, factors);
27126
- if (intent.operation === "insert") {
27127
- applyColumnBlacklist(intent.target, flattenInsertPaths(intent.data), context, factors);
27128
- } else if (intent.operation === "update") {
27129
- const cls = classifyMongoUpdate(intent.set);
27130
- for (const f of cls.tierFactors)
27131
- pushFactor2(factors, f.code, f.severity, f.message);
27132
- applyColumnBlacklist(intent.target, cls.fields, context, factors);
27133
- const where = intent.where ?? {};
27134
- if (Object.keys(where).length === 0) {
27135
- pushFactor2(factors, "nonsql_filter_empty", "block", "MongoDB update filter is empty and would match every document.");
27136
- } else if (!hasIdEquality(where)) {
27137
- if (hasBroadFilter(where)) {
27138
- pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB update filter uses a broad operator and may match multiple documents.");
27139
- } else if (hasNonIdEquality(where)) {
27140
- pushFactor2(factors, "nonsql_missing_id", "warn", "MongoDB update filter does not use _id and may match multiple documents.");
27141
- } else {
27142
- pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB update filter is not an _id equality and may match multiple documents.");
27143
- }
27144
- }
27145
- } else {
27146
- const where = intent.where ?? {};
27147
- if (Object.keys(where).length === 0) {
27148
- pushFactor2(factors, "nonsql_filter_empty", "block", "MongoDB delete filter is empty and would match every document.");
27149
- } else if (!hasIdEquality(where)) {
27150
- if (hasBroadFilter(where)) {
27151
- pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB delete filter uses a broad operator and may match multiple documents.");
27152
- } else if (hasNonIdEquality(where)) {
27153
- pushFactor2(factors, "nonsql_missing_id", "warn", "MongoDB delete filter does not use _id and may match multiple documents.");
27154
- } else {
27155
- pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB delete filter is not an _id equality and may match multiple documents.");
27156
- }
27157
- }
27158
- }
27159
- applySchemaCoverage(intent.target, context, factors);
27160
- return {
27161
- decision: decide2(factors),
27162
- operation: broadOperation(intent.operation),
27163
- targetTables: [intent.target],
27164
- riskFactors: factors,
27165
- recommendations: buildRecommendations2(factors),
27166
- suggestedCommands: buildSuggestedCommands2(intent.target, factors)
27167
- };
27168
- };
27169
- var init_dml_plan = __esm(() => {
27170
- init_permission_guard();
27171
- MONGO_OPERATOR_TIER = {
27172
- $set: "SAFE",
27173
- $unset: "SAFE",
27174
- $rename: "RENAME",
27175
- $inc: "ARITHMETIC",
27176
- $mul: "ARITHMETIC",
27177
- $min: "ARITHMETIC",
27178
- $max: "ARITHMETIC",
27179
- $currentDate: "ARITHMETIC",
27180
- $push: "ARRAY",
27181
- $pull: "ARRAY",
27182
- $pullAll: "ARRAY",
27183
- $pop: "ARRAY",
27184
- $addToSet: "ARRAY",
27185
- $bit: "BITWISE",
27186
- $where: "BLOCK"
27187
- };
27188
- MONGO_BROAD_FILTER_OPERATORS = new Set([
27189
- "$regex",
27190
- "$in",
27191
- "$nin",
27192
- "$gt",
27193
- "$gte",
27194
- "$lt",
27195
- "$lte",
27196
- "$ne",
27197
- "$exists",
27198
- "$or",
27199
- "$and",
27200
- "$nor",
27201
- "$not",
27202
- "$expr",
27203
- "$text"
27204
- ]);
27205
- });
27206
-
27207
27865
  // src/core/redis/dml-plan.ts
27208
27866
  function permissionSql2(operation) {
27209
27867
  if (operation === "insert")
@@ -27245,17 +27903,17 @@ function applyPermission2(operation, context, factors) {
27245
27903
  }
27246
27904
  }
27247
27905
  function applyTableBlacklist2(target, context, factors) {
27248
- const blacklisted = (context.blacklist.tables ?? []).map((t2) => t2.toLowerCase());
27249
- if (blacklisted.includes(target.toLowerCase())) {
27906
+ const blacklisted = (context.blacklist.tables ?? []).map(foldFieldPath);
27907
+ if (blacklisted.includes(foldFieldPath(target))) {
27250
27908
  pushFactor3(factors, "table_blacklisted", "block", `Redis key ${target} is blacklisted.`);
27251
27909
  }
27252
27910
  }
27253
27911
  function applyColumnBlacklist2(target, fields, context, factors) {
27254
27912
  const columns = context.blacklist.columns ?? {};
27255
- const lower = target.toLowerCase();
27913
+ const lower = foldFieldPath(target);
27256
27914
  let blacklistedFields = [];
27257
27915
  for (const [t2, cols] of Object.entries(columns)) {
27258
- if (t2.toLowerCase() === lower) {
27916
+ if (foldFieldPath(t2) === lower) {
27259
27917
  blacklistedFields = cols;
27260
27918
  break;
27261
27919
  }
@@ -27353,6 +28011,7 @@ var analyzeRedisDmlRisk = (intent, context) => {
27353
28011
  };
27354
28012
  };
27355
28013
  var init_dml_plan2 = __esm(() => {
28014
+ init_blacklist_fold();
27356
28015
  init_permission_guard();
27357
28016
  });
27358
28017
 
@@ -27402,17 +28061,17 @@ function applyPermission3(operation, context, factors) {
27402
28061
  }
27403
28062
  }
27404
28063
  function applyTableBlacklist3(target, context, factors) {
27405
- const blacklisted = (context.blacklist.tables ?? []).map((t2) => t2.toLowerCase());
27406
- if (blacklisted.includes(target.toLowerCase())) {
28064
+ const blacklisted = (context.blacklist.tables ?? []).map(foldFieldPath);
28065
+ if (blacklisted.includes(foldFieldPath(target))) {
27407
28066
  pushFactor4(factors, "table_blacklisted", "block", `Elasticsearch index ${target} is blacklisted.`);
27408
28067
  }
27409
28068
  }
27410
28069
  function applyColumnBlacklist3(target, fields, context, factors) {
27411
28070
  const columns = context.blacklist.columns ?? {};
27412
- const lower = target.toLowerCase();
28071
+ const lower = foldFieldPath(target);
27413
28072
  let blacklistedFields = [];
27414
28073
  for (const [t2, cols] of Object.entries(columns)) {
27415
- if (t2.toLowerCase() === lower) {
28074
+ if (foldFieldPath(t2) === lower) {
27416
28075
  blacklistedFields = cols;
27417
28076
  break;
27418
28077
  }
@@ -27495,6 +28154,7 @@ var analyzeElasticsearchDmlRisk = (intent, context) => {
27495
28154
  };
27496
28155
  };
27497
28156
  var init_dml_plan3 = __esm(() => {
28157
+ init_blacklist_fold();
27498
28158
  init_permission_guard();
27499
28159
  });
27500
28160
 
@@ -27665,7 +28325,7 @@ async function insertCommand(table, options, command) {
27665
28325
  const blacklistManager = new BlacklistManager(config);
27666
28326
  const blacklistValidator = new BlacklistValidator(blacklistManager);
27667
28327
  blacklistValidator.checkTableBlacklist("INSERT", table, []);
27668
- blacklistValidator.checkColumnBlacklistOnWrite(table, Object.keys(data), "INSERT");
28328
+ blacklistValidator.checkColumnBlacklistOnWrite(table, flattenInsertPaths(data), "INSERT");
27669
28329
  const preview = `SET ${table} ... (Redis Insert)`;
27670
28330
  if (options.dryRun) {
27671
28331
  const output = {
@@ -27730,7 +28390,7 @@ async function insertCommand(table, options, command) {
27730
28390
  const blacklistManager = new BlacklistManager(config);
27731
28391
  const blacklistValidator = new BlacklistValidator(blacklistManager);
27732
28392
  blacklistValidator.checkTableBlacklist("INSERT", table, []);
27733
- blacklistValidator.checkColumnBlacklistOnWrite(table, Object.keys(data), "INSERT");
28393
+ blacklistValidator.checkColumnBlacklistOnWrite(table, flattenInsertPaths(data), "INSERT");
27734
28394
  const preview = previewInsert(table, data);
27735
28395
  if (options.dryRun) {
27736
28396
  const output = {
@@ -27848,9 +28508,11 @@ var init_insert = __esm(() => {
27848
28508
  init_mutation_outcome();
27849
28509
  init_config();
27850
28510
  init_permission_guard();
28511
+ init_blacklist_manager();
27851
28512
  init_blacklist_validator();
27852
28513
  init_blacklist();
27853
28514
  init_config_path();
28515
+ init_dml_plan();
27854
28516
  init_dml_plan4();
27855
28517
  init_integration_helper();
27856
28518
  });
@@ -28008,7 +28670,11 @@ async function updateCommand(table, options, command) {
28008
28670
  if (hasOperator) {
28009
28671
  const operators = updateDoc;
28010
28672
  for (const [op, payload] of Object.entries(operators)) {
28011
- if ((op === "$set" || op === "$unset") && payload && typeof payload === "object" && !Array.isArray(payload)) {
28673
+ if (!op.startsWith("$")) {
28674
+ writtenFields.add(op);
28675
+ continue;
28676
+ }
28677
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) {
28012
28678
  for (const k of Object.keys(payload))
28013
28679
  writtenFields.add(k);
28014
28680
  }
@@ -28169,6 +28835,7 @@ var init_update = __esm(() => {
28169
28835
  init_mutation_outcome();
28170
28836
  init_config();
28171
28837
  init_permission_guard();
28838
+ init_blacklist_manager();
28172
28839
  init_blacklist_validator();
28173
28840
  init_blacklist();
28174
28841
  init_config_path();
@@ -28468,6 +29135,7 @@ var init_delete = __esm(() => {
28468
29135
  init_mutation_outcome();
28469
29136
  init_config();
28470
29137
  init_permission_guard();
29138
+ init_blacklist_manager();
28471
29139
  init_blacklist_validator();
28472
29140
  init_blacklist();
28473
29141
  init_config_path();
@@ -28911,6 +29579,7 @@ var init_export = __esm(() => {
28911
29579
  init_config();
28912
29580
  init_prompts();
28913
29581
  init_config_path();
29582
+ init_blacklist_manager();
28914
29583
  init_blacklist_validator();
28915
29584
  init_applied_limit();
28916
29585
  init_integration_helper();
@@ -30147,6 +30816,7 @@ async function gatherContext(workspaceRoot, configPath, options = {}) {
30147
30816
  }
30148
30817
  var init_context = __esm(() => {
30149
30818
  init_config();
30819
+ init_blacklist_manager();
30150
30820
  init_loader();
30151
30821
  init_snippet_paths();
30152
30822
  init_semantic();
@@ -31363,11 +32033,8 @@ function auditBlacklistPatterns(cfg) {
31363
32033
  }
31364
32034
  return { warnings };
31365
32035
  }
31366
- function isValidTableNameForSystem(name, system) {
31367
- if (!isValidTableName(name))
31368
- return false;
31369
- const literalMatching = system === "postgresql" || system === "mysql" || system === "mariadb" || system === "mongodb";
31370
- return !(literalMatching && GLOB_CHARS.test(name));
32036
+ function isValidTableNameForSystem(name, _system) {
32037
+ return isValidTableName(name);
31371
32038
  }
31372
32039
  function isValidTableName(name) {
31373
32040
  return VALID_TABLE_NAME.test(name);
@@ -31533,16 +32200,16 @@ function resolveBlacklistConfigPath(options, command) {
31533
32200
  }
31534
32201
  return DEFAULT_CONFIG_PATH;
31535
32202
  }
31536
- var DEFAULT_CONFIG_PATH = ".dbcli", VALID_TABLE_NAME, VALID_COLUMN_NAME, GLOB_CHARS, blacklistCommand, tableCmd, columnCmd;
32203
+ var DEFAULT_CONFIG_PATH = ".dbcli", VALID_TABLE_NAME, VALID_COLUMN_NAME, blacklistCommand, tableCmd, columnCmd;
31537
32204
  var init_blacklist2 = __esm(() => {
31538
32205
  init_config_v2();
31539
32206
  init_esm();
31540
32207
  init_message_loader();
31541
32208
  init_config();
32209
+ init_path_matcher();
31542
32210
  init_validation();
31543
- VALID_TABLE_NAME = /^[a-zA-Z0-9_.*?:@[\]-]+$/;
31544
- VALID_COLUMN_NAME = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
31545
- GLOB_CHARS = /[*?[\]]/;
32211
+ VALID_TABLE_NAME = /^[a-zA-Z0-9_.*?:@[\]\\-]+$/;
32212
+ VALID_COLUMN_NAME = /^[a-zA-Z0-9_*?[\]\\-]+$/;
31546
32213
  blacklistCommand = new Command("blacklist").description(t("blacklist.description"));
31547
32214
  blacklistCommand.command("list").description(t("blacklist.list_title")).option("--config <path>", "Path to .dbcli config file").option("--format <type>", "Output format: text, json", "text").action(async (options, command) => {
31548
32215
  try {
@@ -31613,11 +32280,10 @@ async function checkAction(table, options) {
31613
32280
  const checker = new HealthChecker(adapter);
31614
32281
  const blacklistManager = new BlacklistManager(config);
31615
32282
  const blacklistedColumns = getBlacklistedColumnSet(blacklistManager);
31616
- const blacklistedTables = getBlacklistedTableSet(blacklistManager);
31617
32283
  const checkTypes = options.checks ? options.checks.split(",") : undefined;
31618
32284
  const sampleSize = parseInt(options.sample, 10) || 1e4;
31619
32285
  if (table) {
31620
- if (blacklistedTables.has(table.toLowerCase())) {
32286
+ if (blacklistManager.isTableBlacklisted(table)) {
31621
32287
  throw new Error(`Table "${table}" is blacklisted`);
31622
32288
  }
31623
32289
  const schema = await adapter.getTableSchema(table);
@@ -31632,7 +32298,7 @@ async function checkAction(table, options) {
31632
32298
  const reports = [];
31633
32299
  const skipped = [];
31634
32300
  for (const t2 of tables) {
31635
- if (blacklistedTables.has(t2.name.toLowerCase())) {
32301
+ if (blacklistManager.isTableBlacklisted(t2.name)) {
31636
32302
  skipped.push(`${t2.name} (blacklisted)`);
31637
32303
  continue;
31638
32304
  }
@@ -31708,16 +32374,13 @@ function getBlacklistedColumnSet(manager) {
31708
32374
  }
31709
32375
  return result;
31710
32376
  }
31711
- function getBlacklistedTableSet(manager) {
31712
- const state = manager.state;
31713
- return state?.tables || new Set;
31714
- }
31715
32377
  var ALLOWED_FORMATS6, checkCommand;
31716
32378
  var init_check = __esm(() => {
31717
32379
  init_esm();
31718
32380
  init_adapters();
31719
32381
  init_config();
31720
32382
  init_health_checker();
32383
+ init_blacklist_manager();
31721
32384
  init_size_category();
31722
32385
  init_validation();
31723
32386
  init_connection_selector();
@@ -33594,9 +34257,8 @@ var init_proposals = __esm(() => {
33594
34257
  });
33595
34258
 
33596
34259
  // src/core/orm-drift/compare.ts
33597
- function globToRegex2(glob) {
33598
- const escaped = glob.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
33599
- return new RegExp(`^${escaped}$`);
34260
+ function starOnlyGlob(glob) {
34261
+ return glob.replace(/[?[\]\\]/g, "\\$&");
33600
34262
  }
33601
34263
  function tableMap(schema, defaultSchema) {
33602
34264
  const tables = new Map;
@@ -33630,7 +34292,7 @@ function compareNormalized(orm, db, opts) {
33630
34292
  const tableKeys = new Set([...ormTables.keys(), ...dbTables.keys()]);
33631
34293
  const extraDefaultIgnore = opts.extraDefaultIgnore ?? [];
33632
34294
  const defaultIgnore = [...DEFAULT_IGNORE, ...extraDefaultIgnore];
33633
- const ignorePatterns = opts.ignore.map(globToRegex2);
34295
+ const ignorePatterns = opts.ignore;
33634
34296
  const targetLabel = opts.targetLabel ?? "database";
33635
34297
  const entries = [];
33636
34298
  for (const tableKey of tableKeys) {
@@ -33641,7 +34303,7 @@ function compareNormalized(orm, db, opts) {
33641
34303
  continue;
33642
34304
  const table = qualifiedTableName(normalizedTable.identity);
33643
34305
  const isDefaultIgnored = defaultIgnore.includes(normalizedTable.identity.table);
33644
- if (isDefaultIgnored || ignorePatterns.some((pattern) => pattern.test(table))) {
34306
+ if (isDefaultIgnored || ignorePatterns.some((pattern) => globMatches(starOnlyGlob(pattern), table))) {
33645
34307
  entries.push(entryWithProposals({
33646
34308
  category: "unmanaged",
33647
34309
  severity: "info",
@@ -33823,6 +34485,7 @@ function compareIndexes(ormIndexes, dbIndexes, tableIdentity, table, ormSource,
33823
34485
  var DEFAULT_IGNORE, entryOrder = (left, right) => codePointOrder(left.table, right.table) || codePointOrder(left.object, right.object) || codePointOrder(left.category, right.category) || codePointOrder(left.detail, right.detail);
33824
34486
  var init_compare = __esm(() => {
33825
34487
  init_normalized_schema();
34488
+ init_glob();
33826
34489
  init_proposals();
33827
34490
  DEFAULT_IGNORE = ["_prisma_migrations"];
33828
34491
  });
@@ -34719,7 +35382,7 @@ function shouldEmbedRecent(opts) {
34719
35382
  return opts.forAgent === true || opts.format === "json";
34720
35383
  }
34721
35384
  function briefifyForRecent(entry) {
34722
- const phase = entry.metadata?.es_shell_phase;
35385
+ const phase = entry.metadata?.shell_phase;
34723
35386
  return {
34724
35387
  id: entry.id,
34725
35388
  ts: entry.ts,
@@ -34813,7 +35476,10 @@ async function collectInspect(opts) {
34813
35476
  let version = null;
34814
35477
  if (!opts.noConnect && config?.connection && system) {
34815
35478
  const probeTimeout = opts.probeTimeoutMs ?? DEFAULT_PROBE_MS;
34816
- const adapter = AdapterFactory.createAdapter(config.connection);
35479
+ const adapter = AdapterFactory.createAdapter({
35480
+ ...config,
35481
+ blacklist: config.blacklist ?? { tables: [], columns: {} }
35482
+ });
34817
35483
  let probeError = null;
34818
35484
  const probe = (async () => {
34819
35485
  await adapter.connect();
@@ -35207,8 +35873,12 @@ async function runDiagnostic(input) {
35207
35873
  description: meta.description ?? ""
35208
35874
  };
35209
35875
  let prepared;
35876
+ let redisTokens = [];
35210
35877
  try {
35211
35878
  prepared = prepareExecution(input.snippet, { engine: input.engine, noLimit: false }, {}, {});
35879
+ if (engineFamily(input.engine) === "redis") {
35880
+ redisTokens = parseRedisCommand(prepared.rewrittenSql);
35881
+ }
35212
35882
  } catch (err) {
35213
35883
  const reason = err instanceof SavedQueryError ? err.message : `prepare failed: ${err.message}`;
35214
35884
  return { ...base, rowCount: 0, rows: [], status: "skipped", reason, durationMs: 0 };
@@ -35216,7 +35886,7 @@ async function runDiagnostic(input) {
35216
35886
  const family = engineFamily(input.engine);
35217
35887
  const referencedTables = family === "sql" ? extractTableReferences(prepared.rewrittenSql, {
35218
35888
  ...ENGINE_DIALECT2[input.engine] ? { dialect: ENGINE_DIALECT2[input.engine] } : {}
35219
- }) : family === "es" && prepared.execHints?.index ? [prepared.execHints.index] : [];
35889
+ }) : family === "es" && prepared.execHints?.index ? [prepared.execHints.index] : family === "redis" && redisTokens.length > 0 ? redisCommandTargets(redisTokens[0], redisTokens.slice(1)) : [];
35220
35890
  if (input.blacklistValidator && referencedTables.length > 0) {
35221
35891
  try {
35222
35892
  input.blacklistValidator.checkTablesBlacklist("SELECT", referencedTables);
@@ -35228,7 +35898,7 @@ async function runDiagnostic(input) {
35228
35898
  rowCount: 0,
35229
35899
  rows: [],
35230
35900
  status: "skipped",
35231
- reason: err.message,
35901
+ reason: family === "redis" ? "Redis diagnostic skipped by blacklist" : err.message,
35232
35902
  durationMs: 0
35233
35903
  };
35234
35904
  }
@@ -35244,8 +35914,8 @@ async function runDiagnostic(input) {
35244
35914
  try {
35245
35915
  outcome = await Promise.race([exec, timer]);
35246
35916
  } catch (err) {
35247
- outcome = "error";
35248
- errorMessage2 = err.message;
35917
+ outcome = err instanceof BlacklistRejection ? "skipped" : "error";
35918
+ errorMessage2 = err instanceof BlacklistRejection ? "Redis diagnostic skipped by blacklist" : err.message;
35249
35919
  }
35250
35920
  const durationMs = Math.round(performance.now() - start);
35251
35921
  if (outcome === "timeout") {
@@ -35258,12 +35928,12 @@ async function runDiagnostic(input) {
35258
35928
  durationMs
35259
35929
  };
35260
35930
  }
35261
- if (outcome === "error") {
35931
+ if (outcome === "error" || outcome === "skipped") {
35262
35932
  return {
35263
35933
  ...base,
35264
35934
  rowCount: 0,
35265
35935
  rows: [],
35266
- status: "error",
35936
+ status: outcome,
35267
35937
  reason: errorMessage2 ?? "unknown error",
35268
35938
  durationMs
35269
35939
  };
@@ -35283,6 +35953,9 @@ async function runDiagnostic(input) {
35283
35953
  }
35284
35954
  var ENGINE_DIALECT2;
35285
35955
  var init_run_diagnostic = __esm(() => {
35956
+ init_redis_adapter();
35957
+ init_blacklist_enforcer();
35958
+ init_types3();
35286
35959
  init_saved_queries();
35287
35960
  init_strategies();
35288
35961
  init_sql_tables();
@@ -35346,7 +36019,7 @@ async function collectReport(opts) {
35346
36019
  warnings.push({ severity: "warn", message: "connection config missing after probe" });
35347
36020
  return finalize({ context, sections: [], warnings, generatedAt });
35348
36021
  }
35349
- const adapter = AdapterFactory.createAdapter(config.connection);
36022
+ const adapter = AdapterFactory.createAdapter(config);
35350
36023
  const blacklistValidator = new BlacklistValidator(new BlacklistManager(config));
35351
36024
  const sectionEvidence = new Map;
35352
36025
  for (const id of sections)
@@ -35426,6 +36099,7 @@ var init_collector2 = __esm(() => {
35426
36099
  init_saved_queries();
35427
36100
  init_select_snippets();
35428
36101
  init_run_diagnostic();
36102
+ init_blacklist_manager();
35429
36103
  init_blacklist_validator();
35430
36104
  init_section_map();
35431
36105
  init_types7();
@@ -37431,7 +38105,7 @@ function parseTailN(raw) {
37431
38105
  return requested;
37432
38106
  }
37433
38107
  function briefify(entry) {
37434
- const phase = entry.metadata?.es_shell_phase;
38108
+ const phase = entry.metadata?.shell_phase;
37435
38109
  return {
37436
38110
  ts: entry.ts,
37437
38111
  command: entry.command,
@@ -37486,8 +38160,8 @@ function renderTailAllTable(envelopes) {
37486
38160
  }
37487
38161
  function briefifyShow(entry) {
37488
38162
  const { metadata, redacted_query, ...rest } = entry;
37489
- const phase = metadata?.es_shell_phase;
37490
- return phase === undefined ? rest : { ...rest, metadata: { es_shell_phase: phase } };
38163
+ const phase = metadata?.shell_phase;
38164
+ return phase === undefined ? rest : { ...rest, metadata: { shell_phase: phase } };
37491
38165
  }
37492
38166
  function renderEntryTable(entry) {
37493
38167
  const lines = [];
@@ -38234,6 +38908,7 @@ async function collectElasticsearchDoctorResults(config, runtimeOverrides = {})
38234
38908
  var ALLOWED_FORMATS15, SENSITIVE_PATTERNS, runDoctorChecks, defaultDoctorCollectorRuntime, doctorCommand;
38235
38909
  var init_doctor = __esm(() => {
38236
38910
  init_esm();
38911
+ init_blacklist_fold();
38237
38912
  init_colors();
38238
38913
  init_config();
38239
38914
  init_adapters();
@@ -38350,7 +39025,8 @@ var init_doctor = __esm(() => {
38350
39025
  for (const col of columns) {
38351
39026
  const colLower = col.toLowerCase();
38352
39027
  const isSensitive = SENSITIVE_PATTERNS.some((p) => colLower.includes(p));
38353
- if (isSensitive && !blacklisted.has(col)) {
39028
+ const foldedRules = new Set([...blacklisted].map(foldFieldPath));
39029
+ if (isSensitive && !foldedRules.has(foldFieldPath(col))) {
38354
39030
  unprotected.push(`${table}.${col}`);
38355
39031
  }
38356
39032
  }
@@ -42015,6 +42691,7 @@ var init_snapshot = __esm(() => {
42015
42691
  init_config();
42016
42692
  init_config_path();
42017
42693
  init_validation();
42694
+ init_blacklist_manager();
42018
42695
  init_blacklist_validator();
42019
42696
  init_query_executor();
42020
42697
  init_permission_guard();
@@ -42683,6 +43360,7 @@ var init_assert = __esm(() => {
42683
43360
  init_config();
42684
43361
  init_config_path();
42685
43362
  init_validation();
43363
+ init_blacklist_manager();
42686
43364
  init_blacklist_validator();
42687
43365
  init_query_executor();
42688
43366
  init_saved_queries();
@@ -44861,6 +45539,7 @@ var init_verify2 = __esm(() => {
44861
45539
  init_adapters();
44862
45540
  init_config();
44863
45541
  init_config_path();
45542
+ init_blacklist_manager();
44864
45543
  init_blacklist_validator();
44865
45544
  init_query_executor();
44866
45545
  init_query_risk_analyzer();
@@ -45612,15 +46291,17 @@ class ReplEngine {
45612
46291
  adapter;
45613
46292
  context;
45614
46293
  writeGate;
46294
+ auditSink;
45615
46295
  state;
45616
46296
  buffer;
45617
46297
  history;
45618
46298
  formatter;
45619
46299
  config;
45620
- constructor(adapter, context, historyPath, config = null, writeGate = null) {
46300
+ constructor(adapter, context, historyPath, config = null, writeGate = null, auditSink = null) {
45621
46301
  this.adapter = adapter;
45622
46302
  this.context = context;
45623
46303
  this.writeGate = writeGate;
46304
+ this.auditSink = auditSink;
45624
46305
  this.state = { format: "table", timing: false, connected: true, noLimit: false };
45625
46306
  this.buffer = new MultilineBuffer;
45626
46307
  this.history = new HistoryManager(historyPath);
@@ -45752,10 +46433,11 @@ class ReplEngine {
45752
46433
  } else {
45753
46434
  const permResult = checkPermission(sql, this.context.permission, SQL_DIALECTS.find((dialect) => dialect === this.context.system));
45754
46435
  if (!permResult.allowed) {
46436
+ await this.auditSink?.({ phase: "outcome", success: false, statement: sql });
45755
46437
  return {
45756
46438
  action: "continue",
45757
46439
  output: import_picocolors3.default.red(t_vars("shell.error_permission", {
45758
- required: permResult.classification.type === "UNKNOWN" ? "admin" : "read-write",
46440
+ required: permResult.requiredPermission ?? "admin",
45759
46441
  current: this.context.permission
45760
46442
  }))
45761
46443
  };
@@ -45771,18 +46453,22 @@ class ReplEngine {
45771
46453
  } catch (error) {
45772
46454
  if (!(error instanceof BlacklistError))
45773
46455
  throw error;
46456
+ await this.auditSink?.({ phase: "outcome", success: false, statement: sql });
45774
46457
  return {
45775
46458
  action: "continue",
45776
46459
  output: import_picocolors3.default.red(t_vars("shell.error_blacklisted", { table: error.message }))
45777
46460
  };
45778
46461
  }
45779
46462
  }
45780
- if (this.writeGate && !await this.writeGate(sql))
46463
+ if (this.writeGate && !await this.writeGate(sql)) {
46464
+ await this.auditSink?.({ phase: "outcome", success: false, statement: sql });
45781
46465
  return { action: "continue" };
46466
+ }
45782
46467
  return this.runStatement(sql, blacklistValidator, referencedTables);
45783
46468
  }
45784
46469
  async runStatement(sql, blacklistValidator, referencedTables) {
45785
46470
  const startTime = Date.now();
46471
+ await this.auditSink?.({ phase: "attempt", success: true, statement: sql });
45786
46472
  try {
45787
46473
  const result = await this.adapter.execute(sql, undefined, {
45788
46474
  noLimit: this.state.noLimit
@@ -45808,6 +46494,7 @@ class ReplEngine {
45808
46494
  ` + import_picocolors3.default.dim(t_vars("shell.timing_display", { ms: String(elapsed) }));
45809
46495
  }
45810
46496
  this.state = { ...this.state, connected: true };
46497
+ await this.auditSink?.({ phase: "outcome", success: true, statement: sql });
45811
46498
  return { action: "continue", output };
45812
46499
  } catch (error) {
45813
46500
  if (this.isConnectionError(error) && this.state.connected) {
@@ -45819,12 +46506,14 @@ class ReplEngine {
45819
46506
  console.error(import_picocolors3.default.green(t("shell.error_reconnect_success")));
45820
46507
  return this.runStatement(sql, blacklistValidator, referencedTables);
45821
46508
  } catch (reconnectError) {
46509
+ await this.auditSink?.({ phase: "outcome", success: false, statement: sql });
45822
46510
  return {
45823
46511
  action: "continue",
45824
46512
  output: import_picocolors3.default.red(t_vars("shell.error_reconnect_failed", { message: reconnectError.message }))
45825
46513
  };
45826
46514
  }
45827
46515
  }
46516
+ await this.auditSink?.({ phase: "outcome", success: false, statement: sql });
45828
46517
  return {
45829
46518
  action: "continue",
45830
46519
  output: import_picocolors3.default.red(t_vars("shell.error_sql_failed", { message: error.message }))
@@ -45859,6 +46548,7 @@ var init_repl_engine = __esm(() => {
45859
46548
  init_query_result_formatter();
45860
46549
  init_message_loader();
45861
46550
  init_sql_tables();
46551
+ init_blacklist_manager();
45862
46552
  init_blacklist_validator();
45863
46553
  init_blacklist();
45864
46554
  import_picocolors3 = __toESM(require_picocolors(), 1);
@@ -46146,30 +46836,25 @@ function findStrings(node) {
46146
46836
  return found;
46147
46837
  }
46148
46838
  function namesProtectedField(term, protectedFields) {
46149
- if (protectedFields.has(term))
46150
- return true;
46151
- const parts = term.split(".");
46152
- if (parts.length === 1)
46153
- return false;
46154
- for (let start = 0;start < parts.length; start += 1) {
46155
- for (let end = start + 1;end <= parts.length; end += 1) {
46156
- if (protectedFields.has(parts.slice(start, end).join(".")))
46157
- return true;
46158
- }
46159
- }
46160
- return false;
46839
+ return reachesProtected(foldFieldPath(term), contiguousRulesFor(protectedFields));
46840
+ }
46841
+ function reachesProtected(folded, rules) {
46842
+ return reachesProtectedPath(folded, rules);
46161
46843
  }
46162
46844
  function redactFields(node, fields, trail = []) {
46845
+ return redactWithGlobs(node, contiguousRulesFor(fields), trail.flatMap((entry) => foldFieldPath(entry).split(".")));
46846
+ }
46847
+ function redactWithGlobs(node, rules, trail) {
46163
46848
  if (Array.isArray(node))
46164
- return node.map((item) => redactFields(item, fields, trail));
46849
+ return node.map((item) => redactWithGlobs(item, rules, trail));
46165
46850
  if (node === null || typeof node !== "object")
46166
46851
  return node;
46167
46852
  const out = {};
46168
46853
  for (const [key, value] of Object.entries(node)) {
46169
- const path4 = [...trail, key];
46170
- if (namesProtectedField(path4.join("."), fields))
46854
+ const path4 = [...trail, ...foldFieldPath(key).split(".")];
46855
+ if (reachesProtectedSegments(path4, rules))
46171
46856
  continue;
46172
- out[key] = redactFields(value, fields, path4);
46857
+ out[key] = redactWithGlobs(value, rules, path4);
46173
46858
  }
46174
46859
  return out;
46175
46860
  }
@@ -46222,7 +46907,9 @@ function assertNoBlacklistedIndexNamed(args) {
46222
46907
  }
46223
46908
  }
46224
46909
  function collectProtectedFields(blacklistColumns) {
46225
- return new Set(Object.values(blacklistColumns).flat().map((field) => field.trim()).filter((field) => field.length > 0));
46910
+ const fields = new Set(Object.values(blacklistColumns).flat().map(normalizeBlacklistEntry).filter((field) => field.length > 0));
46911
+ contiguousRulesFor(fields);
46912
+ return fields;
46226
46913
  }
46227
46914
  function queryStringFieldTerms(query) {
46228
46915
  const CONSERVATIVE = /[\s,:()"'[\]{}]+/;
@@ -46264,6 +46951,9 @@ var init_es_shell_guards = __esm(() => {
46264
46951
  init_message_loader();
46265
46952
  init_es_index_target();
46266
46953
  init_elasticsearch();
46954
+ init_blacklist_fold();
46955
+ init_blacklist_manager();
46956
+ init_path_matcher();
46267
46957
  UNSCOPED_METADATA_PREFIXES = ["_cat", "_cluster", "_nodes", "_license"];
46268
46958
  UNSCOPED_METADATA_WITHHELD = {
46269
46959
  _cluster: new Set(["state"]),
@@ -46400,7 +47090,7 @@ async function executeEsBlock(block2, session) {
46400
47090
  target: record4.target,
46401
47091
  sql: record4.statement,
46402
47092
  sideEffectTier: record4.tierOverride,
46403
- metadata: { es_shell_phase: record4.phase }
47093
+ metadata: { shell_phase: record4.phase }
46404
47094
  })
46405
47095
  });
46406
47096
  console.log(JSON.stringify(res, null, 2));
@@ -46600,6 +47290,25 @@ var init_shell_write_gate = __esm(() => {
46600
47290
  init_write_gate_guard();
46601
47291
  });
46602
47292
 
47293
+ // src/commands/shell-audit-sink.ts
47294
+ var exports_shell_audit_sink = {};
47295
+ __export(exports_shell_audit_sink, {
47296
+ createShellAuditSink: () => createShellAuditSink
47297
+ });
47298
+ function createShellAuditSink(options) {
47299
+ const write = options.write ?? writeAuditEntryResult;
47300
+ return async (record4) => {
47301
+ return await write(options.config, "shell", { config: options.configPath }, {
47302
+ success: record4.success,
47303
+ sql: record4.statement,
47304
+ metadata: { shell_phase: record4.phase }
47305
+ });
47306
+ };
47307
+ }
47308
+ var init_shell_audit_sink = __esm(() => {
47309
+ init_integration_helper();
47310
+ });
47311
+
46603
47312
  // src/commands/shell.ts
46604
47313
  var exports_shell = {};
46605
47314
  __export(exports_shell, {
@@ -46717,7 +47426,11 @@ async function runShell(options, configPath) {
46717
47426
  dialect,
46718
47427
  ask: asker.ask
46719
47428
  }) : null;
46720
- const engine = new ReplEngine(adapter, context, HISTORY_PATH, config, writeGate);
47429
+ const auditSink = (await Promise.resolve().then(() => (init_shell_audit_sink(), exports_shell_audit_sink))).createShellAuditSink({
47430
+ config,
47431
+ configPath
47432
+ });
47433
+ const engine = new ReplEngine(adapter, context, HISTORY_PATH, config, writeGate, auditSink);
46721
47434
  const complete = createCompleter(context);
46722
47435
  console.error(import_picocolors5.default.bold(t_vars("shell.welcome", {
46723
47436
  system: config.connection.system,
@@ -47549,6 +48262,7 @@ var init_migrate = __esm(() => {
47549
48262
  init_ddl2();
47550
48263
  init_ddl_executor();
47551
48264
  init_mutation_confirm();
48265
+ init_blacklist_manager();
47552
48266
  init_config_path();
47553
48267
  migrateCommand = new Command("migrate").description(t("migrate.description"));
47554
48268
  addExecOpts(migrateCommand.command("create <table>").description(t("migrate.create_description")).option("--column <spec...>", 'Column definitions (e.g., "id:serial:pk" "name:varchar(50):not-null")')).action(async (table, opts, command) => {
@@ -47851,7 +48565,9 @@ function envVarNameFor(connName, field) {
47851
48565
  const slug = connName.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
47852
48566
  return `DBCLI_${slug}_${field.toUpperCase()}`;
47853
48567
  }
47854
- var init_config_v2_mutations = () => {};
48568
+ var init_config_v2_mutations = __esm(() => {
48569
+ init_validation();
48570
+ });
47855
48571
 
47856
48572
  // src/core/env-file-writer.ts
47857
48573
  import { chmod as chmod2, mkdir as mkdir18, readFile as readFile9, writeFile as writeFile9 } from "fs/promises";
@@ -48023,7 +48739,7 @@ async function verifyNewPassword(configPath, target, connectionName, password) {
48023
48739
  process.env[target.varName] = password;
48024
48740
  try {
48025
48741
  const config = await configModule.read(storagePath, connectionName);
48026
- const adapter = AdapterFactory.createAdapter({
48742
+ const adapter = AdapterFactory.createAdapterWithoutRules({
48027
48743
  ...config.connection,
48028
48744
  password
48029
48745
  });
@@ -52231,6 +52947,9 @@ var init_data_access = __esm(() => {
52231
52947
  });
52232
52948
 
52233
52949
  // src/core/orm-drift/change-set.ts
52950
+ function starOnlyGlob2(glob) {
52951
+ return glob.replace(/[?[\]\\]/g, "\\$&");
52952
+ }
52234
52953
  function normalizeProposedChanges(input) {
52235
52954
  validateInput(input);
52236
52955
  const gaps = [
@@ -52288,7 +53007,7 @@ function tableMap2(schema, defaultSchema, side) {
52288
53007
  return tables;
52289
53008
  }
52290
53009
  function ignoredTableKeys(input, declared, baseline, gaps) {
52291
- const patterns = [...DEFAULT_IGNORE2, ...input.ignore ?? []].map(globToRegex3);
53010
+ const patterns = [...DEFAULT_IGNORE2, ...input.ignore ?? []];
52292
53011
  const ignored = new Set;
52293
53012
  for (const [side, tables] of [
52294
53013
  ["declared", declared],
@@ -52296,7 +53015,7 @@ function ignoredTableKeys(input, declared, baseline, gaps) {
52296
53015
  ]) {
52297
53016
  for (const [key, resolved] of tables) {
52298
53017
  const name = qualifiedTableName(resolved.identity);
52299
- if (!patterns.some((pattern) => pattern.test(name) || pattern.test(resolved.identity.table)))
53018
+ if (!patterns.some((pattern) => globMatches(starOnlyGlob2(pattern), name) || globMatches(starOnlyGlob2(pattern), resolved.identity.table)))
52300
53019
  continue;
52301
53020
  if (ignored.has(key))
52302
53021
  continue;
@@ -52474,10 +53193,6 @@ function unparsedGaps(schema, side) {
52474
53193
  reason: entry.reason
52475
53194
  }));
52476
53195
  }
52477
- function globToRegex3(glob) {
52478
- const escaped = glob.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
52479
- return new RegExp(`^${escaped}$`);
52480
- }
52481
53196
  function gapOrder(left, right) {
52482
53197
  return codePointOrder5(left.side, right.side) || codePointOrder5(left.kind, right.kind) || codePointOrder5(left.location, right.location) || codePointOrder5(left.reason, right.reason);
52483
53198
  }
@@ -52493,6 +53208,7 @@ function codePointOrder5(left, right) {
52493
53208
  }
52494
53209
  var DEFAULT_IGNORE2, NormalizedChangeSetError;
52495
53210
  var init_change_set = __esm(() => {
53211
+ init_glob();
52496
53212
  DEFAULT_IGNORE2 = ["_prisma_migrations"];
52497
53213
  NormalizedChangeSetError = class NormalizedChangeSetError extends Error {
52498
53214
  constructor(message) {