@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.
- package/README.md +67 -11
- package/dist/index.js +727 -54
- package/dist/qcp.js +727 -54
- package/package.json +1 -1
package/dist/qcp.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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
285527
|
-
|
|
285528
|
-
|
|
285529
|
-
|
|
285530
|
-
|
|
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
|
-
|
|
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
|
-
|
|
285568
|
-
|
|
285569
|
-
|
|
285570
|
-
|
|
285571
|
-
|
|
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
|
-
|
|
285879
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
285967
|
-
|
|
285968
|
-
|
|
285969
|
-
|
|
285970
|
-
}
|
|
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
|
-
|
|
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,
|
|
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();
|