@moduna/qcp 0.1.3-alpha.2 → 0.1.3-alpha.3

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.
Files changed (4) hide show
  1. package/README.md +67 -11
  2. package/dist/index.js +727 -54
  3. package/dist/qcp.js +727 -54
  4. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2373,7 +2373,7 @@ var package_default;
2373
2373
  var init_package = __esm(() => {
2374
2374
  package_default = {
2375
2375
  name: "@moduna/qcp",
2376
- version: "0.1.3-alpha.2",
2376
+ version: "0.1.3-alpha.3",
2377
2377
  description: "AI-powered CLI for querying PostgreSQL in natural language",
2378
2378
  type: "module",
2379
2379
  bin: {
@@ -80117,6 +80117,11 @@ function validateSql(sql) {
80117
80117
  }
80118
80118
  return report;
80119
80119
  }
80120
+ const readOnlyViolation = findNonReadOnlyStatement(stmt);
80121
+ if (readOnlyViolation) {
80122
+ report.errors.push(`Dangerous operation rejected: ${readOnlyViolation} is not permitted inside this query. ` + `qcp is read-only — only SELECT and WITH are allowed.`);
80123
+ return report;
80124
+ }
80120
80125
  report.allowedStatement = true;
80121
80126
  report.readOnly = true;
80122
80127
  let processedSql = trimmedSql.replace(/;\s*$/, "");
@@ -80156,6 +80161,439 @@ function extractLimitValue(stmt) {
80156
80161
  return null;
80157
80162
  }
80158
80163
  }
80164
+ function findNonReadOnlyStatement(stmt) {
80165
+ if (stmt.type === "select" || stmt.type === "values")
80166
+ return null;
80167
+ if (stmt.type === "union" || stmt.type === "union all") {
80168
+ return findNonReadOnlyStatement(stmt.left) ?? findNonReadOnlyStatement(stmt.right);
80169
+ }
80170
+ if (stmt.type === "with") {
80171
+ for (const binding of stmt.bind) {
80172
+ const bindingViolation = findNonReadOnlyBinding(binding.statement);
80173
+ if (bindingViolation)
80174
+ return bindingViolation;
80175
+ }
80176
+ return findNonReadOnlyBinding(stmt.in);
80177
+ }
80178
+ if (stmt.type === "with recursive")
80179
+ return "WITH RECURSIVE";
80180
+ return DANGEROUS_STATEMENT_TYPES[stmt.type] ?? stmt.type.toUpperCase();
80181
+ }
80182
+ function findNonReadOnlyBinding(binding) {
80183
+ if (binding.type === "select" || binding.type === "union" || binding.type === "union all" || binding.type === "values" || binding.type === "with") {
80184
+ return findNonReadOnlyStatement(binding);
80185
+ }
80186
+ return DANGEROUS_STATEMENT_TYPES[binding.type] ?? binding.type.toUpperCase();
80187
+ }
80188
+ function enforceTenantIsolation(sql, schema, context) {
80189
+ const report = {
80190
+ safe: false,
80191
+ errors: [],
80192
+ warnings: [],
80193
+ processedSql: sql.trim(),
80194
+ injectedPredicates: [],
80195
+ scopedTables: []
80196
+ };
80197
+ const contextResult = securityRequestContextSchema.safeParse(context);
80198
+ if (!contextResult.success) {
80199
+ report.errors.push("Trusted tenant context is required.");
80200
+ return report;
80201
+ }
80202
+ let statements;
80203
+ try {
80204
+ statements = import_pgsql_ast_parser.parse(sql.trim(), { locationTracking: false });
80205
+ } catch {
80206
+ report.errors.push("SQL parsing failed during tenant isolation.");
80207
+ return report;
80208
+ }
80209
+ if (statements.length !== 1) {
80210
+ report.errors.push("Tenant isolation requires exactly one SQL statement.");
80211
+ return report;
80212
+ }
80213
+ const statement = statements[0];
80214
+ const readOnlyViolation = findNonReadOnlyStatement(statement);
80215
+ if (readOnlyViolation) {
80216
+ report.errors.push(`Tenant isolation rejected non-read-only statement: ${readOnlyViolation}.`);
80217
+ return report;
80218
+ }
80219
+ const state = {
80220
+ schema,
80221
+ context: contextResult.data,
80222
+ tableIndex: buildTableIndex(schema),
80223
+ errors: report.errors,
80224
+ warnings: report.warnings,
80225
+ injectedPredicates: report.injectedPredicates,
80226
+ scopedTables: report.scopedTables
80227
+ };
80228
+ const scopedStatement = scopeStatement(statement, state, new Set);
80229
+ if (!scopedStatement || state.errors.length > 0) {
80230
+ return report;
80231
+ }
80232
+ report.processedSql = import_pgsql_ast_parser.toSql.statement(scopedStatement);
80233
+ report.safe = true;
80234
+ return report;
80235
+ }
80236
+ function buildTableIndex(schema) {
80237
+ const index = new Map;
80238
+ for (const table of schema.tables) {
80239
+ const fullName = tableKey(table.schema, table.name);
80240
+ index.set(fullName, [table]);
80241
+ const byName = index.get(table.name.toLowerCase()) ?? [];
80242
+ byName.push(table);
80243
+ index.set(table.name.toLowerCase(), byName);
80244
+ }
80245
+ return index;
80246
+ }
80247
+ function scopeStatement(statement, state, cteNames) {
80248
+ if (statement.type === "with") {
80249
+ return scopeWithStatement(statement, state, cteNames);
80250
+ }
80251
+ if (statement.type === "select" || statement.type === "union" || statement.type === "union all" || statement.type === "values") {
80252
+ return scopeSelectStatement(statement, state, cteNames);
80253
+ }
80254
+ state.errors.push(`Tenant isolation supports SELECT and WITH only, not ${statement.type}.`);
80255
+ return null;
80256
+ }
80257
+ function scopeWithStatement(statement, state, parentCteNames) {
80258
+ const cteNames = new Set(parentCteNames);
80259
+ for (const binding of statement.bind) {
80260
+ cteNames.add(binding.alias.name.toLowerCase());
80261
+ }
80262
+ const bindings = statement.bind.map((binding) => {
80263
+ const scoped = scopeWithBinding(binding.statement, state, cteNames);
80264
+ return scoped ? { ...binding, statement: scoped } : binding;
80265
+ });
80266
+ const scopedIn = scopeWithBinding(statement.in, state, cteNames);
80267
+ if (!scopedIn)
80268
+ return null;
80269
+ return {
80270
+ ...statement,
80271
+ bind: bindings,
80272
+ in: scopedIn
80273
+ };
80274
+ }
80275
+ function scopeWithBinding(binding, state, cteNames) {
80276
+ if (binding.type === "select" || binding.type === "union" || binding.type === "union all" || binding.type === "values" || binding.type === "with") {
80277
+ return scopeSelectStatement(binding, state, cteNames);
80278
+ }
80279
+ state.errors.push(`Tenant isolation rejected non-read-only WITH binding: ${binding.type}.`);
80280
+ return null;
80281
+ }
80282
+ function scopeSelectStatement(statement, state, cteNames) {
80283
+ switch (statement.type) {
80284
+ case "with":
80285
+ return scopeWithStatement(statement, state, cteNames);
80286
+ case "with recursive":
80287
+ state.errors.push("WITH RECURSIVE is not supported by tenant isolation.");
80288
+ return null;
80289
+ case "union":
80290
+ case "union all":
80291
+ return scopeUnionStatement(statement, state, cteNames);
80292
+ case "values":
80293
+ return statement;
80294
+ case "select":
80295
+ return scopeSelectFromStatement(statement, state, cteNames);
80296
+ }
80297
+ }
80298
+ function scopeUnionStatement(statement, state, cteNames) {
80299
+ const left = scopeSelectStatement(statement.left, state, cteNames);
80300
+ const right = scopeSelectStatement(statement.right, state, cteNames);
80301
+ if (!left || !right)
80302
+ return null;
80303
+ return {
80304
+ ...statement,
80305
+ left,
80306
+ right
80307
+ };
80308
+ }
80309
+ function scopeSelectFromStatement(statement, state, cteNames) {
80310
+ if (containsNestedSelect(statement)) {
80311
+ state.errors.push("Nested subqueries are not supported by tenant isolation.");
80312
+ return null;
80313
+ }
80314
+ const from = statement.from?.map((item) => scopeFrom(item, state, cteNames));
80315
+ if (state.errors.length > 0)
80316
+ return null;
80317
+ const scopes = (from ?? []).flatMap((item) => collectTableScopes(item));
80318
+ let where = statement.where;
80319
+ for (const scope of scopes) {
80320
+ const conflict = findConflictingScopePredicate(where, scope, state.context);
80321
+ if (conflict) {
80322
+ state.errors.push(conflict);
80323
+ return null;
80324
+ }
80325
+ const predicates = buildScopePredicates(scope, state.context);
80326
+ for (const predicate of predicates) {
80327
+ where = andExpr(where, predicate.expr);
80328
+ state.injectedPredicates.push(predicate.label);
80329
+ }
80330
+ state.scopedTables.push(scope.tableId);
80331
+ }
80332
+ return {
80333
+ ...statement,
80334
+ from,
80335
+ where
80336
+ };
80337
+ }
80338
+ function scopeFrom(from, state, cteNames) {
80339
+ assertSafeJoin(from, state);
80340
+ if (from.type === "call") {
80341
+ state.errors.push("Table functions are not supported by tenant isolation.");
80342
+ return from;
80343
+ }
80344
+ if (from.lateral) {
80345
+ state.errors.push("LATERAL queries are not supported by tenant isolation.");
80346
+ return from;
80347
+ }
80348
+ if (from.type === "statement") {
80349
+ const scopedStatement = scopeSelectStatement(from.statement, state, cteNames);
80350
+ return scopedStatement ? {
80351
+ ...from,
80352
+ statement: scopedStatement
80353
+ } : from;
80354
+ }
80355
+ if (isCteReference(from, cteNames))
80356
+ return from;
80357
+ const scope = resolveTableScope(from, state);
80358
+ return scope ? {
80359
+ ...from,
80360
+ __qcpScope: scope
80361
+ } : from;
80362
+ }
80363
+ function collectTableScopes(from) {
80364
+ const scope = from.__qcpScope;
80365
+ return scope ? [scope] : [];
80366
+ }
80367
+ function assertSafeJoin(from, state) {
80368
+ const joinType = from.join?.type;
80369
+ if (!joinType)
80370
+ return;
80371
+ if (joinType === "INNER JOIN" || joinType === "CROSS JOIN")
80372
+ return;
80373
+ state.errors.push(`${joinType} is not supported by tenant isolation.`);
80374
+ }
80375
+ function isCteReference(from, cteNames) {
80376
+ return !from.name.schema && cteNames.has(from.name.name.toLowerCase());
80377
+ }
80378
+ function resolveTableScope(from, state) {
80379
+ const table = resolveSchemaTable(from.name, state);
80380
+ if (!table)
80381
+ return null;
80382
+ const columns = new Set(table.columns.map((column) => column.name.toLowerCase()));
80383
+ const tenantColumn = TENANT_COLUMNS.find((column) => columns.has(column));
80384
+ const userColumn = USER_COLUMNS.find((column) => columns.has(column));
80385
+ if (!tenantColumn && !userColumn) {
80386
+ state.errors.push(`Table ${formatTableId(table)} has no supported tenant or user scope column.`);
80387
+ return null;
80388
+ }
80389
+ return {
80390
+ alias: from.name.alias ?? from.name.name,
80391
+ tableId: formatTableId(table),
80392
+ tenantColumn,
80393
+ userColumn
80394
+ };
80395
+ }
80396
+ function resolveSchemaTable(name, state) {
80397
+ if (name.schema && SYSTEM_SCHEMAS.has(name.schema.toLowerCase())) {
80398
+ return null;
80399
+ }
80400
+ const key = name.schema ? tableKey(name.schema, name.name) : name.name.toLowerCase();
80401
+ const matches = state.tableIndex.get(key) ?? [];
80402
+ if (matches.length === 1)
80403
+ return matches[0];
80404
+ if (matches.length > 1) {
80405
+ state.errors.push(`Table ${name.name} is ambiguous across schemas; qualify it explicitly.`);
80406
+ return null;
80407
+ }
80408
+ state.errors.push(`Unknown table rejected by tenant isolation: ${name.name}.`);
80409
+ return null;
80410
+ }
80411
+ function buildScopePredicates(scope, context) {
80412
+ const predicates = [];
80413
+ if (scope.tenantColumn) {
80414
+ predicates.push({
80415
+ expr: equalityExpr(scope.alias, scope.tenantColumn, context.tenantId),
80416
+ label: `${scope.tableId}.${scope.tenantColumn} = [tenantId]`
80417
+ });
80418
+ }
80419
+ if (scope.userColumn) {
80420
+ predicates.push({
80421
+ expr: equalityExpr(scope.alias, scope.userColumn, context.userId),
80422
+ label: `${scope.tableId}.${scope.userColumn} = [userId]`
80423
+ });
80424
+ }
80425
+ return predicates;
80426
+ }
80427
+ function equalityExpr(table, column, value2) {
80428
+ return {
80429
+ type: "binary",
80430
+ op: "=",
80431
+ left: {
80432
+ type: "ref",
80433
+ table: { name: table },
80434
+ name: column
80435
+ },
80436
+ right: {
80437
+ type: "string",
80438
+ value: value2
80439
+ }
80440
+ };
80441
+ }
80442
+ function andExpr(left, right) {
80443
+ if (!left)
80444
+ return right;
80445
+ return {
80446
+ type: "binary",
80447
+ op: "AND",
80448
+ left,
80449
+ right
80450
+ };
80451
+ }
80452
+ function findConflictingScopePredicate(expr, scope, context) {
80453
+ if (!expr)
80454
+ return null;
80455
+ if (expr.type === "binary") {
80456
+ const directConflict = scopePredicateConflict(expr, scope, context);
80457
+ if (directConflict)
80458
+ return directConflict;
80459
+ return findConflictingScopePredicate(expr.left, scope, context) ?? findConflictingScopePredicate(expr.right, scope, context);
80460
+ }
80461
+ if (expr.type === "unary") {
80462
+ return findConflictingScopePredicate(expr.operand, scope, context);
80463
+ }
80464
+ if (expr.type === "ternary") {
80465
+ return findConflictingScopePredicate(expr.value, scope, context) ?? findConflictingScopePredicate(expr.lo, scope, context) ?? findConflictingScopePredicate(expr.hi, scope, context);
80466
+ }
80467
+ if (expr.type === "case") {
80468
+ for (const when of expr.whens) {
80469
+ const conflict = findConflictingScopePredicate(when.when, scope, context) ?? findConflictingScopePredicate(when.value, scope, context);
80470
+ if (conflict)
80471
+ return conflict;
80472
+ }
80473
+ return findConflictingScopePredicate(expr.else, scope, context);
80474
+ }
80475
+ if (expr.type === "list" || expr.type === "array") {
80476
+ for (const item of expr.expressions) {
80477
+ const conflict = findConflictingScopePredicate(item, scope, context);
80478
+ if (conflict)
80479
+ return conflict;
80480
+ }
80481
+ }
80482
+ if (expr.type === "cast") {
80483
+ return findConflictingScopePredicate(expr.operand, scope, context);
80484
+ }
80485
+ return null;
80486
+ }
80487
+ function scopePredicateConflict(expr, scope, context) {
80488
+ if (expr.op !== "=")
80489
+ return null;
80490
+ const conflict = refLiteralConflict(expr.left, expr.right, scope, context) ?? refLiteralConflict(expr.right, expr.left, scope, context);
80491
+ return conflict ? `Query attempted to override trusted ${conflict} scope.` : null;
80492
+ }
80493
+ function refLiteralConflict(ref, literal2, scope, context) {
80494
+ if (ref.type !== "ref")
80495
+ return null;
80496
+ const tableName = ref.table?.name.toLowerCase();
80497
+ if (tableName && tableName !== scope.alias.toLowerCase())
80498
+ return null;
80499
+ const literalValue = literalToString(literal2);
80500
+ if (literalValue === null)
80501
+ return null;
80502
+ if (scope.tenantColumn && ref.name.toLowerCase() === scope.tenantColumn && literalValue !== context.tenantId) {
80503
+ return "tenant";
80504
+ }
80505
+ if (scope.userColumn && ref.name.toLowerCase() === scope.userColumn && literalValue !== context.userId) {
80506
+ return "user";
80507
+ }
80508
+ return null;
80509
+ }
80510
+ function literalToString(expr) {
80511
+ if (expr.type === "string")
80512
+ return expr.value;
80513
+ if (expr.type === "integer" || expr.type === "numeric") {
80514
+ return String(expr.value);
80515
+ }
80516
+ if (expr.type === "cast")
80517
+ return literalToString(expr.operand);
80518
+ return null;
80519
+ }
80520
+ function containsNestedSelect(statement) {
80521
+ const expressions = [
80522
+ ...statement.columns?.map((column) => column.expr) ?? [],
80523
+ statement.where,
80524
+ statement.having,
80525
+ ...statement.groupBy ?? [],
80526
+ ...statement.orderBy?.map((orderBy) => orderBy.by) ?? []
80527
+ ].filter((expr) => !!expr);
80528
+ return expressions.some(exprContainsSelect);
80529
+ }
80530
+ function exprContainsSelect(expr) {
80531
+ if (expr.type === "select" || expr.type === "with" || expr.type === "union" || expr.type === "union all" || expr.type === "values" || expr.type === "array select") {
80532
+ return true;
80533
+ }
80534
+ if (expr.type === "binary") {
80535
+ return exprContainsSelect(expr.left) || exprContainsSelect(expr.right);
80536
+ }
80537
+ if (expr.type === "unary")
80538
+ return exprContainsSelect(expr.operand);
80539
+ if (expr.type === "ternary") {
80540
+ return exprContainsSelect(expr.value) || exprContainsSelect(expr.lo) || exprContainsSelect(expr.hi);
80541
+ }
80542
+ if (expr.type === "cast")
80543
+ return exprContainsSelect(expr.operand);
80544
+ if (expr.type === "case") {
80545
+ return expr.whens.some((when) => exprContainsSelect(when.when) || exprContainsSelect(when.value)) || (expr.else ? exprContainsSelect(expr.else) : false);
80546
+ }
80547
+ if (expr.type === "call")
80548
+ return expr.args.some(exprContainsSelect);
80549
+ if (expr.type === "list" || expr.type === "array") {
80550
+ return expr.expressions.some(exprContainsSelect);
80551
+ }
80552
+ if (expr.type === "member")
80553
+ return exprContainsSelect(expr.operand);
80554
+ if (expr.type === "arrayIndex") {
80555
+ return exprContainsSelect(expr.array) || exprContainsSelect(expr.index);
80556
+ }
80557
+ if (expr.type === "overlay") {
80558
+ return exprContainsSelect(expr.value) || exprContainsSelect(expr.placing) || exprContainsSelect(expr.from) || (expr.for ? exprContainsSelect(expr.for) : false);
80559
+ }
80560
+ if (expr.type === "substring") {
80561
+ return exprContainsSelect(expr.value) || (expr.from ? exprContainsSelect(expr.from) : false) || (expr.for ? exprContainsSelect(expr.for) : false);
80562
+ }
80563
+ return false;
80564
+ }
80565
+ function tableKey(schema, name) {
80566
+ return `${schema.toLowerCase()}.${name.toLowerCase()}`;
80567
+ }
80568
+ function formatTableId(table) {
80569
+ return table.schema === "public" ? table.name : `${table.schema}.${table.name}`;
80570
+ }
80571
+ function sanitizeSensitiveData(value2) {
80572
+ return sanitizeUnknown(value2);
80573
+ }
80574
+ function sanitizeUnknown(value2) {
80575
+ if (typeof value2 === "string")
80576
+ return sanitizeString(value2);
80577
+ if (Array.isArray(value2))
80578
+ return value2.map((item) => sanitizeUnknown(item));
80579
+ if (isPlainRecord(value2)) {
80580
+ const sanitized = {};
80581
+ for (const [key, item] of Object.entries(value2)) {
80582
+ sanitized[key] = sanitizeUnknown(item);
80583
+ }
80584
+ return sanitized;
80585
+ }
80586
+ return value2;
80587
+ }
80588
+ function sanitizeString(value2) {
80589
+ return SENSITIVE_STRING_PATTERNS.reduce((current, [pattern, replacement]) => current.replace(pattern, replacement), value2);
80590
+ }
80591
+ function isPlainRecord(value2) {
80592
+ return typeof value2 === "object" && value2 !== null && (Object.getPrototypeOf(value2) === Object.prototype || Object.getPrototypeOf(value2) === null);
80593
+ }
80594
+ function sanitizeDatabaseError(_error) {
80595
+ return "Database query failed. The request was not completed.";
80596
+ }
80159
80597
  function detectSensitiveTables(sql, patterns) {
80160
80598
  const lower = sql.toLowerCase();
80161
80599
  const found = patterns.filter((p) => lower.includes(p.toLowerCase()));
@@ -80178,8 +80616,12 @@ function getApprovalReasons(sql, _report, sensitivePatterns, estimatedRows) {
80178
80616
  }
80179
80617
  return reasons;
80180
80618
  }
80181
- var import_pgsql_ast_parser, ALLOWED_STATEMENT_TYPES, DANGEROUS_STATEMENT_TYPES, MAX_LIMIT = 100, EXPLAIN_REGEX;
80619
+ function requiresSensitiveApproval(sql, report, sensitivePatterns) {
80620
+ return getApprovalReasons(sql, report, sensitivePatterns).some((reason) => reason.type === "sensitive_table");
80621
+ }
80622
+ var import_pgsql_ast_parser, ALLOWED_STATEMENT_TYPES, DANGEROUS_STATEMENT_TYPES, MAX_LIMIT = 100, TENANT_COLUMNS, USER_COLUMNS, SYSTEM_SCHEMAS, securityRequestContextSchema, EXPLAIN_REGEX, SENSITIVE_STRING_PATTERNS;
80182
80623
  var init_safety = __esm(() => {
80624
+ init_zod();
80183
80625
  import_pgsql_ast_parser = __toESM(require_pgsql_ast_parser(), 1);
80184
80626
  ALLOWED_STATEMENT_TYPES = new Set(["select", "with"]);
80185
80627
  DANGEROUS_STATEMENT_TYPES = {
@@ -80205,7 +80647,38 @@ var init_safety = __esm(() => {
80205
80647
  copy: "COPY",
80206
80648
  do: "DO (procedural block)"
80207
80649
  };
80650
+ TENANT_COLUMNS = [
80651
+ "organization_id",
80652
+ "tenant_id",
80653
+ "org_id",
80654
+ "workspace_id",
80655
+ "account_id"
80656
+ ];
80657
+ USER_COLUMNS = ["user_id", "owner_id"];
80658
+ SYSTEM_SCHEMAS = new Set(["information_schema", "pg_catalog"]);
80659
+ securityRequestContextSchema = exports_external.object({
80660
+ tenantId: exports_external.string().min(1),
80661
+ userId: exports_external.string().min(1)
80662
+ });
80208
80663
  EXPLAIN_REGEX = /^\s*EXPLAIN\s*(\([^)]*\))?\s*/i;
80664
+ SENSITIVE_STRING_PATTERNS = [
80665
+ [/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, "[REDACTED_EMAIL]"],
80666
+ [
80667
+ /\b(?:\+?1[\s.-]?)?(?:\(?\d{3}\)?[\s.-]?)\d{3}[\s.-]?\d{4}\b/g,
80668
+ "[REDACTED_PHONE]"
80669
+ ],
80670
+ [/\b\d{3}-\d{2}-\d{4}\b/g, "[REDACTED_SSN]"],
80671
+ [
80672
+ /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g,
80673
+ "[REDACTED_TOKEN]"
80674
+ ],
80675
+ [/\bBearer\s+[A-Za-z0-9._~+/=-]{16,}\b/gi, "Bearer [REDACTED_TOKEN]"],
80676
+ [
80677
+ /\b(api[_-]?key|token|secret|password)(["'\s:=]+)([A-Za-z0-9._~+/=-]{8,})\b/gi,
80678
+ "$1$2[REDACTED_SECRET]"
80679
+ ],
80680
+ [/\b[A-Za-z0-9_-]{32,}\b/g, "[REDACTED_SECRET]"]
80681
+ ];
80209
80682
  });
80210
80683
 
80211
80684
  // node_modules/@mastra/core/dist/chunk-TBHKQLPL.js
@@ -285476,6 +285949,7 @@ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:f
285476
285949
  function createPrismaTools(options2) {
285477
285950
  const queryExecutor = options2.queryExecutor ?? executeQuery;
285478
285951
  const explainExecutor = options2.explainExecutor ?? explainQuery;
285952
+ const sensitiveTablePatterns = [...options2.sensitiveTablePatterns ?? []];
285479
285953
  return {
285480
285954
  qcp_validate_sql: createTool({
285481
285955
  id: "qcp_validate_sql",
@@ -285498,21 +285972,37 @@ function createPrismaTools(options2) {
285498
285972
  qcp_execute_read_sql: createTool({
285499
285973
  id: "qcp_execute_read_sql",
285500
285974
  description: "Execute a SQL query only after qcp AST validation succeeds. Rejected SQL is returned as a structured safety error.",
285975
+ strict: true,
285501
285976
  inputSchema: exports_external.object({
285502
285977
  sql: exports_external.string().min(1)
285503
285978
  }),
285979
+ requestContextSchema: securityRequestContextSchema,
285504
285980
  outputSchema: exports_external.discriminatedUnion("ok", [
285505
285981
  exports_external.object({
285506
285982
  ok: exports_external.literal(true),
285507
285983
  safety: safetyReportSchema,
285508
- result: queryResultSchema
285984
+ isolation: tenantIsolationReportSchema,
285985
+ result: queryResultSchema,
285986
+ approvalReasons: exports_external.array(approvalReasonSchema)
285509
285987
  }),
285510
285988
  exports_external.object({
285511
285989
  ok: exports_external.literal(false),
285512
285990
  safety: safetyReportSchema,
285513
- error: exports_external.string()
285991
+ isolation: tenantIsolationReportSchema.optional(),
285992
+ error: exports_external.string(),
285993
+ approvalReasons: exports_external.array(approvalReasonSchema)
285514
285994
  })
285515
285995
  ]),
285996
+ requireApproval: async ({ sql }, context2) => shouldRequirePrismaToolApproval({
285997
+ sql,
285998
+ databaseUrl: options2.databaseUrl,
285999
+ schema: options2.schema,
286000
+ sensitiveTablePatterns,
286001
+ explainExecutor,
286002
+ requestContext: context2?.requestContext
286003
+ }),
286004
+ toModelOutput: sanitizedToolModelOutput,
286005
+ transform: secureToolTransform(),
285516
286006
  mcp: {
285517
286007
  annotations: {
285518
286008
  title: "Execute Read SQL",
@@ -285522,38 +286012,48 @@ function createPrismaTools(options2) {
285522
286012
  openWorldHint: false
285523
286013
  }
285524
286014
  },
285525
- execute: async ({ sql }) => {
285526
- const safety = validateSql(sql);
285527
- if (!safety.safe) {
285528
- return {
285529
- ok: false,
285530
- safety,
285531
- error: safety.errors[0] ?? "SQL rejected by qcp safety policy."
285532
- };
285533
- }
285534
- const result = await queryExecutor(options2.databaseUrl, safety.processedSql);
285535
- return { ok: true, safety, result };
285536
- }
286015
+ execute: async ({ sql }, context2) => executeSecurePrismaReadQuery({
286016
+ databaseUrl: options2.databaseUrl,
286017
+ schema: options2.schema,
286018
+ queryExecutor,
286019
+ sensitiveTablePatterns
286020
+ }, sql, context2)
285537
286021
  }),
285538
286022
  qcp_explain_read_sql: createTool({
285539
286023
  id: "qcp_explain_read_sql",
285540
286024
  description: "Run EXPLAIN for a SQL query only after qcp AST validation succeeds.",
286025
+ strict: true,
285541
286026
  inputSchema: exports_external.object({
285542
286027
  sql: exports_external.string().min(1)
285543
286028
  }),
286029
+ requestContextSchema: securityRequestContextSchema,
285544
286030
  outputSchema: exports_external.discriminatedUnion("ok", [
285545
286031
  exports_external.object({
285546
286032
  ok: exports_external.literal(true),
285547
286033
  safety: safetyReportSchema,
286034
+ isolation: tenantIsolationReportSchema,
285548
286035
  plan: exports_external.string(),
285549
- estimatedRows: exports_external.number()
286036
+ estimatedRows: exports_external.number(),
286037
+ approvalReasons: exports_external.array(approvalReasonSchema)
285550
286038
  }),
285551
286039
  exports_external.object({
285552
286040
  ok: exports_external.literal(false),
285553
286041
  safety: safetyReportSchema,
285554
- error: exports_external.string()
286042
+ isolation: tenantIsolationReportSchema.optional(),
286043
+ error: exports_external.string(),
286044
+ approvalReasons: exports_external.array(approvalReasonSchema)
285555
286045
  })
285556
286046
  ]),
286047
+ requireApproval: async ({ sql }, context2) => shouldRequirePrismaToolApproval({
286048
+ sql,
286049
+ databaseUrl: options2.databaseUrl,
286050
+ schema: options2.schema,
286051
+ sensitiveTablePatterns,
286052
+ explainExecutor,
286053
+ requestContext: context2?.requestContext
286054
+ }),
286055
+ toModelOutput: sanitizedToolModelOutput,
286056
+ transform: secureToolTransform(),
285557
286057
  mcp: {
285558
286058
  annotations: {
285559
286059
  title: "Explain Read SQL",
@@ -285563,23 +286063,12 @@ function createPrismaTools(options2) {
285563
286063
  openWorldHint: false
285564
286064
  }
285565
286065
  },
285566
- execute: async ({ sql }) => {
285567
- const safety = validateSql(sql);
285568
- if (!safety.safe) {
285569
- return {
285570
- ok: false,
285571
- safety,
285572
- error: safety.errors[0] ?? "SQL rejected by qcp safety policy."
285573
- };
285574
- }
285575
- const explain = await explainExecutor(options2.databaseUrl, safety.processedSql);
285576
- return {
285577
- ok: true,
285578
- safety,
285579
- plan: explain.plan,
285580
- estimatedRows: explain.estimatedRows
285581
- };
285582
- }
286066
+ execute: async ({ sql }, context2) => executeSecurePrismaExplainQuery({
286067
+ databaseUrl: options2.databaseUrl,
286068
+ schema: options2.schema,
286069
+ explainExecutor,
286070
+ sensitiveTablePatterns
286071
+ }, sql, context2)
285583
286072
  }),
285584
286073
  qcp_read_prisma_context: createTool({
285585
286074
  id: "qcp_read_prisma_context",
@@ -285608,6 +286097,169 @@ function createPrismaTools(options2) {
285608
286097
  })
285609
286098
  };
285610
286099
  }
286100
+ async function executeSecurePrismaReadQuery(options2, sql, context2) {
286101
+ const base2 = prepareSecurePrismaQuery(sql, options2.schema, options2.sensitiveTablePatterns ?? [], context2);
286102
+ if (!base2.ok)
286103
+ return base2;
286104
+ const queryExecutor = options2.queryExecutor ?? executeQuery;
286105
+ try {
286106
+ const result = await queryExecutor(options2.databaseUrl, base2.isolation.processedSql);
286107
+ return {
286108
+ ok: true,
286109
+ safety: base2.safety,
286110
+ isolation: base2.isolation,
286111
+ result: sanitizeSensitiveData(result),
286112
+ approvalReasons: base2.approvalReasons
286113
+ };
286114
+ } catch (err) {
286115
+ return {
286116
+ ok: false,
286117
+ safety: base2.safety,
286118
+ isolation: base2.isolation,
286119
+ error: sanitizeDatabaseError(err),
286120
+ approvalReasons: base2.approvalReasons
286121
+ };
286122
+ }
286123
+ }
286124
+ async function executeSecurePrismaExplainQuery(options2, sql, context2) {
286125
+ const base2 = prepareSecurePrismaQuery(sql, options2.schema, options2.sensitiveTablePatterns ?? [], context2);
286126
+ if (!base2.ok)
286127
+ return base2;
286128
+ const explainExecutor = options2.explainExecutor ?? explainQuery;
286129
+ try {
286130
+ const explain = await explainExecutor(options2.databaseUrl, base2.isolation.processedSql);
286131
+ const approvalReasons = getApprovalReasons(base2.isolation.processedSql, base2.safety, [...options2.sensitiveTablePatterns ?? []], explain.estimatedRows);
286132
+ return {
286133
+ ok: true,
286134
+ safety: base2.safety,
286135
+ isolation: base2.isolation,
286136
+ plan: sanitizeSensitiveData(explain.plan),
286137
+ estimatedRows: explain.estimatedRows,
286138
+ approvalReasons
286139
+ };
286140
+ } catch (err) {
286141
+ return {
286142
+ ok: false,
286143
+ safety: base2.safety,
286144
+ isolation: base2.isolation,
286145
+ error: sanitizeDatabaseError(err),
286146
+ approvalReasons: base2.approvalReasons
286147
+ };
286148
+ }
286149
+ }
286150
+ function prepareSecurePrismaQuery(sql, schema, sensitiveTablePatterns, context2) {
286151
+ const safety = validateSql(sql);
286152
+ if (!safety.safe) {
286153
+ return {
286154
+ ok: false,
286155
+ safety,
286156
+ error: safety.errors[0] ?? "SQL rejected by qcp safety policy.",
286157
+ approvalReasons: []
286158
+ };
286159
+ }
286160
+ const securityContext = extractSecurityRequestContext(context2);
286161
+ if (!securityContext) {
286162
+ return {
286163
+ ok: false,
286164
+ safety,
286165
+ error: "Trusted tenant context is required.",
286166
+ approvalReasons: []
286167
+ };
286168
+ }
286169
+ const isolation = enforceTenantIsolation(safety.processedSql, schema, securityContext);
286170
+ if (!isolation.safe) {
286171
+ return {
286172
+ ok: false,
286173
+ safety,
286174
+ isolation,
286175
+ error: isolation.errors[0] ?? "Query rejected by qcp tenant isolation policy.",
286176
+ approvalReasons: []
286177
+ };
286178
+ }
286179
+ return {
286180
+ ok: true,
286181
+ safety,
286182
+ isolation,
286183
+ approvalReasons: getApprovalReasons(isolation.processedSql, safety, [...sensitiveTablePatterns])
286184
+ };
286185
+ }
286186
+ async function shouldRequirePrismaToolApproval(options2) {
286187
+ const safety = validateSql(options2.sql);
286188
+ if (!safety.safe)
286189
+ return false;
286190
+ const securityContext = extractSecurityRequestContext({
286191
+ requestContext: options2.requestContext
286192
+ });
286193
+ if (!securityContext)
286194
+ return false;
286195
+ const isolation = enforceTenantIsolation(safety.processedSql, options2.schema, securityContext);
286196
+ if (!isolation.safe)
286197
+ return false;
286198
+ if (requiresSensitiveApproval(isolation.processedSql, safety, [...options2.sensitiveTablePatterns])) {
286199
+ return true;
286200
+ }
286201
+ try {
286202
+ const explain = await options2.explainExecutor(options2.databaseUrl, isolation.processedSql);
286203
+ const reasons = getApprovalReasons(isolation.processedSql, safety, [...options2.sensitiveTablePatterns], explain.estimatedRows);
286204
+ return reasons.some((reason) => reason.type === "high_cost");
286205
+ } catch {
286206
+ return true;
286207
+ }
286208
+ }
286209
+ function extractSecurityRequestContext(context2) {
286210
+ const requestContext = getRecordValue(context2, "requestContext") ?? context2;
286211
+ const candidates = [];
286212
+ if (isRequestContextLike(requestContext)) {
286213
+ candidates.push({
286214
+ tenantId: requestContext.get("tenantId"),
286215
+ userId: requestContext.get("userId")
286216
+ });
286217
+ }
286218
+ const all = getRecordValue(requestContext, "all");
286219
+ if (all)
286220
+ candidates.push(all);
286221
+ candidates.push(requestContext);
286222
+ for (const candidate of candidates) {
286223
+ const result = securityRequestContextSchema.safeParse(candidate);
286224
+ if (result.success)
286225
+ return result.data;
286226
+ }
286227
+ return null;
286228
+ }
286229
+ function isRequestContextLike(value2) {
286230
+ return typeof value2 === "object" && value2 !== null && typeof value2.get === "function";
286231
+ }
286232
+ function getRecordValue(value2, key) {
286233
+ if (typeof value2 !== "object" || value2 === null)
286234
+ return;
286235
+ return value2[key];
286236
+ }
286237
+ function sanitizedToolModelOutput(output) {
286238
+ return {
286239
+ type: "json",
286240
+ value: sanitizeSensitiveData(output)
286241
+ };
286242
+ }
286243
+ function secureToolTransform() {
286244
+ return {
286245
+ display: {
286246
+ input: redactedToolInput,
286247
+ output: ({ output }) => sanitizeSensitiveData(output),
286248
+ error: sanitizedToolError
286249
+ },
286250
+ transcript: {
286251
+ input: redactedToolInput,
286252
+ output: ({ output }) => sanitizeSensitiveData(output),
286253
+ error: sanitizedToolError
286254
+ }
286255
+ };
286256
+ }
286257
+ function redactedToolInput(_payload) {
286258
+ return { sql: "[REDACTED_SQL]" };
286259
+ }
286260
+ function sanitizedToolError() {
286261
+ return { message: sanitizeDatabaseError(undefined) };
286262
+ }
285611
286263
  async function loadPrismaMcpToolsets(databaseUrl, clientFactory = createPrismaMcpClient) {
285612
286264
  let client = null;
285613
286265
  try {
@@ -285660,6 +286312,7 @@ async function generateSqlWithPrismaAgent(options2) {
285660
286312
  databaseUrl: options2.databaseUrl,
285661
286313
  schema: options2.schema,
285662
286314
  prismaSchemaPath: options2.prismaSchemaPath,
286315
+ sensitiveTablePatterns: options2.config.sensitiveTablePatterns,
285663
286316
  instructions: [
285664
286317
  "Use qcp_read_prisma_context and qcp_validate_sql to improve SQL quality.",
285665
286318
  "Do not call qcp_execute_read_sql while generating SQL for qcp CLI output.",
@@ -285769,7 +286422,7 @@ function stringProcessEnv() {
285769
286422
  }
285770
286423
  return env4;
285771
286424
  }
285772
- var safetyReportSchema, queryResultSchema, prismaContextSchema, PrismaAgent;
286425
+ var safetyReportSchema, tenantIsolationReportSchema, approvalReasonSchema, queryResultSchema, prismaContextSchema, PrismaAgent;
285773
286426
  var init_prisma_agent = __esm(() => {
285774
286427
  init_tools();
285775
286428
  init_dist32();
@@ -285789,6 +286442,18 @@ var init_prisma_agent = __esm(() => {
285789
286442
  processedSql: exports_external.string(),
285790
286443
  statementType: exports_external.string()
285791
286444
  });
286445
+ tenantIsolationReportSchema = exports_external.object({
286446
+ safe: exports_external.boolean(),
286447
+ errors: exports_external.array(exports_external.string()),
286448
+ warnings: exports_external.array(exports_external.string()),
286449
+ processedSql: exports_external.string(),
286450
+ injectedPredicates: exports_external.array(exports_external.string()),
286451
+ scopedTables: exports_external.array(exports_external.string())
286452
+ });
286453
+ approvalReasonSchema = exports_external.object({
286454
+ type: exports_external.enum(["sensitive_table", "large_scan", "no_limit", "high_cost"]),
286455
+ detail: exports_external.string()
286456
+ });
285792
286457
  queryResultSchema = exports_external.object({
285793
286458
  rows: exports_external.array(exports_external.record(exports_external.string(), exports_external.unknown())),
285794
286459
  rowCount: exports_external.number(),
@@ -285814,6 +286479,7 @@ var init_prisma_agent = __esm(() => {
285814
286479
  databaseUrl: config2.databaseUrl,
285815
286480
  schema: config2.schema,
285816
286481
  prismaSchemaPath: config2.prismaSchemaPath,
286482
+ sensitiveTablePatterns: config2.sensitiveTablePatterns,
285817
286483
  queryExecutor: config2.queryExecutor,
285818
286484
  explainExecutor: config2.explainExecutor
285819
286485
  }) : {}
@@ -285875,8 +286541,12 @@ async function handlePrismaQuestion(input) {
285875
286541
  let estimatedRows;
285876
286542
  const safeMode = options2.safeMode ?? input.config.safeMode;
285877
286543
  if (safeMode || options2.debug) {
285878
- try {
285879
- const explain = await explainQuery(input.databaseUrl, safetyReport.processedSql);
286544
+ const explain = await executeSecurePrismaExplainQuery({
286545
+ databaseUrl: input.databaseUrl,
286546
+ schema: input.schema,
286547
+ sensitiveTablePatterns: input.config.sensitiveTablePatterns
286548
+ }, safetyReport.processedSql);
286549
+ if (explain.ok) {
285880
286550
  estimatedRows = explain.estimatedRows;
285881
286551
  if (options2.debug) {
285882
286552
  console.log(source_default.dim(`
@@ -285884,7 +286554,9 @@ async function handlePrismaQuestion(input) {
285884
286554
  console.log(source_default.dim(explain.plan.slice(0, 1200)));
285885
286555
  console.log();
285886
286556
  }
285887
- } catch {}
286557
+ } else if (options2.debug) {
286558
+ console.log(source_default.dim(`EXPLAIN unavailable: ${explain.error}`));
286559
+ }
285888
286560
  }
285889
286561
  if (safeMode && !options2.noConfirm) {
285890
286562
  const approvalReasons = getApprovalReasons(safetyReport.processedSql, safetyReport, input.config.sensitiveTablePatterns, estimatedRows);
@@ -285908,13 +286580,13 @@ async function handlePrismaQuestion(input) {
285908
286580
  const queryResult = await executePrismaQuery(input, safetyReport.processedSql);
285909
286581
  if (!queryResult)
285910
286582
  return false;
285911
- printResults(queryResult);
285912
- const summaryResult = await summarizePrismaResults(input, sqlResult, queryResult);
286583
+ printResults(queryResult.result);
286584
+ const summaryResult = await summarizePrismaResults(input, sqlResult, queryResult.result, queryResult.isolation.processedSql);
285913
286585
  if (summaryResult)
285914
286586
  printSummary(summaryResult.summary);
285915
286587
  if (sqlResult.explanation)
285916
286588
  printExplanation(sqlResult.explanation);
285917
- const totalMs = sqlResult.latencyMs + queryResult.executionTimeMs + (summaryResult?.latencyMs ?? 0);
286589
+ const totalMs = sqlResult.latencyMs + queryResult.result.executionTimeMs + (summaryResult?.latencyMs ?? 0);
285918
286590
  trackQuery({
285919
286591
  provider: input.config.provider,
285920
286592
  model: input.config.model,
@@ -285926,7 +286598,7 @@ async function handlePrismaQuestion(input) {
285926
286598
  tokensOut: (sqlResult.tokensOut ?? 0) + (summaryResult?.tokensOut ?? 0),
285927
286599
  totalLatencyMs: totalMs,
285928
286600
  sqlGenerationMs: sqlResult.latencyMs,
285929
- executionMs: queryResult.executionTimeMs,
286601
+ executionMs: queryResult.result.executionTimeMs,
285930
286602
  summaryMs: summaryResult?.latencyMs ?? 0,
285931
286603
  provider: input.config.provider,
285932
286604
  model: input.config.model
@@ -285936,7 +286608,7 @@ async function handlePrismaQuestion(input) {
285936
286608
  log3("info", "Prisma query completed", {
285937
286609
  provider: input.config.provider,
285938
286610
  model: input.config.model,
285939
- rows: queryResult.rowCount,
286611
+ rows: queryResult.result.rowCount,
285940
286612
  latencyMs: totalMs
285941
286613
  });
285942
286614
  return true;
@@ -285963,22 +286635,24 @@ async function generateSql(input) {
285963
286635
  }
285964
286636
  async function executePrismaQuery(input, sql) {
285965
286637
  const spinner = ora(input.commandName === "chat" ? "Executing..." : "Executing query...").start();
285966
- try {
285967
- const queryResult = await executeQuery(input.databaseUrl, sql);
285968
- spinner.succeed(input.commandName === "chat" ? `${queryResult.rowCount} row(s) · ${queryResult.executionTimeMs}ms` : `Query executed · ${queryResult.rowCount} row(s) · ${queryResult.executionTimeMs}ms`);
285969
- return queryResult;
285970
- } catch (err) {
286638
+ const queryResult = await executeSecurePrismaReadQuery({
286639
+ databaseUrl: input.databaseUrl,
286640
+ schema: input.schema,
286641
+ sensitiveTablePatterns: input.config.sensitiveTablePatterns
286642
+ }, sql);
286643
+ if (!queryResult.ok) {
285971
286644
  spinner.fail(input.commandName === "chat" ? "Execution failed" : "Query execution failed");
285972
- const message = err instanceof Error ? err.message : String(err);
285973
- printError(message);
286645
+ printError(queryResult.error);
285974
286646
  trackError(input.commandName, "prisma_query_execution_failed");
285975
286647
  return;
285976
286648
  }
286649
+ spinner.succeed(input.commandName === "chat" ? `${queryResult.result.rowCount} row(s) · ${queryResult.result.executionTimeMs}ms` : `Query executed · ${queryResult.result.rowCount} row(s) · ${queryResult.result.executionTimeMs}ms`);
286650
+ return queryResult;
285977
286651
  }
285978
- async function summarizePrismaResults(input, sqlResult, queryResult) {
286652
+ async function summarizePrismaResults(input, sqlResult, queryResult, executedSql) {
285979
286653
  const spinner = ora(input.commandName === "chat" ? "Summarizing..." : "Generating summary...").start();
285980
286654
  try {
285981
- const summaryResult = await input.provider.generateSummary(input.question, sqlResult.sql, queryResult, (chunk) => {
286655
+ const summaryResult = await input.provider.generateSummary(input.question, executedSql, queryResult, (chunk) => {
285982
286656
  if (input.options?.debug)
285983
286657
  process.stderr.write(source_default.dim(chunk));
285984
286658
  });
@@ -285998,7 +286672,6 @@ var init_prisma_query = __esm(() => {
285998
286672
  init_dist18();
285999
286673
  init_ora();
286000
286674
  init_prisma_agent();
286001
- init_db();
286002
286675
  init_logger2();
286003
286676
  init_output();
286004
286677
  init_safety();