@carllee1983/dbcli 4.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: "6.0.0",
55
55
  description: "Database CLI for AI agents",
56
56
  type: "module",
57
57
  publishConfig: {
@@ -4372,7 +4372,7 @@ function validateFormat(value, allowedFormats, commandName) {
4372
4372
  throw new Error(`Invalid format "${value}" for ${commandName}. Allowed: ${allowed}`);
4373
4373
  }
4374
4374
  }
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;
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, DEFAULT_AUDIT_ROTATION, AuditRotationConfigSchema, AuditConfigSchema, DbcliConfigSchema, SqlNamedConnectionSchema, MongoDBNamedConnectionSchema, RedisNamedConnectionSchema, ElasticsearchNamedConnectionSchema, NamedConnectionUnion, NamedConnectionSchema, DbcliConfigV2Schema;
4376
4376
  var init_validation = __esm(() => {
4377
4377
  init_zod();
4378
4378
  EnvRefSchema = exports_external.object({
@@ -4474,10 +4474,11 @@ var init_validation = __esm(() => {
4474
4474
  RedisConfigSchema = exports_external.object({
4475
4475
  mask: exports_external.array(RedisMaskRuleSchema).default([])
4476
4476
  }).optional();
4477
+ DEFAULT_AUDIT_ROTATION = { max_bytes: 10485760, max_entries: 1e4 };
4477
4478
  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 });
4479
+ max_bytes: exports_external.number().int().positive().default(DEFAULT_AUDIT_ROTATION.max_bytes),
4480
+ max_entries: exports_external.number().int().positive().default(DEFAULT_AUDIT_ROTATION.max_entries)
4481
+ }).optional().default({ ...DEFAULT_AUDIT_ROTATION });
4481
4482
  AuditConfigSchema = exports_external.object({
4482
4483
  enabled: exports_external.boolean().default(true),
4483
4484
  strict: exports_external.boolean().default(false),
@@ -4488,7 +4489,7 @@ var init_validation = __esm(() => {
4488
4489
  }).optional().default({
4489
4490
  enabled: true,
4490
4491
  strict: false,
4491
- rotation: { max_bytes: 10485760, max_entries: 1000 }
4492
+ rotation: { ...DEFAULT_AUDIT_ROTATION }
4492
4493
  });
4493
4494
  DbcliConfigSchema = exports_external.object({
4494
4495
  connection: ConnectionConfigSchema,
@@ -6321,7 +6322,7 @@ var init_config = __esm(() => {
6321
6322
  audit: {
6322
6323
  enabled: true,
6323
6324
  strict: false,
6324
- rotation: { max_bytes: 10485760, max_entries: 1000 }
6325
+ rotation: { ...DEFAULT_AUDIT_ROTATION }
6325
6326
  }
6326
6327
  };
6327
6328
  configModule = {
@@ -11164,6 +11165,233 @@ var init_server_side_script = __esm(() => {
11164
11165
  ES_OPAQUE_BODY_KEYS = ["wrapper"];
11165
11166
  });
11166
11167
 
11168
+ // src/utils/glob.ts
11169
+ function findClassEnd(glob, open2) {
11170
+ for (let i = open2 + 1;i < glob.length; i++) {
11171
+ if (glob[i] === "\\") {
11172
+ i++;
11173
+ continue;
11174
+ }
11175
+ if (glob[i] === "]")
11176
+ return i;
11177
+ }
11178
+ return -1;
11179
+ }
11180
+ function isValidCharacterClass(body) {
11181
+ try {
11182
+ new RegExp(body);
11183
+ return true;
11184
+ } catch {
11185
+ return false;
11186
+ }
11187
+ }
11188
+ function parseGlob(glob) {
11189
+ const memo = parsedGlobs.get(glob);
11190
+ if (memo !== undefined)
11191
+ return memo;
11192
+ const parsed = parseGlobUncached(glob);
11193
+ parsedGlobs.set(glob, parsed);
11194
+ return parsed;
11195
+ }
11196
+ function parseGlobUncached(glob) {
11197
+ const runs = [];
11198
+ let current = [];
11199
+ let leadingStar = false;
11200
+ let trailingStar = false;
11201
+ let sawStar = false;
11202
+ const endRun = () => {
11203
+ if (current.length > 0)
11204
+ runs.push(current);
11205
+ current = [];
11206
+ };
11207
+ for (let i = 0;i < glob.length; i++) {
11208
+ const c = glob[i];
11209
+ if (c === "\\" && i + 1 < glob.length) {
11210
+ trailingStar = false;
11211
+ current.push({ kind: "literal", char: glob[++i] });
11212
+ continue;
11213
+ }
11214
+ if (c === "*") {
11215
+ if (!sawStar && current.length === 0)
11216
+ leadingStar = true;
11217
+ sawStar = true;
11218
+ trailingStar = true;
11219
+ endRun();
11220
+ continue;
11221
+ }
11222
+ trailingStar = false;
11223
+ if (c === "?") {
11224
+ current.push({ kind: "any" });
11225
+ continue;
11226
+ }
11227
+ if (c === "[") {
11228
+ const end = findClassEnd(glob, i);
11229
+ const body = end === -1 ? "" : glob.slice(i, end + 1);
11230
+ if (body !== "" && isValidCharacterClass(body)) {
11231
+ current.push({ kind: "class", test: new RegExp(`^${body}$`, "s") });
11232
+ i = end;
11233
+ continue;
11234
+ }
11235
+ current.push({ kind: "literal", char: "[" });
11236
+ continue;
11237
+ }
11238
+ current.push({ kind: "literal", char: c });
11239
+ }
11240
+ endRun();
11241
+ return { runs, leadingStar, trailingStar, anchored: !sawStar };
11242
+ }
11243
+ function runMatchesAt(run, text2, at) {
11244
+ if (at + run.length > text2.length)
11245
+ return false;
11246
+ for (let i = 0;i < run.length; i++) {
11247
+ const token = run[i];
11248
+ const ch = text2[at + i];
11249
+ if (token.kind === "literal") {
11250
+ if (token.char !== ch)
11251
+ return false;
11252
+ } else if (token.kind === "class") {
11253
+ if (!token.test.test(ch))
11254
+ return false;
11255
+ }
11256
+ }
11257
+ return true;
11258
+ }
11259
+ function globMatches(glob, text2) {
11260
+ const { runs, leadingStar, trailingStar, anchored } = parseGlob(glob);
11261
+ if (anchored) {
11262
+ const run = runs[0] ?? [];
11263
+ return text2.length === run.length && runMatchesAt(run, text2, 0);
11264
+ }
11265
+ if (runs.length === 0)
11266
+ return true;
11267
+ let cursor = 0;
11268
+ let first = 0;
11269
+ let last = runs.length;
11270
+ if (!leadingStar) {
11271
+ const head = runs[0];
11272
+ if (!runMatchesAt(head, text2, 0))
11273
+ return false;
11274
+ cursor = head.length;
11275
+ first = 1;
11276
+ }
11277
+ if (!trailingStar) {
11278
+ const tail = runs[last - 1];
11279
+ const at = text2.length - tail.length;
11280
+ if (at < cursor || !runMatchesAt(tail, text2, at))
11281
+ return false;
11282
+ last -= 1;
11283
+ }
11284
+ const limit = trailingStar ? text2.length : text2.length - runs[runs.length - 1].length;
11285
+ for (let r = first;r < last; r++) {
11286
+ const run = runs[r];
11287
+ let found = -1;
11288
+ for (let at = cursor;at + run.length <= limit; at++) {
11289
+ if (runMatchesAt(run, text2, at)) {
11290
+ found = at;
11291
+ break;
11292
+ }
11293
+ }
11294
+ if (found === -1)
11295
+ return false;
11296
+ cursor = found + run.length;
11297
+ }
11298
+ return true;
11299
+ }
11300
+ function escapeGlob(literal) {
11301
+ return literal.replace(/[*?[\]\\]/g, "\\$&");
11302
+ }
11303
+ function globNeverMatches(glob) {
11304
+ for (let i = 0;i < glob.length; i++) {
11305
+ const c = glob[i];
11306
+ if (c === "\\") {
11307
+ i++;
11308
+ continue;
11309
+ }
11310
+ if (c !== "[")
11311
+ continue;
11312
+ const end = findClassEnd(glob, i);
11313
+ if (end === -1) {
11314
+ return "unclosed character class";
11315
+ }
11316
+ const body = glob.slice(i, end + 1);
11317
+ if (body === "[]" || body === "[^]")
11318
+ return "empty character class matches nothing";
11319
+ if (!isValidCharacterClass(body))
11320
+ return "invalid character class";
11321
+ i = end;
11322
+ }
11323
+ return null;
11324
+ }
11325
+ var parsedGlobs;
11326
+ var init_glob = __esm(() => {
11327
+ parsedGlobs = new Map;
11328
+ });
11329
+
11330
+ // src/core/mongo/path-matcher.ts
11331
+ function compilePatterns(raw) {
11332
+ const patterns = [];
11333
+ const rejected = [];
11334
+ for (const entry of raw) {
11335
+ if (typeof entry !== "string" || entry.length === 0) {
11336
+ rejected.push({ raw: String(entry ?? ""), reason: "must be a non-empty string" });
11337
+ continue;
11338
+ }
11339
+ const segments = entry.split(".");
11340
+ if (segments.some((s) => s.length === 0)) {
11341
+ rejected.push({ raw: entry, reason: "empty path segment" });
11342
+ continue;
11343
+ }
11344
+ const globstar = segments.find((seg) => seg.includes("**"));
11345
+ if (globstar !== undefined) {
11346
+ rejected.push({
11347
+ raw: entry,
11348
+ reason: "`**` matches one segment, not a subtree; write `*` or a longer path"
11349
+ });
11350
+ continue;
11351
+ }
11352
+ const dead = segments.map((seg) => globNeverMatches(seg)).find((r) => r !== null);
11353
+ if (dead !== undefined && dead !== null) {
11354
+ rejected.push({ raw: entry, reason: dead });
11355
+ continue;
11356
+ }
11357
+ const wildcardTail = segments.length > 1 && segments[segments.length - 1] === "*";
11358
+ const literal = wildcardTail ? segments.slice(0, -1) : segments;
11359
+ patterns.push({
11360
+ raw: entry,
11361
+ segments: literal,
11362
+ wildcardTail
11363
+ });
11364
+ }
11365
+ return { patterns, rejected };
11366
+ }
11367
+ function matchAny(path, patterns) {
11368
+ if (patterns.length === 0)
11369
+ return false;
11370
+ const pathSegments = path.split(".");
11371
+ for (const pat of patterns) {
11372
+ if (pat.wildcardTail) {
11373
+ if (pathSegments.length < pat.segments.length)
11374
+ continue;
11375
+ } else {
11376
+ if (pat.segments.length !== pathSegments.length)
11377
+ continue;
11378
+ }
11379
+ let ok = true;
11380
+ for (let i = 0;i < pat.segments.length; i++) {
11381
+ if (!globMatches(pat.segments[i], pathSegments[i])) {
11382
+ ok = false;
11383
+ break;
11384
+ }
11385
+ }
11386
+ if (ok)
11387
+ return true;
11388
+ }
11389
+ return false;
11390
+ }
11391
+ var init_path_matcher = __esm(() => {
11392
+ init_glob();
11393
+ });
11394
+
11167
11395
  // src/core/mongo/request-fields.ts
11168
11396
  function asFieldPath(text2) {
11169
11397
  if (text2.startsWith("$$"))
@@ -11172,15 +11400,31 @@ function asFieldPath(text2) {
11172
11400
  return text2.slice(1);
11173
11401
  return text2;
11174
11402
  }
11175
- function reachesProtectedField(path, protectedFields) {
11403
+ function globRulesOf(protectedFields) {
11404
+ const globbed = [...protectedFields].filter((rule) => /[*?[\\]/.test(rule));
11405
+ if (globbed.length === 0)
11406
+ return [];
11407
+ const { patterns, rejected } = compilePatterns(globbed);
11408
+ if (rejected.length > 0) {
11409
+ const detail = rejected.map((r) => `'${r.raw}' (${r.reason})`).join(", ");
11410
+ throw new Error(`BlacklistRejection: blacklist entries this matcher cannot read: ${detail}`);
11411
+ }
11412
+ return patterns;
11413
+ }
11414
+ function reachesProtectedField(path, protectedFields, globs) {
11176
11415
  if (protectedFields.has(path))
11177
11416
  return true;
11417
+ if (globs.length > 0 && matchAny(path, globs))
11418
+ return true;
11178
11419
  const parts = path.split(".");
11179
11420
  if (parts.length === 1)
11180
11421
  return false;
11181
11422
  for (let start = 0;start < parts.length; start += 1) {
11182
11423
  for (let end = start + 1;end <= parts.length; end += 1) {
11183
- if (protectedFields.has(parts.slice(start, end).join(".")))
11424
+ const candidate = parts.slice(start, end).join(".");
11425
+ if (protectedFields.has(candidate))
11426
+ return true;
11427
+ if (globs.length > 0 && matchAny(candidate, globs))
11184
11428
  return true;
11185
11429
  }
11186
11430
  }
@@ -11189,6 +11433,7 @@ function reachesProtectedField(path, protectedFields) {
11189
11433
  function findProtectedFieldReference(request, protectedFields) {
11190
11434
  if (protectedFields.size === 0)
11191
11435
  return;
11436
+ const globs = globRulesOf(protectedFields);
11192
11437
  const candidates = [];
11193
11438
  let movesWholeDocument = false;
11194
11439
  const walk = (node) => {
@@ -11222,7 +11467,7 @@ function findProtectedFieldReference(request, protectedFields) {
11222
11467
  }
11223
11468
  };
11224
11469
  walk(request);
11225
- const named = candidates.find((path) => path.length > 0 && reachesProtectedField(path, protectedFields));
11470
+ const named = candidates.find((path) => path.length > 0 && reachesProtectedField(path, protectedFields, globs));
11226
11471
  if (named !== undefined)
11227
11472
  return named;
11228
11473
  return movesWholeDocument ? [...protectedFields][0] : undefined;
@@ -11261,6 +11506,7 @@ function protectedFieldsForRequest(request, collection, columns) {
11261
11506
  }
11262
11507
  var WHOLE_DOCUMENT_VARIABLES, RESHAPES_DOCUMENT, NAMES_A_FIELD_DYNAMICALLY = "$getField";
11263
11508
  var init_request_fields = __esm(() => {
11509
+ init_path_matcher();
11264
11510
  WHOLE_DOCUMENT_VARIABLES = new Set(["ROOT", "CURRENT"]);
11265
11511
  RESHAPES_DOCUMENT = new Set(["$objectToArray", "$replaceRoot", "$replaceWith"]);
11266
11512
  });
@@ -12215,66 +12461,13 @@ var init_size_guard = __esm(() => {
12215
12461
  init_types3();
12216
12462
  });
12217
12463
 
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
12464
  // src/adapters/redis/blacklist-enforcer.ts
12270
12465
  function patternsOverlap(a, b) {
12271
- const ra = globToRegex(a);
12272
- const rb = globToRegex(b);
12273
12466
  const probeA = sampleFromGlob(a);
12274
12467
  const probeB = sampleFromGlob(b);
12275
- if (probeA !== null && rb.test(probeA))
12468
+ if (probeA !== null && globMatches(b, probeA))
12276
12469
  return true;
12277
- if (probeB !== null && ra.test(probeB))
12470
+ if (probeB !== null && globMatches(a, probeB))
12278
12471
  return true;
12279
12472
  return false;
12280
12473
  }
@@ -12317,7 +12510,7 @@ function checkKeyArgs(command, args, rules) {
12317
12510
  if (key === undefined)
12318
12511
  continue;
12319
12512
  for (const pat of rules) {
12320
- if (globToRegex(pat).test(key)) {
12513
+ if (globMatches(pat, key)) {
12321
12514
  return { ok: false, matchedKey: key, matchedPattern: pat };
12322
12515
  }
12323
12516
  }
@@ -12374,6 +12567,7 @@ function expandKeyArity(arity, args, argCount) {
12374
12567
  }
12375
12568
  var init_blacklist_enforcer = __esm(() => {
12376
12569
  init_command_metadata();
12570
+ init_glob();
12377
12571
  });
12378
12572
 
12379
12573
  // src/adapters/redis/returned-key-names.ts
@@ -12385,8 +12579,7 @@ function filterReturnedKeyNames(command, reply, rules) {
12385
12579
  return reply;
12386
12580
  if (!returnsKeyNames(command))
12387
12581
  return reply;
12388
- const regexes = rules.map((pattern) => globToRegex(pattern));
12389
- const permitted = (key) => typeof key === "string" && regexes.every((regex) => !regex.test(key));
12582
+ const permitted = (key) => typeof key === "string" && rules.every((rule) => !globMatches(rule, key));
12390
12583
  if (Array.isArray(reply) && reply.length === 2 && Array.isArray(reply[1])) {
12391
12584
  return [reply[0], reply[1].filter(permitted)];
12392
12585
  }
@@ -12396,12 +12589,13 @@ function filterReturnedKeyNames(command, reply, rules) {
12396
12589
  }
12397
12590
  var RETURNS_KEY_NAMES;
12398
12591
  var init_returned_key_names = __esm(() => {
12592
+ init_glob();
12399
12593
  RETURNS_KEY_NAMES = new Set(["SCAN", "KEYS"]);
12400
12594
  });
12401
12595
 
12402
12596
  // src/adapters/redis/value-masker.ts
12403
12597
  function planFor(key, rules) {
12404
- const matched = rules.filter((r) => globToRegex(r.keyPattern).test(key));
12598
+ const matched = rules.filter((r) => globMatches(r.keyPattern, key));
12405
12599
  if (matched.length === 0)
12406
12600
  return null;
12407
12601
  const wholeValue = matched.some((r) => !r.fields || r.fields.length === 0);
@@ -12456,7 +12650,7 @@ function maskRedisRows(command, args, rows, rules) {
12456
12650
  }
12457
12651
  var REDACTED = "[REDACTED]", MASKABLE;
12458
12652
  var init_value_masker = __esm(() => {
12459
- init_blacklist_enforcer();
12653
+ init_glob();
12460
12654
  MASKABLE = new Set(["GET", "GETRANGE", "HGETALL", "HGET", "HMGET", "HVALS"]);
12461
12655
  });
12462
12656
 
@@ -12547,8 +12741,7 @@ class RedisAdapter {
12547
12741
  async sampleKeyNames(limit) {
12548
12742
  const client = this.requireClient();
12549
12743
  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)));
12744
+ const { keys, scanned } = await scanAllKeys(client, "*", limit, limit, (key) => rules.every((rule) => !globMatches(rule, key)));
12552
12745
  return { names: keys, truncated: scanned >= limit };
12553
12746
  }
12554
12747
  async getDbSize() {
@@ -12927,6 +13120,7 @@ var init_redis_adapter = __esm(() => {
12927
13120
  init_types2();
12928
13121
  init_size_guard();
12929
13122
  init_blacklist_enforcer();
13123
+ init_glob();
12930
13124
  init_returned_key_names();
12931
13125
  init_types3();
12932
13126
  init_command_metadata();
@@ -13449,7 +13643,7 @@ async function writeV2InitConfig(configPath, connectionName, connection, permiss
13449
13643
  audit: {
13450
13644
  enabled: true,
13451
13645
  strict: false,
13452
- rotation: { max_bytes: 10485760, max_entries: 1000 }
13646
+ rotation: { ...DEFAULT_AUDIT_ROTATION }
13453
13647
  }
13454
13648
  };
13455
13649
  await writeV2Config(storagePath, v2Config);
@@ -13464,6 +13658,7 @@ var init_init_shared = __esm(() => {
13464
13658
  init_config();
13465
13659
  init_config_v2();
13466
13660
  init_prompts();
13661
+ init_validation();
13467
13662
  init_config_binding();
13468
13663
  });
13469
13664
 
@@ -16457,6 +16652,23 @@ class ColumnIndexBuilder {
16457
16652
  }
16458
16653
  }
16459
16654
  // src/core/blacklist-manager.ts
16655
+ function normalizeBlacklistEntry(raw) {
16656
+ const trimmed = raw.trim();
16657
+ const quote = trimmed[0];
16658
+ if ((quote === '"' || quote === "`") && trimmed.length > 1 && trimmed.endsWith(quote)) {
16659
+ return trimmed.slice(1, -1).trim();
16660
+ }
16661
+ return trimmed;
16662
+ }
16663
+ function assertNotTableQualified(tableKey, column) {
16664
+ const dot = column.indexOf(".");
16665
+ if (dot <= 0)
16666
+ return;
16667
+ if (column.slice(0, dot).toLowerCase() !== tableKey.toLowerCase())
16668
+ return;
16669
+ 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))}.`);
16670
+ }
16671
+
16460
16672
  class BlacklistManager {
16461
16673
  config;
16462
16674
  state;
@@ -16465,6 +16677,7 @@ class BlacklistManager {
16465
16677
  this.config = config;
16466
16678
  this.overrideEnabled = (overrideEnvValue ?? Bun.env.DBCLI_OVERRIDE_BLACKLIST ?? "") === "true";
16467
16679
  this.state = this.loadBlacklist();
16680
+ this.wildcardTables = [...this.state.tables].filter((entry) => /[*?[\\]/.test(entry));
16468
16681
  }
16469
16682
  loadBlacklist() {
16470
16683
  const tables = new Set;
@@ -16476,7 +16689,7 @@ class BlacklistManager {
16476
16689
  if (Array.isArray(blacklistConfig.tables)) {
16477
16690
  for (const tableName of blacklistConfig.tables) {
16478
16691
  if (typeof tableName === "string") {
16479
- tables.add(tableName.toLowerCase());
16692
+ tables.add(normalizeBlacklistEntry(tableName).toLowerCase());
16480
16693
  } else {
16481
16694
  console.warn(`[BlacklistManager] Invalid table name in blacklist config: ${JSON.stringify(tableName)}`);
16482
16695
  }
@@ -16497,13 +16710,15 @@ class BlacklistManager {
16497
16710
  const columnSet = new Set;
16498
16711
  for (const col of cols) {
16499
16712
  if (typeof col === "string") {
16500
- columnSet.add(col);
16713
+ const entry = normalizeBlacklistEntry(col);
16714
+ assertNotTableQualified(normalizeBlacklistEntry(tableName), entry);
16715
+ columnSet.add(entry);
16501
16716
  } else {
16502
16717
  console.warn(`[BlacklistManager] Invalid column name in blacklist.columns["${tableName}"]: ${JSON.stringify(col)}`);
16503
16718
  }
16504
16719
  }
16505
16720
  if (columnSet.size > 0) {
16506
- columns.set(tableName.toLowerCase(), columnSet);
16721
+ columns.set(normalizeBlacklistEntry(tableName).toLowerCase(), columnSet);
16507
16722
  }
16508
16723
  }
16509
16724
  } else if (blacklistConfig.columns !== undefined) {
@@ -16512,17 +16727,47 @@ class BlacklistManager {
16512
16727
  return { tables, columns };
16513
16728
  }
16514
16729
  isTableBlacklisted(tableName) {
16515
- return this.state.tables.has(tableName.toLowerCase());
16730
+ const name = tableName.toLowerCase();
16731
+ if (this.state.tables.has(name))
16732
+ return true;
16733
+ for (const pattern of this.wildcardTables) {
16734
+ if (globMatches(pattern, name))
16735
+ return true;
16736
+ }
16737
+ return false;
16738
+ }
16739
+ wildcardTables;
16740
+ columnRulesFor(tableName) {
16741
+ const key = normalizeBlacklistEntry(tableName).toLowerCase();
16742
+ const direct = this.state.columns.get(key);
16743
+ if (direct)
16744
+ return direct;
16745
+ const dot = key.lastIndexOf(".");
16746
+ if (dot > 0) {
16747
+ const bare = this.state.columns.get(key.slice(dot + 1));
16748
+ if (bare)
16749
+ return bare;
16750
+ }
16751
+ let merged;
16752
+ for (const [ruleKey, columns] of this.state.columns) {
16753
+ const ruleDot = ruleKey.lastIndexOf(".");
16754
+ if (ruleDot <= 0 || ruleKey.slice(ruleDot + 1) !== key)
16755
+ continue;
16756
+ merged ??= new Set;
16757
+ for (const column of columns)
16758
+ merged.add(column);
16759
+ }
16760
+ return merged;
16516
16761
  }
16517
16762
  isColumnBlacklisted(tableName, columnName) {
16518
- const columnSet = this.state.columns.get(tableName.toLowerCase());
16763
+ const columnSet = this.columnRulesFor(tableName);
16519
16764
  if (!columnSet) {
16520
16765
  return false;
16521
16766
  }
16522
- return columnSet.has(columnName);
16767
+ return columnSet.has(normalizeBlacklistEntry(columnName).toLowerCase());
16523
16768
  }
16524
16769
  getBlacklistedColumns(tableName) {
16525
- const columnSet = this.state.columns.get(tableName.toLowerCase());
16770
+ const columnSet = this.columnRulesFor(tableName);
16526
16771
  if (!columnSet) {
16527
16772
  return [];
16528
16773
  }
@@ -16543,6 +16788,9 @@ class BlacklistManager {
16543
16788
  return this.state;
16544
16789
  }
16545
16790
  }
16791
+ var init_blacklist_manager = __esm(() => {
16792
+ init_glob();
16793
+ });
16546
16794
 
16547
16795
  // src/types/blacklist.ts
16548
16796
  var BlacklistError;
@@ -16819,7 +17067,7 @@ function expandIndexTargets(target) {
16819
17067
  function matchesIndexGlob(pattern, name) {
16820
17068
  const normalized = pattern.toLowerCase() === "_all" ? "*" : pattern.toLowerCase();
16821
17069
  try {
16822
- return globToRegex(normalized).test(name.toLowerCase());
17070
+ return globMatches(normalized, name.toLowerCase());
16823
17071
  } catch {
16824
17072
  return true;
16825
17073
  }
@@ -16851,7 +17099,9 @@ function indexExpressionReaches(expression, blacklisted) {
16851
17099
  return concrete.some(reachesName) || wildcards.some((pattern) => entries.names.some((entry) => matchesIndexGlob(pattern, entry)) || entries.patterns.length > 0);
16852
17100
  }
16853
17101
  var MAX_DECODE_PASSES = 4;
16854
- var init_es_index_target = () => {};
17102
+ var init_es_index_target = __esm(() => {
17103
+ init_glob();
17104
+ });
16855
17105
 
16856
17106
  // src/core/blacklist-validator.ts
16857
17107
  function flattenArrayRow(row) {
@@ -16876,6 +17126,17 @@ function dedupe2(values) {
16876
17126
  }
16877
17127
  return result;
16878
17128
  }
17129
+ function compileGlobRules(rules, fold, table, operation) {
17130
+ const globbed = rules.filter((rule) => /[*?[\\]/.test(rule));
17131
+ if (globbed.length === 0)
17132
+ return [];
17133
+ const { patterns, rejected } = compilePatterns(globbed.map(fold));
17134
+ if (rejected.length > 0) {
17135
+ const detail = rejected.map((r) => `'${r.raw}' (${r.reason})`).join(", ");
17136
+ throw new BlacklistError(`blacklist.columns for '${table}' has entries this matcher cannot read: ${detail}`, table, operation);
17137
+ }
17138
+ return patterns;
17139
+ }
16879
17140
 
16880
17141
  class BlacklistValidator {
16881
17142
  manager;
@@ -16933,7 +17194,27 @@ class BlacklistValidator {
16933
17194
  if (blacklisted.length === 0 || fields.length === 0) {
16934
17195
  return;
16935
17196
  }
16936
- const conflicts = fields.filter((f) => blacklisted.includes(f));
17197
+ const protectedPaths = new Set(blacklisted.map((name) => name.toLowerCase()));
17198
+ const globs = compileGlobRules(blacklisted, (value) => value.toLowerCase(), tableName, operation);
17199
+ const conflicts = fields.filter((field) => {
17200
+ const name = field.toLowerCase();
17201
+ if (protectedPaths.has(name))
17202
+ return true;
17203
+ if (matchAny(name, globs))
17204
+ return true;
17205
+ let dot = name.indexOf(".");
17206
+ while (dot >= 0) {
17207
+ if (dot > 0) {
17208
+ const ancestor = name.slice(0, dot);
17209
+ if (protectedPaths.has(ancestor))
17210
+ return true;
17211
+ if (matchAny(ancestor, globs))
17212
+ return true;
17213
+ }
17214
+ dot = name.indexOf(".", dot + 1);
17215
+ }
17216
+ return false;
17217
+ });
16937
17218
  if (conflicts.length === 0) {
16938
17219
  return;
16939
17220
  }
@@ -16984,11 +17265,20 @@ class BlacklistValidator {
16984
17265
  }
16985
17266
  collect(row);
16986
17267
  }
16987
- const protectedPaths = new Set(blacklistedColumns);
17268
+ const foldHead = (path) => {
17269
+ const dot = path.indexOf(".");
17270
+ return dot < 0 ? path.toLowerCase() : path.slice(0, dot).toLowerCase() + path.slice(dot);
17271
+ };
17272
+ const protectedPaths = new Set(blacklistedColumns.map(foldHead));
17273
+ const globRules = compileGlobRules(blacklistedColumns, foldHead, tables[0] ?? "unknown", "SELECT");
17274
+ const presentByFolded = new Map;
17275
+ for (const column of presentColumns)
17276
+ presentByFolded.set(foldHead(column), column);
16988
17277
  const omitted = new Set;
16989
17278
  for (const path of blacklistedColumns) {
16990
- if (presentColumns.has(path)) {
16991
- omitted.add(path);
17279
+ const present = presentByFolded.get(foldHead(path));
17280
+ if (present !== undefined) {
17281
+ omitted.add(present);
16992
17282
  continue;
16993
17283
  }
16994
17284
  const dot = path.indexOf(".");
@@ -17003,13 +17293,30 @@ class BlacklistValidator {
17003
17293
  for (const column of presentColumns) {
17004
17294
  let dot = column.indexOf(".");
17005
17295
  while (dot >= 0) {
17006
- if (dot > 0 && protectedPaths.has(column.slice(0, dot))) {
17296
+ if (dot > 0 && protectedPaths.has(column.slice(0, dot).toLowerCase())) {
17007
17297
  omitted.add(column);
17008
17298
  break;
17009
17299
  }
17010
17300
  dot = column.indexOf(".", dot + 1);
17011
17301
  }
17012
17302
  }
17303
+ if (globRules.length > 0) {
17304
+ for (const column of presentColumns) {
17305
+ const folded = foldHead(column);
17306
+ if (matchAny(folded, globRules)) {
17307
+ omitted.add(column);
17308
+ continue;
17309
+ }
17310
+ let dot = folded.indexOf(".");
17311
+ while (dot >= 0) {
17312
+ if (dot > 0 && matchAny(folded.slice(0, dot), globRules)) {
17313
+ omitted.add(column);
17314
+ break;
17315
+ }
17316
+ dot = folded.indexOf(".", dot + 1);
17317
+ }
17318
+ }
17319
+ }
17013
17320
  const omittedColumns = Array.from(omitted);
17014
17321
  if (omittedColumns.length === 0) {
17015
17322
  return { filteredRows: rows, omittedColumns: [] };
@@ -17030,6 +17337,7 @@ var init_blacklist_validator = __esm(() => {
17030
17337
  init_blacklist();
17031
17338
  init_message_loader();
17032
17339
  init_field_projection();
17340
+ init_path_matcher();
17033
17341
  init_es_index_target();
17034
17342
  });
17035
17343
 
@@ -18040,6 +18348,7 @@ var init_core = __esm(() => {
18040
18348
  init_schema_updater();
18041
18349
  init_concurrent_lock();
18042
18350
  init_error_recovery();
18351
+ init_blacklist_manager();
18043
18352
  init_blacklist_validator();
18044
18353
  init_size_category();
18045
18354
  init_health_checker();
@@ -18418,6 +18727,355 @@ var init_sql_lexical = __esm(() => {
18418
18727
  DOLLAR_QUOTE_DELIMITER = /^\$(?:(?:[A-Za-z_]|[\u0080-\uFFFF])(?:[A-Za-z0-9_]|[\u0080-\uFFFF])*)?\$/;
18419
18728
  });
18420
18729
 
18730
+ // src/utils/sql-tables.ts
18731
+ var exports_sql_tables = {};
18732
+ __export(exports_sql_tables, {
18733
+ extractTableReferences: () => extractTableReferences
18734
+ });
18735
+ function tokenize(sql, dialect, backslashEscapes) {
18736
+ const tokens = [];
18737
+ const mysqlDialect = dialect === "mysql" || dialect === "mariadb";
18738
+ let i = 0;
18739
+ while (i < sql.length) {
18740
+ const char = sql[i];
18741
+ const dashFollowerCode = sql.charCodeAt(i + 2);
18742
+ if (char === "-" && sql[i + 1] === "-" && (!mysqlDialect || sql[i + 2] === undefined || dashFollowerCode <= 32 || dashFollowerCode === 127)) {
18743
+ while (i < sql.length && sql[i] !== `
18744
+ `)
18745
+ i++;
18746
+ continue;
18747
+ }
18748
+ if (mysqlDialect && char === "#") {
18749
+ while (i < sql.length && sql[i] !== `
18750
+ `)
18751
+ i++;
18752
+ continue;
18753
+ }
18754
+ if (char === "/" && sql[i + 1] === "*") {
18755
+ if (mysqlDialect && (sql.startsWith("/*!", i) || sql.startsWith("/*M!", i))) {
18756
+ const prefixLength = sql.startsWith("/*M!", i) ? 4 : 3;
18757
+ const closingIndex = sql.indexOf("*/", i + prefixLength);
18758
+ const bodyEnd = closingIndex === -1 ? sql.length : closingIndex;
18759
+ const body = sql.slice(i + prefixLength, bodyEnd).replace(/^\d+/, " ");
18760
+ tokens.push(...tokenize(body, dialect, backslashEscapes));
18761
+ i = closingIndex === -1 ? sql.length : closingIndex + 2;
18762
+ continue;
18763
+ }
18764
+ const nests = dialect === "postgresql";
18765
+ let depth = 1;
18766
+ i += 2;
18767
+ while (i < sql.length && depth > 0) {
18768
+ if (nests && sql[i] === "/" && sql[i + 1] === "*") {
18769
+ depth++;
18770
+ i += 2;
18771
+ continue;
18772
+ }
18773
+ if (sql[i] === "*" && sql[i + 1] === "/") {
18774
+ depth--;
18775
+ i += 2;
18776
+ continue;
18777
+ }
18778
+ i++;
18779
+ }
18780
+ continue;
18781
+ }
18782
+ if (dialect === "postgresql" && char === "$") {
18783
+ const delimiter = dollarQuoteDelimiterAt(sql, i);
18784
+ if (delimiter) {
18785
+ i += delimiter.length;
18786
+ const closingIndex = sql.indexOf(delimiter, i);
18787
+ i = closingIndex === -1 ? sql.length : closingIndex + delimiter.length;
18788
+ continue;
18789
+ }
18790
+ }
18791
+ if (char === "'") {
18792
+ i++;
18793
+ while (i < sql.length) {
18794
+ if (sql[i] === "'") {
18795
+ if (sql[i + 1] === "'") {
18796
+ i += 2;
18797
+ continue;
18798
+ }
18799
+ i++;
18800
+ break;
18801
+ }
18802
+ if (backslashEscapes && sql[i] === "\\") {
18803
+ i += 2;
18804
+ continue;
18805
+ }
18806
+ i++;
18807
+ }
18808
+ continue;
18809
+ }
18810
+ if (char === '"' || char === "`") {
18811
+ const quote = char;
18812
+ i++;
18813
+ let value = "";
18814
+ while (i < sql.length) {
18815
+ if (sql[i] === quote) {
18816
+ if (sql[i + 1] === quote) {
18817
+ value += quote;
18818
+ i += 2;
18819
+ continue;
18820
+ }
18821
+ i++;
18822
+ break;
18823
+ }
18824
+ if (backslashEscapes && sql[i] === "\\") {
18825
+ value += sql[i + 1] ?? "";
18826
+ i += 2;
18827
+ continue;
18828
+ }
18829
+ value += sql[i];
18830
+ i++;
18831
+ }
18832
+ tokens.push({ value, kind: "identifier", quoted: true });
18833
+ continue;
18834
+ }
18835
+ if (IDENTIFIER_START2.test(char)) {
18836
+ let value = "";
18837
+ while (i < sql.length && IDENTIFIER_PART.test(sql[i])) {
18838
+ value += sql[i];
18839
+ i++;
18840
+ }
18841
+ tokens.push({ value, kind: "identifier", quoted: false });
18842
+ continue;
18843
+ }
18844
+ if (char === "." || char === "," || char === "(" || char === ")" || char === ";") {
18845
+ tokens.push({ value: char, kind: "punctuation", quoted: false });
18846
+ i++;
18847
+ continue;
18848
+ }
18849
+ i++;
18850
+ }
18851
+ return tokens;
18852
+ }
18853
+ function isKeyword(token, keywords) {
18854
+ if (!token || token.kind !== "identifier" || token.quoted)
18855
+ return false;
18856
+ return keywords.has(token.value.toUpperCase());
18857
+ }
18858
+ function isPunctuation(token, value) {
18859
+ return token?.kind === "punctuation" && token.value === value;
18860
+ }
18861
+ function readQualifiedName(tokens, index) {
18862
+ const first = tokens[index];
18863
+ if (!first || first.kind !== "identifier")
18864
+ return null;
18865
+ const parts = [first.value];
18866
+ let cursor = index + 1;
18867
+ while (isPunctuation(tokens[cursor], ".") && tokens[cursor + 1]?.kind === "identifier") {
18868
+ parts.push(tokens[cursor + 1].value);
18869
+ cursor += 2;
18870
+ }
18871
+ return { parts, next: cursor };
18872
+ }
18873
+ function decodedVariants(value) {
18874
+ const decoded = value.replace(ANY_ESCAPE_SEQUENCE, (whole, _escape, long, short) => fromCodePointOrRaw(long ?? short, whole));
18875
+ return decoded === value ? [] : [decoded];
18876
+ }
18877
+ function fromCodePointOrRaw(hex, whole) {
18878
+ const codePoint = parseInt(hex, 16);
18879
+ return codePoint <= 1114111 ? String.fromCodePoint(codePoint) : whole;
18880
+ }
18881
+ function extractTableReferences(sql, options = {}) {
18882
+ const seen = new Set;
18883
+ const references = [];
18884
+ const record = (name) => {
18885
+ const key = name.toLowerCase();
18886
+ if (name.length === 0 || seen.has(key))
18887
+ return;
18888
+ seen.add(key);
18889
+ references.push(name);
18890
+ };
18891
+ const dialects = options.dialect ? [options.dialect] : ["postgresql", "mysql", undefined];
18892
+ for (const dialect of dialects) {
18893
+ for (const backslashEscapes of [false, true]) {
18894
+ collectReferences(tokenize(sql, dialect, backslashEscapes), record);
18895
+ }
18896
+ }
18897
+ return references;
18898
+ }
18899
+ function collectReferences(tokens, record) {
18900
+ const recordName = (parts) => {
18901
+ const bare = parts[parts.length - 1];
18902
+ record(bare);
18903
+ for (const variant of decodedVariants(bare))
18904
+ record(variant);
18905
+ if (parts.length > 1)
18906
+ record(parts.join("."));
18907
+ };
18908
+ let i = 0;
18909
+ while (i < tokens.length) {
18910
+ if (!isKeyword(tokens[i], TABLE_INTRODUCERS)) {
18911
+ i++;
18912
+ continue;
18913
+ }
18914
+ const introducer = tokens[i].value.toUpperCase();
18915
+ const parenMeansFunction = introducer === "FROM" || introducer === "JOIN";
18916
+ let cursor = i + 1;
18917
+ while (isKeyword(tokens[cursor], PRE_TABLE_NOISE))
18918
+ cursor++;
18919
+ while (parenMeansFunction && isPunctuation(tokens[cursor], "(") && !isKeyword(tokens[cursor + 1], SUBQUERY_OPENERS) && tokens[cursor + 1]?.kind === "identifier") {
18920
+ cursor++;
18921
+ }
18922
+ let expectTable = true;
18923
+ while (expectTable) {
18924
+ expectTable = false;
18925
+ const name = readQualifiedName(tokens, cursor);
18926
+ if (!name)
18927
+ break;
18928
+ const isFunctionCall = parenMeansFunction && isPunctuation(tokens[name.next], "(");
18929
+ if (!isFunctionCall)
18930
+ recordName(name.parts);
18931
+ cursor = name.next;
18932
+ if (isFunctionCall)
18933
+ break;
18934
+ if (isKeyword(tokens[cursor], AS_KEYWORD))
18935
+ cursor++;
18936
+ if (tokens[cursor]?.kind === "identifier" && !isKeyword(tokens[cursor], POST_TABLE_KEYWORDS)) {
18937
+ cursor++;
18938
+ }
18939
+ while (isPunctuation(tokens[cursor], "(")) {
18940
+ let depth = 0;
18941
+ do {
18942
+ if (isPunctuation(tokens[cursor], "("))
18943
+ depth++;
18944
+ else if (isPunctuation(tokens[cursor], ")"))
18945
+ depth--;
18946
+ cursor++;
18947
+ } while (depth > 0 && cursor < tokens.length);
18948
+ if (tokens[cursor]?.kind === "identifier" && !isKeyword(tokens[cursor], POST_TABLE_KEYWORDS))
18949
+ cursor++;
18950
+ }
18951
+ if (isPunctuation(tokens[cursor], ",")) {
18952
+ cursor++;
18953
+ expectTable = true;
18954
+ }
18955
+ }
18956
+ i = Math.max(cursor, i + 1);
18957
+ }
18958
+ let index = 0;
18959
+ while (index < tokens.length) {
18960
+ const token = tokens[index];
18961
+ if (!token || token.kind !== "identifier") {
18962
+ index++;
18963
+ continue;
18964
+ }
18965
+ const name = readQualifiedName(tokens, index);
18966
+ for (let part = 0;part < name.parts.length; part++) {
18967
+ const value = name.parts[part];
18968
+ const isQuoted = tokens[index + part * 2]?.quoted === true;
18969
+ if (isQuoted || !RESERVED_KEYWORDS.has(value.toUpperCase())) {
18970
+ record(value);
18971
+ for (const variant of decodedVariants(value))
18972
+ record(variant);
18973
+ }
18974
+ }
18975
+ if (name.parts.length > 1)
18976
+ record(name.parts.join("."));
18977
+ index = name.next;
18978
+ }
18979
+ }
18980
+ var TABLE_INTRODUCERS, PRE_TABLE_NOISE, SUBQUERY_OPENERS, AS_KEYWORD, POST_TABLE_KEYWORDS, RESERVED_KEYWORDS, IDENTIFIER_START2, IDENTIFIER_PART, ANY_ESCAPE_SEQUENCE;
18981
+ var init_sql_tables = __esm(() => {
18982
+ init_sql_lexical();
18983
+ TABLE_INTRODUCERS = new Set([
18984
+ "FROM",
18985
+ "JOIN",
18986
+ "INTO",
18987
+ "UPDATE",
18988
+ "TABLE",
18989
+ "TRUNCATE",
18990
+ "COPY",
18991
+ "USING",
18992
+ "STRAIGHT_JOIN"
18993
+ ]);
18994
+ PRE_TABLE_NOISE = new Set(["ONLY", "LATERAL", "TABLE"]);
18995
+ SUBQUERY_OPENERS = new Set(["SELECT", "WITH", "VALUES", "TABLE"]);
18996
+ AS_KEYWORD = new Set(["AS"]);
18997
+ POST_TABLE_KEYWORDS = new Set([
18998
+ "AS",
18999
+ "ON",
19000
+ "USING",
19001
+ "WHERE",
19002
+ "GROUP",
19003
+ "ORDER",
19004
+ "HAVING",
19005
+ "LIMIT",
19006
+ "OFFSET",
19007
+ "FETCH",
19008
+ "WINDOW",
19009
+ "UNION",
19010
+ "INTERSECT",
19011
+ "EXCEPT",
19012
+ "JOIN",
19013
+ "INNER",
19014
+ "LEFT",
19015
+ "RIGHT",
19016
+ "FULL",
19017
+ "OUTER",
19018
+ "CROSS",
19019
+ "NATURAL",
19020
+ "STRAIGHT_JOIN",
19021
+ "SET",
19022
+ "VALUES",
19023
+ "SELECT",
19024
+ "RETURNING",
19025
+ "FOR",
19026
+ "INTO",
19027
+ "PARTITION",
19028
+ "WITH",
19029
+ "TABLESAMPLE",
19030
+ "FORCE",
19031
+ "IGNORE",
19032
+ "USE"
19033
+ ]);
19034
+ RESERVED_KEYWORDS = new Set([
19035
+ "ALL",
19036
+ "AND",
19037
+ "AS",
19038
+ "ASC",
19039
+ "CASE",
19040
+ "CROSS",
19041
+ "DESC",
19042
+ "DISTINCT",
19043
+ "ELSE",
19044
+ "FALSE",
19045
+ "FOR",
19046
+ "FROM",
19047
+ "GROUP",
19048
+ "HAVING",
19049
+ "IN",
19050
+ "INNER",
19051
+ "INTO",
19052
+ "IS",
19053
+ "JOIN",
19054
+ "LEFT",
19055
+ "LIKE",
19056
+ "LIMIT",
19057
+ "NOT",
19058
+ "NULL",
19059
+ "ON",
19060
+ "OR",
19061
+ "ORDER",
19062
+ "OUTER",
19063
+ "RIGHT",
19064
+ "SELECT",
19065
+ "THEN",
19066
+ "TRUE",
19067
+ "UNION",
19068
+ "USING",
19069
+ "VALUES",
19070
+ "WHEN",
19071
+ "WHERE",
19072
+ "WITH"
19073
+ ]);
19074
+ IDENTIFIER_START2 = /[A-Za-z_\u0080-\uFFFF]/;
19075
+ IDENTIFIER_PART = /[A-Za-z0-9_$\u0080-\uFFFF]/;
19076
+ ANY_ESCAPE_SEQUENCE = /([^0-9a-fA-F+'"\s])(?:\+([0-9a-fA-F]{6})|([0-9a-fA-F]{4}))/g;
19077
+ });
19078
+
18421
19079
  // src/core/permission/sql-analysis.ts
18422
19080
  function normalizeSQL(sql) {
18423
19081
  return sql.replace(/--[^\n]*\n/g, `
@@ -19010,6 +19668,9 @@ var init_engine_hints = __esm(() => {
19010
19668
  });
19011
19669
 
19012
19670
  // src/core/audit/integration-helper.ts
19671
+ function sqlDialectFor(engine) {
19672
+ return SQL_AUDIT_DIALECTS.includes(engine) ? engine : null;
19673
+ }
19013
19674
  async function getAuditLogger(config, configPath, connectionName) {
19014
19675
  const storagePath = await resolveConfigStoragePath(configPath);
19015
19676
  const connName = connectionName || config.effectiveConnectionName || getGlobalConnectionName() || "default";
@@ -19051,6 +19712,8 @@ async function writeAuditEntryResult(config, commandName, options, outcome) {
19051
19712
  const logger = await getAuditLogger(config, options.config || ".dbcli", connectionName);
19052
19713
  const engine = config.connection?.system || "postgresql";
19053
19714
  const target = outcome.target || getOperationTarget(engine, commandName, options, outcome.sql);
19715
+ const auditDialect = sqlDialectFor(engine);
19716
+ const blacklistChecked = auditDialect ? extractTableReferences(outcome.sql ?? "", { dialect: auditDialect }) : [];
19054
19717
  let tier = outcome.sideEffectTier ?? getEngineCapability(engine, commandName).tier;
19055
19718
  if (options.dryRun || options.plan) {
19056
19719
  tier = "dry-run";
@@ -19075,6 +19738,7 @@ async function writeAuditEntryResult(config, commandName, options, outcome) {
19075
19738
  ...outcome.recovery_ref && { recovery_ref: outcome.recovery_ref },
19076
19739
  metadata: {
19077
19740
  ...outcome.metadata ?? {},
19741
+ ...blacklistChecked.length > 0 && { blacklist_checked: blacklistChecked },
19078
19742
  connection_name: connectionName,
19079
19743
  environment: config.effectiveEnvironment ?? null
19080
19744
  }
@@ -19087,15 +19751,17 @@ async function writeAuditEntryResult(config, commandName, options, outcome) {
19087
19751
  };
19088
19752
  }
19089
19753
  }
19090
- var _sessionIdService = null, _loggers, AuditRequiredError;
19754
+ var SQL_AUDIT_DIALECTS, _sessionIdService = null, _loggers, AuditRequiredError;
19091
19755
  var init_integration_helper = __esm(() => {
19092
19756
  init_logger2();
19093
19757
  init_session_id();
19094
19758
  init_config_binding();
19095
19759
  init_config();
19096
19760
  init_capabilities();
19761
+ init_sql_tables();
19097
19762
  init_redaction();
19098
19763
  init_engine_hints();
19764
+ SQL_AUDIT_DIALECTS = ["postgresql", "mysql", "mariadb"];
19099
19765
  _loggers = new Map;
19100
19766
  AuditRequiredError = class AuditRequiredError extends Error {
19101
19767
  constructor(detail) {
@@ -19170,73 +19836,6 @@ async function getAllTablesFromAdapter(adapter) {
19170
19836
  }
19171
19837
  var init_error_suggester = () => {};
19172
19838
 
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
19839
  // src/utils/bounded-parallel.ts
19241
19840
  async function mapWithConcurrency(items, limit, worker) {
19242
19841
  if (items.length === 0)
@@ -21322,7 +21921,11 @@ function markRedactedColumns(cols, collection, blacklist) {
21322
21921
  const raw = (blacklist.columns ?? {})[collection];
21323
21922
  if (!raw || raw.length === 0)
21324
21923
  return cols;
21325
- const { patterns } = compilePatterns(raw);
21924
+ const { patterns, rejected } = compilePatterns(raw);
21925
+ if (rejected.length > 0) {
21926
+ const detail = rejected.map((r) => `'${r.raw}' (${r.reason})`).join(", ");
21927
+ throw new Error(`blacklist.columns for '${collection}' has entries this matcher cannot read: ${detail}`);
21928
+ }
21326
21929
  if (patterns.length === 0)
21327
21930
  return cols;
21328
21931
  return cols.map((c) => matchAny(c.name, patterns) ? { ...c, redacted: true } : c);
@@ -21770,6 +22373,7 @@ var init_schema = __esm(() => {
21770
22373
  init_validation();
21771
22374
  init_connection_selector();
21772
22375
  init_error_suggester();
22376
+ init_path_matcher();
21773
22377
  init_config_path();
21774
22378
  ALLOWED_FORMATS2 = ["table", "json"];
21775
22379
  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 +22478,6 @@ async function openInBrowser(target) {
21874
22478
  }
21875
22479
  var init_opener = () => {};
21876
22480
 
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
22481
  // src/core/limits.ts
22227
22482
  var DEFAULT_QUERY_ONLY_LIMIT = 1000;
22228
22483
 
@@ -22402,7 +22657,10 @@ function maskMongoRowsForCollections(rows, collections, blacklist) {
22402
22657
  const prefixKey = `\x00${scope.collection}@${scope.prefix}`;
22403
22658
  return maskMongoRows(atTopLevel, prefixKey, {
22404
22659
  ...blacklist,
22405
- columns: { ...columns, [prefixKey]: rules.map((rule) => `${scope.prefix}.${rule}`) }
22660
+ columns: {
22661
+ ...columns,
22662
+ [prefixKey]: rules.map((rule) => `${escapeGlob(scope.prefix)}.${rule}`)
22663
+ }
22406
22664
  });
22407
22665
  }, rows);
22408
22666
  }
@@ -22411,10 +22669,14 @@ function maskMongoRows(rows, collection, blacklist) {
22411
22669
  const raw = columns[collection] ?? findCaseInsensitive(columns, collection);
22412
22670
  if (!raw || raw.length === 0)
22413
22671
  return rows;
22414
- const { patterns } = compilePatterns(raw);
22672
+ const { patterns, rejected } = compilePatterns(raw);
22673
+ if (rejected.length > 0) {
22674
+ const detail = rejected.map((r) => `'${r.raw}' (${r.reason})`).join(", ");
22675
+ throw new BlacklistError(`blacklist.columns for '${collection}' has entries this matcher cannot read: ${detail}`, collection, "READ");
22676
+ }
22415
22677
  if (patterns.length === 0)
22416
22678
  return rows;
22417
- const idAffected = patterns.some((p) => p.segments.length === 1 && p.segments[0] === "_id" && !p.wildcardTail);
22679
+ const idAffected = patterns.some((p) => p.segments.length === 1 && !p.wildcardTail && globMatches(p.segments[0], "_id"));
22418
22680
  if (idAffected) {
22419
22681
  console.error(`[blacklist] collection '${collection}' blacklists '_id'; read paths still expose _id to preserve document references.`);
22420
22682
  }
@@ -22456,7 +22718,12 @@ function findCaseInsensitive(columns, name) {
22456
22718
  return;
22457
22719
  }
22458
22720
  var REDACTED2 = "[REDACTED]";
22459
- var init_field_masker = () => {};
22721
+ var init_field_masker = __esm(() => {
22722
+ init_blacklist();
22723
+ init_path_matcher();
22724
+ init_glob();
22725
+ init_glob();
22726
+ });
22460
22727
 
22461
22728
  // src/core/mongo/collection-references.ts
22462
22729
  function recordTarget(value, found) {
@@ -24224,6 +24491,7 @@ var init_query = __esm(() => {
24224
24491
  init_opener();
24225
24492
  init_query_executor();
24226
24493
  init_config();
24494
+ init_blacklist_manager();
24227
24495
  init_blacklist_validator();
24228
24496
  init_config_path();
24229
24497
  init_validation();
@@ -26161,6 +26429,7 @@ async function qMongoBranch(snippet, prepared, options, config) {
26161
26429
  }
26162
26430
  var init_q_mongo = __esm(() => {
26163
26431
  init_adapters();
26432
+ init_blacklist_manager();
26164
26433
  init_blacklist_validator();
26165
26434
  init_field_masker();
26166
26435
  init_collection_references();
@@ -26524,6 +26793,7 @@ var init_q = __esm(() => {
26524
26793
  init_adapters();
26525
26794
  init_config();
26526
26795
  init_config_path();
26796
+ init_blacklist_manager();
26527
26797
  init_blacklist_validator();
26528
26798
  init_blacklist();
26529
26799
  init_permission_guard();
@@ -26792,6 +27062,365 @@ function previewDelete(collection, filter) {
26792
27062
  }
26793
27063
  var INDENT = 2;
26794
27064
 
27065
+ // src/utils/where-parser.ts
27066
+ function parseWhereClause(whereClause) {
27067
+ if (!whereClause || whereClause.trim() === "") {
27068
+ throw new Error("WHERE clause cannot be empty");
27069
+ }
27070
+ const conditions = {};
27071
+ const andParts = whereClause.split(/\s+AND\s+/i);
27072
+ for (const part of andParts) {
27073
+ const match = part.match(/^(\w+)\s*=\s*(.+)$/);
27074
+ if (!match) {
27075
+ throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
27076
+ }
27077
+ const column = match[1];
27078
+ const valueStr = match[2];
27079
+ if (valueStr === undefined || column === undefined) {
27080
+ throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
27081
+ }
27082
+ const trimmed = valueStr.trim();
27083
+ const isFullyQuoted = trimmed.length >= 2 && (trimmed.startsWith("'") && trimmed.endsWith("'") || trimmed.startsWith('"') && trimmed.endsWith('"'));
27084
+ if (!isFullyQuoted && /\s+AND\s*$/i.test(trimmed)) {
27085
+ throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
27086
+ }
27087
+ const stripped = isFullyQuoted ? trimmed.slice(1, -1) : trimmed;
27088
+ let value = stripped;
27089
+ if (stripped !== "" && !isNaN(Number(stripped))) {
27090
+ value = Number(stripped);
27091
+ }
27092
+ if (stripped === "true")
27093
+ value = true;
27094
+ else if (stripped === "false")
27095
+ value = false;
27096
+ else if (stripped === "null")
27097
+ value = null;
27098
+ conditions[column] = value;
27099
+ }
27100
+ return conditions;
27101
+ }
27102
+
27103
+ // src/core/mongo/dml-plan.ts
27104
+ function permissionSql(operation) {
27105
+ if (operation === "insert")
27106
+ return "INSERT INTO dummy";
27107
+ if (operation === "update")
27108
+ return "UPDATE dummy";
27109
+ return "DELETE FROM dummy";
27110
+ }
27111
+ function broadOperation(operation) {
27112
+ if (operation === "insert")
27113
+ return "INSERT";
27114
+ if (operation === "update")
27115
+ return "UPDATE";
27116
+ return "DELETE";
27117
+ }
27118
+ function pushFactor2(factors, code, severity, message) {
27119
+ if (factors.some((f) => f.code === code && f.message === message))
27120
+ return;
27121
+ factors.push({ code, severity, message });
27122
+ }
27123
+ function decide2(factors) {
27124
+ if (factors.some((f) => f.severity === "block"))
27125
+ return "BLOCK";
27126
+ if (factors.some((f) => f.severity === "warn"))
27127
+ return "WARN";
27128
+ return "ALLOW";
27129
+ }
27130
+ function classifyMongoUpdate(setDoc) {
27131
+ const fields = new Set;
27132
+ const tierFactors = [];
27133
+ let hasBlock = false;
27134
+ const hasAnyOperator = Object.keys(setDoc).some((k) => k.startsWith("$"));
27135
+ if (!hasAnyOperator) {
27136
+ for (const k of Object.keys(setDoc))
27137
+ fields.add(k);
27138
+ return { fields: Array.from(fields), tierFactors, hasBlock };
27139
+ }
27140
+ const seenTiers = new Map;
27141
+ for (const [op, payload] of Object.entries(setDoc)) {
27142
+ if (!op.startsWith("$")) {
27143
+ fields.add(op);
27144
+ continue;
27145
+ }
27146
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) {
27147
+ for (const k of Object.keys(payload))
27148
+ fields.add(k);
27149
+ }
27150
+ const tier = MONGO_OPERATOR_TIER[op];
27151
+ if (tier === undefined) {
27152
+ hasBlock = true;
27153
+ tierFactors.push({
27154
+ code: "mongo_unknown_operator",
27155
+ severity: "block",
27156
+ message: `Update uses unknown operator '${op}'. Reject by default; add to tier table if intentional.`
27157
+ });
27158
+ continue;
27159
+ }
27160
+ if (tier === "BLOCK") {
27161
+ hasBlock = true;
27162
+ tierFactors.push({
27163
+ code: "mongo_unknown_operator",
27164
+ severity: "block",
27165
+ message: `Update uses '${op}' which executes server-side code. Operation rejected.`
27166
+ });
27167
+ continue;
27168
+ }
27169
+ if (tier === "SAFE")
27170
+ continue;
27171
+ const bucket = seenTiers.get(tier) ?? [];
27172
+ bucket.push(op);
27173
+ seenTiers.set(tier, bucket);
27174
+ }
27175
+ for (const [tier, ops] of seenTiers) {
27176
+ if (tier === "RENAME") {
27177
+ tierFactors.push({
27178
+ code: "mongo_rename_operator",
27179
+ severity: "warn",
27180
+ message: `Update uses ${ops.join(", ")}; a renamed field keeps its value under a name the read mask does not know.`
27181
+ });
27182
+ } else if (tier === "ARITHMETIC") {
27183
+ tierFactors.push({
27184
+ code: "mongo_arithmetic_operator",
27185
+ severity: "warn",
27186
+ message: `Update uses ${ops.join(", ")}; numeric mutation may compound silently.`
27187
+ });
27188
+ } else if (tier === "ARRAY") {
27189
+ tierFactors.push({
27190
+ code: "mongo_array_operator",
27191
+ severity: "warn",
27192
+ message: `Update uses ${ops.join(", ")}; array mutation can grow unboundedly without a size guard.`
27193
+ });
27194
+ } else if (tier === "BITWISE") {
27195
+ tierFactors.push({
27196
+ code: "mongo_bitwise_operator",
27197
+ severity: "warn",
27198
+ message: `Update uses ${ops.join(", ")}; bitwise updates skip type promotion checks.`
27199
+ });
27200
+ }
27201
+ }
27202
+ return { fields: Array.from(fields), tierFactors, hasBlock };
27203
+ }
27204
+ function hasIdEquality(where) {
27205
+ if (!("_id" in where))
27206
+ return false;
27207
+ const value = where._id;
27208
+ if (value === null || value === undefined)
27209
+ return false;
27210
+ if (typeof value === "object")
27211
+ return false;
27212
+ return true;
27213
+ }
27214
+ function hasNonIdEquality(where) {
27215
+ for (const [key, value] of Object.entries(where)) {
27216
+ if (key === "_id")
27217
+ continue;
27218
+ if (key.startsWith("$"))
27219
+ continue;
27220
+ if (value === null)
27221
+ continue;
27222
+ if (typeof value === "object")
27223
+ continue;
27224
+ return true;
27225
+ }
27226
+ return false;
27227
+ }
27228
+ function hasBroadFilter(where) {
27229
+ for (const [key, value] of Object.entries(where)) {
27230
+ if (MONGO_BROAD_FILTER_OPERATORS.has(key))
27231
+ return true;
27232
+ if (value && typeof value === "object" && !Array.isArray(value)) {
27233
+ for (const inner of Object.keys(value)) {
27234
+ if (MONGO_BROAD_FILTER_OPERATORS.has(inner))
27235
+ return true;
27236
+ }
27237
+ }
27238
+ }
27239
+ return false;
27240
+ }
27241
+ function applyPermission(operation, context, factors) {
27242
+ const permResult = checkPermission(permissionSql(operation), context.permission);
27243
+ if (!permResult.allowed) {
27244
+ pushFactor2(factors, "permission_denied", "block", permResult.reason);
27245
+ }
27246
+ }
27247
+ function applyTableBlacklist(target, context, factors) {
27248
+ const blacklisted = (context.blacklist.tables ?? []).map((t2) => t2.toLowerCase());
27249
+ if (blacklisted.includes(target.toLowerCase())) {
27250
+ pushFactor2(factors, "table_blacklisted", "block", `Target collection ${target} is blacklisted.`);
27251
+ }
27252
+ }
27253
+ function applyColumnBlacklist(target, fields, context, factors) {
27254
+ const columns = context.blacklist.columns ?? {};
27255
+ const lower = target.toLowerCase();
27256
+ let raw = [];
27257
+ for (const [t2, cols] of Object.entries(columns)) {
27258
+ if (t2.toLowerCase() === lower) {
27259
+ raw = cols;
27260
+ break;
27261
+ }
27262
+ }
27263
+ if (raw.length === 0)
27264
+ return;
27265
+ const { patterns, rejected } = compilePatterns(raw);
27266
+ if (rejected.length > 0) {
27267
+ const detail = rejected.map((r) => `'${r.raw}' (${r.reason})`).join(", ");
27268
+ pushFactor2(factors, "blacklist_unreadable", "block", `blacklist.columns for '${target}' has entries this matcher cannot read: ${detail}`);
27269
+ return;
27270
+ }
27271
+ if (patterns.length === 0)
27272
+ return;
27273
+ for (const field of fields) {
27274
+ if (matchAny(field, patterns)) {
27275
+ pushFactor2(factors, "blacklisted_column", "block", `MongoDB write would touch blacklisted path ${target}.${field}.`);
27276
+ }
27277
+ }
27278
+ }
27279
+ function applySchemaCoverage(target, context, factors) {
27280
+ const schemaTables = Object.keys(context.schema);
27281
+ if (schemaTables.length === 0) {
27282
+ pushFactor2(factors, "schema_cache_missing", "warn", "Schema cache is missing for the selected connection.");
27283
+ return;
27284
+ }
27285
+ const known = schemaTables.some((t2) => t2.toLowerCase() === target.toLowerCase());
27286
+ if (!known) {
27287
+ pushFactor2(factors, "schema_table_unknown", "warn", `Target collection ${target} is missing from schema cache.`);
27288
+ }
27289
+ }
27290
+ function buildRecommendations2(factors) {
27291
+ const out = new Set;
27292
+ const codes = new Set(factors.map((f) => f.code));
27293
+ if (codes.has("nonsql_filter_empty")) {
27294
+ out.add("Add an _id equality filter before executing this MongoDB write.");
27295
+ }
27296
+ if (codes.has("nonsql_missing_id")) {
27297
+ out.add("Prefer an _id equality filter before executing this MongoDB write.");
27298
+ }
27299
+ if (codes.has("nonsql_filter_broad")) {
27300
+ out.add("Narrow the filter to an _id equality or a tightly bounded condition.");
27301
+ }
27302
+ if (codes.has("nonsql_unsupported_operator")) {
27303
+ out.add("Restrict the update document to $set / $unset for planner-safe writes.");
27304
+ }
27305
+ if (codes.has("permission_denied")) {
27306
+ out.add("Switch to a connection with sufficient permission only if the operation is intended.");
27307
+ }
27308
+ if (codes.has("table_blacklisted") || codes.has("blacklisted_column")) {
27309
+ out.add("Review blacklist rules before accessing sensitive data.");
27310
+ }
27311
+ if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
27312
+ out.add("Refresh schema cache for the target collection before executing.");
27313
+ }
27314
+ out.add("Use --dry-run on the actual write command.");
27315
+ return Array.from(out);
27316
+ }
27317
+ function buildSuggestedCommands2(target, factors) {
27318
+ const codes = new Set(factors.map((f) => f.code));
27319
+ if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
27320
+ return [`dbcli schema ${target} --format json`];
27321
+ }
27322
+ return [];
27323
+ }
27324
+ function flattenInsertPaths(data, prefix = "") {
27325
+ const out = [];
27326
+ for (const [k, v] of Object.entries(data)) {
27327
+ const path2 = prefix === "" ? k : `${prefix}.${k}`;
27328
+ out.push(path2);
27329
+ if (v && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date)) {
27330
+ const ctor = v.constructor?.name;
27331
+ if (ctor === "Object") {
27332
+ out.push(...flattenInsertPaths(v, path2));
27333
+ }
27334
+ }
27335
+ }
27336
+ return out;
27337
+ }
27338
+ var MONGO_OPERATOR_TIER, MONGO_BROAD_FILTER_OPERATORS, analyzeMongoDmlRisk = (intent, context) => {
27339
+ const factors = [];
27340
+ applyPermission(intent.operation, context, factors);
27341
+ applyTableBlacklist(intent.target, context, factors);
27342
+ if (intent.operation === "insert") {
27343
+ applyColumnBlacklist(intent.target, flattenInsertPaths(intent.data), context, factors);
27344
+ } else if (intent.operation === "update") {
27345
+ const cls = classifyMongoUpdate(intent.set);
27346
+ for (const f of cls.tierFactors)
27347
+ pushFactor2(factors, f.code, f.severity, f.message);
27348
+ applyColumnBlacklist(intent.target, cls.fields, context, factors);
27349
+ const where = intent.where ?? {};
27350
+ if (Object.keys(where).length === 0) {
27351
+ pushFactor2(factors, "nonsql_filter_empty", "block", "MongoDB update filter is empty and would match every document.");
27352
+ } else if (!hasIdEquality(where)) {
27353
+ if (hasBroadFilter(where)) {
27354
+ pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB update filter uses a broad operator and may match multiple documents.");
27355
+ } else if (hasNonIdEquality(where)) {
27356
+ pushFactor2(factors, "nonsql_missing_id", "warn", "MongoDB update filter does not use _id and may match multiple documents.");
27357
+ } else {
27358
+ pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB update filter is not an _id equality and may match multiple documents.");
27359
+ }
27360
+ }
27361
+ } else {
27362
+ const where = intent.where ?? {};
27363
+ if (Object.keys(where).length === 0) {
27364
+ pushFactor2(factors, "nonsql_filter_empty", "block", "MongoDB delete filter is empty and would match every document.");
27365
+ } else if (!hasIdEquality(where)) {
27366
+ if (hasBroadFilter(where)) {
27367
+ pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB delete filter uses a broad operator and may match multiple documents.");
27368
+ } else if (hasNonIdEquality(where)) {
27369
+ pushFactor2(factors, "nonsql_missing_id", "warn", "MongoDB delete filter does not use _id and may match multiple documents.");
27370
+ } else {
27371
+ pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB delete filter is not an _id equality and may match multiple documents.");
27372
+ }
27373
+ }
27374
+ }
27375
+ applySchemaCoverage(intent.target, context, factors);
27376
+ return {
27377
+ decision: decide2(factors),
27378
+ operation: broadOperation(intent.operation),
27379
+ targetTables: [intent.target],
27380
+ riskFactors: factors,
27381
+ recommendations: buildRecommendations2(factors),
27382
+ suggestedCommands: buildSuggestedCommands2(intent.target, factors)
27383
+ };
27384
+ };
27385
+ var init_dml_plan = __esm(() => {
27386
+ init_path_matcher();
27387
+ init_permission_guard();
27388
+ MONGO_OPERATOR_TIER = {
27389
+ $set: "SAFE",
27390
+ $unset: "SAFE",
27391
+ $rename: "RENAME",
27392
+ $inc: "ARITHMETIC",
27393
+ $mul: "ARITHMETIC",
27394
+ $min: "ARITHMETIC",
27395
+ $max: "ARITHMETIC",
27396
+ $currentDate: "ARITHMETIC",
27397
+ $push: "ARRAY",
27398
+ $pull: "ARRAY",
27399
+ $pullAll: "ARRAY",
27400
+ $pop: "ARRAY",
27401
+ $addToSet: "ARRAY",
27402
+ $bit: "BITWISE",
27403
+ $where: "BLOCK"
27404
+ };
27405
+ MONGO_BROAD_FILTER_OPERATORS = new Set([
27406
+ "$regex",
27407
+ "$in",
27408
+ "$nin",
27409
+ "$gt",
27410
+ "$gte",
27411
+ "$lt",
27412
+ "$lte",
27413
+ "$ne",
27414
+ "$exists",
27415
+ "$or",
27416
+ "$and",
27417
+ "$nor",
27418
+ "$not",
27419
+ "$expr",
27420
+ "$text"
27421
+ ]);
27422
+ });
27423
+
26795
27424
  // src/core/dml-plan-sql.ts
26796
27425
  function assertIdentifier(value, role) {
26797
27426
  if (!value || !IDENTIFIER_RE.test(value)) {
@@ -26851,359 +27480,6 @@ var init_dml_plan_sql = __esm(() => {
26851
27480
  IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
26852
27481
  });
26853
27482
 
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
27483
  // src/core/redis/dml-plan.ts
27208
27484
  function permissionSql2(operation) {
27209
27485
  if (operation === "insert")
@@ -27665,7 +27941,7 @@ async function insertCommand(table, options, command) {
27665
27941
  const blacklistManager = new BlacklistManager(config);
27666
27942
  const blacklistValidator = new BlacklistValidator(blacklistManager);
27667
27943
  blacklistValidator.checkTableBlacklist("INSERT", table, []);
27668
- blacklistValidator.checkColumnBlacklistOnWrite(table, Object.keys(data), "INSERT");
27944
+ blacklistValidator.checkColumnBlacklistOnWrite(table, flattenInsertPaths(data), "INSERT");
27669
27945
  const preview = `SET ${table} ... (Redis Insert)`;
27670
27946
  if (options.dryRun) {
27671
27947
  const output = {
@@ -27730,7 +28006,7 @@ async function insertCommand(table, options, command) {
27730
28006
  const blacklistManager = new BlacklistManager(config);
27731
28007
  const blacklistValidator = new BlacklistValidator(blacklistManager);
27732
28008
  blacklistValidator.checkTableBlacklist("INSERT", table, []);
27733
- blacklistValidator.checkColumnBlacklistOnWrite(table, Object.keys(data), "INSERT");
28009
+ blacklistValidator.checkColumnBlacklistOnWrite(table, flattenInsertPaths(data), "INSERT");
27734
28010
  const preview = previewInsert(table, data);
27735
28011
  if (options.dryRun) {
27736
28012
  const output = {
@@ -27848,9 +28124,11 @@ var init_insert = __esm(() => {
27848
28124
  init_mutation_outcome();
27849
28125
  init_config();
27850
28126
  init_permission_guard();
28127
+ init_blacklist_manager();
27851
28128
  init_blacklist_validator();
27852
28129
  init_blacklist();
27853
28130
  init_config_path();
28131
+ init_dml_plan();
27854
28132
  init_dml_plan4();
27855
28133
  init_integration_helper();
27856
28134
  });
@@ -28008,7 +28286,11 @@ async function updateCommand(table, options, command) {
28008
28286
  if (hasOperator) {
28009
28287
  const operators = updateDoc;
28010
28288
  for (const [op, payload] of Object.entries(operators)) {
28011
- if ((op === "$set" || op === "$unset") && payload && typeof payload === "object" && !Array.isArray(payload)) {
28289
+ if (!op.startsWith("$")) {
28290
+ writtenFields.add(op);
28291
+ continue;
28292
+ }
28293
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) {
28012
28294
  for (const k of Object.keys(payload))
28013
28295
  writtenFields.add(k);
28014
28296
  }
@@ -28169,6 +28451,7 @@ var init_update = __esm(() => {
28169
28451
  init_mutation_outcome();
28170
28452
  init_config();
28171
28453
  init_permission_guard();
28454
+ init_blacklist_manager();
28172
28455
  init_blacklist_validator();
28173
28456
  init_blacklist();
28174
28457
  init_config_path();
@@ -28468,6 +28751,7 @@ var init_delete = __esm(() => {
28468
28751
  init_mutation_outcome();
28469
28752
  init_config();
28470
28753
  init_permission_guard();
28754
+ init_blacklist_manager();
28471
28755
  init_blacklist_validator();
28472
28756
  init_blacklist();
28473
28757
  init_config_path();
@@ -28911,6 +29195,7 @@ var init_export = __esm(() => {
28911
29195
  init_config();
28912
29196
  init_prompts();
28913
29197
  init_config_path();
29198
+ init_blacklist_manager();
28914
29199
  init_blacklist_validator();
28915
29200
  init_applied_limit();
28916
29201
  init_integration_helper();
@@ -30147,6 +30432,7 @@ async function gatherContext(workspaceRoot, configPath, options = {}) {
30147
30432
  }
30148
30433
  var init_context = __esm(() => {
30149
30434
  init_config();
30435
+ init_blacklist_manager();
30150
30436
  init_loader();
30151
30437
  init_snippet_paths();
30152
30438
  init_semantic();
@@ -31363,11 +31649,8 @@ function auditBlacklistPatterns(cfg) {
31363
31649
  }
31364
31650
  return { warnings };
31365
31651
  }
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));
31652
+ function isValidTableNameForSystem(name, _system) {
31653
+ return isValidTableName(name);
31371
31654
  }
31372
31655
  function isValidTableName(name) {
31373
31656
  return VALID_TABLE_NAME.test(name);
@@ -31533,16 +31816,16 @@ function resolveBlacklistConfigPath(options, command) {
31533
31816
  }
31534
31817
  return DEFAULT_CONFIG_PATH;
31535
31818
  }
31536
- var DEFAULT_CONFIG_PATH = ".dbcli", VALID_TABLE_NAME, VALID_COLUMN_NAME, GLOB_CHARS, blacklistCommand, tableCmd, columnCmd;
31819
+ var DEFAULT_CONFIG_PATH = ".dbcli", VALID_TABLE_NAME, VALID_COLUMN_NAME, blacklistCommand, tableCmd, columnCmd;
31537
31820
  var init_blacklist2 = __esm(() => {
31538
31821
  init_config_v2();
31539
31822
  init_esm();
31540
31823
  init_message_loader();
31541
31824
  init_config();
31825
+ init_path_matcher();
31542
31826
  init_validation();
31543
- VALID_TABLE_NAME = /^[a-zA-Z0-9_.*?:@[\]-]+$/;
31544
- VALID_COLUMN_NAME = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
31545
- GLOB_CHARS = /[*?[\]]/;
31827
+ VALID_TABLE_NAME = /^[a-zA-Z0-9_.*?:@[\]\\-]+$/;
31828
+ VALID_COLUMN_NAME = /^[a-zA-Z0-9_*?[\]\\-]+$/;
31546
31829
  blacklistCommand = new Command("blacklist").description(t("blacklist.description"));
31547
31830
  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
31831
  try {
@@ -31718,6 +32001,7 @@ var init_check = __esm(() => {
31718
32001
  init_adapters();
31719
32002
  init_config();
31720
32003
  init_health_checker();
32004
+ init_blacklist_manager();
31721
32005
  init_size_category();
31722
32006
  init_validation();
31723
32007
  init_connection_selector();
@@ -33594,9 +33878,8 @@ var init_proposals = __esm(() => {
33594
33878
  });
33595
33879
 
33596
33880
  // src/core/orm-drift/compare.ts
33597
- function globToRegex2(glob) {
33598
- const escaped = glob.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
33599
- return new RegExp(`^${escaped}$`);
33881
+ function starOnlyGlob(glob) {
33882
+ return glob.replace(/[?[\]\\]/g, "\\$&");
33600
33883
  }
33601
33884
  function tableMap(schema, defaultSchema) {
33602
33885
  const tables = new Map;
@@ -33630,7 +33913,7 @@ function compareNormalized(orm, db, opts) {
33630
33913
  const tableKeys = new Set([...ormTables.keys(), ...dbTables.keys()]);
33631
33914
  const extraDefaultIgnore = opts.extraDefaultIgnore ?? [];
33632
33915
  const defaultIgnore = [...DEFAULT_IGNORE, ...extraDefaultIgnore];
33633
- const ignorePatterns = opts.ignore.map(globToRegex2);
33916
+ const ignorePatterns = opts.ignore;
33634
33917
  const targetLabel = opts.targetLabel ?? "database";
33635
33918
  const entries = [];
33636
33919
  for (const tableKey of tableKeys) {
@@ -33641,7 +33924,7 @@ function compareNormalized(orm, db, opts) {
33641
33924
  continue;
33642
33925
  const table = qualifiedTableName(normalizedTable.identity);
33643
33926
  const isDefaultIgnored = defaultIgnore.includes(normalizedTable.identity.table);
33644
- if (isDefaultIgnored || ignorePatterns.some((pattern) => pattern.test(table))) {
33927
+ if (isDefaultIgnored || ignorePatterns.some((pattern) => globMatches(starOnlyGlob(pattern), table))) {
33645
33928
  entries.push(entryWithProposals({
33646
33929
  category: "unmanaged",
33647
33930
  severity: "info",
@@ -33823,6 +34106,7 @@ function compareIndexes(ormIndexes, dbIndexes, tableIdentity, table, ormSource,
33823
34106
  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
34107
  var init_compare = __esm(() => {
33825
34108
  init_normalized_schema();
34109
+ init_glob();
33826
34110
  init_proposals();
33827
34111
  DEFAULT_IGNORE = ["_prisma_migrations"];
33828
34112
  });
@@ -34719,7 +35003,7 @@ function shouldEmbedRecent(opts) {
34719
35003
  return opts.forAgent === true || opts.format === "json";
34720
35004
  }
34721
35005
  function briefifyForRecent(entry) {
34722
- const phase = entry.metadata?.es_shell_phase;
35006
+ const phase = entry.metadata?.shell_phase;
34723
35007
  return {
34724
35008
  id: entry.id,
34725
35009
  ts: entry.ts,
@@ -35426,6 +35710,7 @@ var init_collector2 = __esm(() => {
35426
35710
  init_saved_queries();
35427
35711
  init_select_snippets();
35428
35712
  init_run_diagnostic();
35713
+ init_blacklist_manager();
35429
35714
  init_blacklist_validator();
35430
35715
  init_section_map();
35431
35716
  init_types7();
@@ -37431,7 +37716,7 @@ function parseTailN(raw) {
37431
37716
  return requested;
37432
37717
  }
37433
37718
  function briefify(entry) {
37434
- const phase = entry.metadata?.es_shell_phase;
37719
+ const phase = entry.metadata?.shell_phase;
37435
37720
  return {
37436
37721
  ts: entry.ts,
37437
37722
  command: entry.command,
@@ -37486,8 +37771,8 @@ function renderTailAllTable(envelopes) {
37486
37771
  }
37487
37772
  function briefifyShow(entry) {
37488
37773
  const { metadata, redacted_query, ...rest } = entry;
37489
- const phase = metadata?.es_shell_phase;
37490
- return phase === undefined ? rest : { ...rest, metadata: { es_shell_phase: phase } };
37774
+ const phase = metadata?.shell_phase;
37775
+ return phase === undefined ? rest : { ...rest, metadata: { shell_phase: phase } };
37491
37776
  }
37492
37777
  function renderEntryTable(entry) {
37493
37778
  const lines = [];
@@ -42015,6 +42300,7 @@ var init_snapshot = __esm(() => {
42015
42300
  init_config();
42016
42301
  init_config_path();
42017
42302
  init_validation();
42303
+ init_blacklist_manager();
42018
42304
  init_blacklist_validator();
42019
42305
  init_query_executor();
42020
42306
  init_permission_guard();
@@ -42683,6 +42969,7 @@ var init_assert = __esm(() => {
42683
42969
  init_config();
42684
42970
  init_config_path();
42685
42971
  init_validation();
42972
+ init_blacklist_manager();
42686
42973
  init_blacklist_validator();
42687
42974
  init_query_executor();
42688
42975
  init_saved_queries();
@@ -44861,6 +45148,7 @@ var init_verify2 = __esm(() => {
44861
45148
  init_adapters();
44862
45149
  init_config();
44863
45150
  init_config_path();
45151
+ init_blacklist_manager();
44864
45152
  init_blacklist_validator();
44865
45153
  init_query_executor();
44866
45154
  init_query_risk_analyzer();
@@ -45612,15 +45900,17 @@ class ReplEngine {
45612
45900
  adapter;
45613
45901
  context;
45614
45902
  writeGate;
45903
+ auditSink;
45615
45904
  state;
45616
45905
  buffer;
45617
45906
  history;
45618
45907
  formatter;
45619
45908
  config;
45620
- constructor(adapter, context, historyPath, config = null, writeGate = null) {
45909
+ constructor(adapter, context, historyPath, config = null, writeGate = null, auditSink = null) {
45621
45910
  this.adapter = adapter;
45622
45911
  this.context = context;
45623
45912
  this.writeGate = writeGate;
45913
+ this.auditSink = auditSink;
45624
45914
  this.state = { format: "table", timing: false, connected: true, noLimit: false };
45625
45915
  this.buffer = new MultilineBuffer;
45626
45916
  this.history = new HistoryManager(historyPath);
@@ -45752,10 +46042,11 @@ class ReplEngine {
45752
46042
  } else {
45753
46043
  const permResult = checkPermission(sql, this.context.permission, SQL_DIALECTS.find((dialect) => dialect === this.context.system));
45754
46044
  if (!permResult.allowed) {
46045
+ await this.auditSink?.({ phase: "outcome", success: false, statement: sql });
45755
46046
  return {
45756
46047
  action: "continue",
45757
46048
  output: import_picocolors3.default.red(t_vars("shell.error_permission", {
45758
- required: permResult.classification.type === "UNKNOWN" ? "admin" : "read-write",
46049
+ required: permResult.requiredPermission ?? "admin",
45759
46050
  current: this.context.permission
45760
46051
  }))
45761
46052
  };
@@ -45771,18 +46062,22 @@ class ReplEngine {
45771
46062
  } catch (error) {
45772
46063
  if (!(error instanceof BlacklistError))
45773
46064
  throw error;
46065
+ await this.auditSink?.({ phase: "outcome", success: false, statement: sql });
45774
46066
  return {
45775
46067
  action: "continue",
45776
46068
  output: import_picocolors3.default.red(t_vars("shell.error_blacklisted", { table: error.message }))
45777
46069
  };
45778
46070
  }
45779
46071
  }
45780
- if (this.writeGate && !await this.writeGate(sql))
46072
+ if (this.writeGate && !await this.writeGate(sql)) {
46073
+ await this.auditSink?.({ phase: "outcome", success: false, statement: sql });
45781
46074
  return { action: "continue" };
46075
+ }
45782
46076
  return this.runStatement(sql, blacklistValidator, referencedTables);
45783
46077
  }
45784
46078
  async runStatement(sql, blacklistValidator, referencedTables) {
45785
46079
  const startTime = Date.now();
46080
+ await this.auditSink?.({ phase: "attempt", success: true, statement: sql });
45786
46081
  try {
45787
46082
  const result = await this.adapter.execute(sql, undefined, {
45788
46083
  noLimit: this.state.noLimit
@@ -45808,6 +46103,7 @@ class ReplEngine {
45808
46103
  ` + import_picocolors3.default.dim(t_vars("shell.timing_display", { ms: String(elapsed) }));
45809
46104
  }
45810
46105
  this.state = { ...this.state, connected: true };
46106
+ await this.auditSink?.({ phase: "outcome", success: true, statement: sql });
45811
46107
  return { action: "continue", output };
45812
46108
  } catch (error) {
45813
46109
  if (this.isConnectionError(error) && this.state.connected) {
@@ -45819,12 +46115,14 @@ class ReplEngine {
45819
46115
  console.error(import_picocolors3.default.green(t("shell.error_reconnect_success")));
45820
46116
  return this.runStatement(sql, blacklistValidator, referencedTables);
45821
46117
  } catch (reconnectError) {
46118
+ await this.auditSink?.({ phase: "outcome", success: false, statement: sql });
45822
46119
  return {
45823
46120
  action: "continue",
45824
46121
  output: import_picocolors3.default.red(t_vars("shell.error_reconnect_failed", { message: reconnectError.message }))
45825
46122
  };
45826
46123
  }
45827
46124
  }
46125
+ await this.auditSink?.({ phase: "outcome", success: false, statement: sql });
45828
46126
  return {
45829
46127
  action: "continue",
45830
46128
  output: import_picocolors3.default.red(t_vars("shell.error_sql_failed", { message: error.message }))
@@ -45859,6 +46157,7 @@ var init_repl_engine = __esm(() => {
45859
46157
  init_query_result_formatter();
45860
46158
  init_message_loader();
45861
46159
  init_sql_tables();
46160
+ init_blacklist_manager();
45862
46161
  init_blacklist_validator();
45863
46162
  init_blacklist();
45864
46163
  import_picocolors3 = __toESM(require_picocolors(), 1);
@@ -46400,7 +46699,7 @@ async function executeEsBlock(block2, session) {
46400
46699
  target: record4.target,
46401
46700
  sql: record4.statement,
46402
46701
  sideEffectTier: record4.tierOverride,
46403
- metadata: { es_shell_phase: record4.phase }
46702
+ metadata: { shell_phase: record4.phase }
46404
46703
  })
46405
46704
  });
46406
46705
  console.log(JSON.stringify(res, null, 2));
@@ -46600,6 +46899,25 @@ var init_shell_write_gate = __esm(() => {
46600
46899
  init_write_gate_guard();
46601
46900
  });
46602
46901
 
46902
+ // src/commands/shell-audit-sink.ts
46903
+ var exports_shell_audit_sink = {};
46904
+ __export(exports_shell_audit_sink, {
46905
+ createShellAuditSink: () => createShellAuditSink
46906
+ });
46907
+ function createShellAuditSink(options) {
46908
+ const write = options.write ?? writeAuditEntryResult;
46909
+ return async (record4) => {
46910
+ return await write(options.config, "shell", { config: options.configPath }, {
46911
+ success: record4.success,
46912
+ sql: record4.statement,
46913
+ metadata: { shell_phase: record4.phase }
46914
+ });
46915
+ };
46916
+ }
46917
+ var init_shell_audit_sink = __esm(() => {
46918
+ init_integration_helper();
46919
+ });
46920
+
46603
46921
  // src/commands/shell.ts
46604
46922
  var exports_shell = {};
46605
46923
  __export(exports_shell, {
@@ -46717,7 +47035,11 @@ async function runShell(options, configPath) {
46717
47035
  dialect,
46718
47036
  ask: asker.ask
46719
47037
  }) : null;
46720
- const engine = new ReplEngine(adapter, context, HISTORY_PATH, config, writeGate);
47038
+ const auditSink = (await Promise.resolve().then(() => (init_shell_audit_sink(), exports_shell_audit_sink))).createShellAuditSink({
47039
+ config,
47040
+ configPath
47041
+ });
47042
+ const engine = new ReplEngine(adapter, context, HISTORY_PATH, config, writeGate, auditSink);
46721
47043
  const complete = createCompleter(context);
46722
47044
  console.error(import_picocolors5.default.bold(t_vars("shell.welcome", {
46723
47045
  system: config.connection.system,
@@ -47549,6 +47871,7 @@ var init_migrate = __esm(() => {
47549
47871
  init_ddl2();
47550
47872
  init_ddl_executor();
47551
47873
  init_mutation_confirm();
47874
+ init_blacklist_manager();
47552
47875
  init_config_path();
47553
47876
  migrateCommand = new Command("migrate").description(t("migrate.description"));
47554
47877
  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 +48174,9 @@ function envVarNameFor(connName, field) {
47851
48174
  const slug = connName.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
47852
48175
  return `DBCLI_${slug}_${field.toUpperCase()}`;
47853
48176
  }
47854
- var init_config_v2_mutations = () => {};
48177
+ var init_config_v2_mutations = __esm(() => {
48178
+ init_validation();
48179
+ });
47855
48180
 
47856
48181
  // src/core/env-file-writer.ts
47857
48182
  import { chmod as chmod2, mkdir as mkdir18, readFile as readFile9, writeFile as writeFile9 } from "fs/promises";
@@ -52231,6 +52556,9 @@ var init_data_access = __esm(() => {
52231
52556
  });
52232
52557
 
52233
52558
  // src/core/orm-drift/change-set.ts
52559
+ function starOnlyGlob2(glob) {
52560
+ return glob.replace(/[?[\]\\]/g, "\\$&");
52561
+ }
52234
52562
  function normalizeProposedChanges(input) {
52235
52563
  validateInput(input);
52236
52564
  const gaps = [
@@ -52288,7 +52616,7 @@ function tableMap2(schema, defaultSchema, side) {
52288
52616
  return tables;
52289
52617
  }
52290
52618
  function ignoredTableKeys(input, declared, baseline, gaps) {
52291
- const patterns = [...DEFAULT_IGNORE2, ...input.ignore ?? []].map(globToRegex3);
52619
+ const patterns = [...DEFAULT_IGNORE2, ...input.ignore ?? []];
52292
52620
  const ignored = new Set;
52293
52621
  for (const [side, tables] of [
52294
52622
  ["declared", declared],
@@ -52296,7 +52624,7 @@ function ignoredTableKeys(input, declared, baseline, gaps) {
52296
52624
  ]) {
52297
52625
  for (const [key, resolved] of tables) {
52298
52626
  const name = qualifiedTableName(resolved.identity);
52299
- if (!patterns.some((pattern) => pattern.test(name) || pattern.test(resolved.identity.table)))
52627
+ if (!patterns.some((pattern) => globMatches(starOnlyGlob2(pattern), name) || globMatches(starOnlyGlob2(pattern), resolved.identity.table)))
52300
52628
  continue;
52301
52629
  if (ignored.has(key))
52302
52630
  continue;
@@ -52474,10 +52802,6 @@ function unparsedGaps(schema, side) {
52474
52802
  reason: entry.reason
52475
52803
  }));
52476
52804
  }
52477
- function globToRegex3(glob) {
52478
- const escaped = glob.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
52479
- return new RegExp(`^${escaped}$`);
52480
- }
52481
52805
  function gapOrder(left, right) {
52482
52806
  return codePointOrder5(left.side, right.side) || codePointOrder5(left.kind, right.kind) || codePointOrder5(left.location, right.location) || codePointOrder5(left.reason, right.reason);
52483
52807
  }
@@ -52493,6 +52817,7 @@ function codePointOrder5(left, right) {
52493
52817
  }
52494
52818
  var DEFAULT_IGNORE2, NormalizedChangeSetError;
52495
52819
  var init_change_set = __esm(() => {
52820
+ init_glob();
52496
52821
  DEFAULT_IGNORE2 = ["_prisma_migrations"];
52497
52822
  NormalizedChangeSetError = class NormalizedChangeSetError extends Error {
52498
52823
  constructor(message) {