{"version":3,"sources":["../src/index.ts","../src/sql-validator.ts","../src/table-access.ts","../src/tenant-scope.ts","../src/limit.ts","../src/execute-sql.tool.ts"],"sourcesContent":["export { SqlValidator, SqlValidationError, type SqlValidationResult } from './sql-validator.js';\nexport {\n  type TableAccessPolicy,\n  type GroupTableAccessConfig,\n  GroupTableAccessPolicy,\n} from './table-access.js';\nexport {\n  TenantScopeRewriter,\n  type TenantScopeConfig,\n} from './tenant-scope.js';\nexport { injectLimit } from './limit.js';\nexport {\n  createExecuteSqlTool,\n  type QueryRunner,\n  type ExecuteSqlDeps,\n  type ExecuteSqlResult,\n} from './execute-sql.tool.js';\n","import { Parser } from 'node-sql-parser';\n\n/**\n * Thrown when a statement is not a single, read-only SELECT — i.e. it is an\n * INSERT/UPDATE/DELETE, DDL, a CALL, a multi-statement string, or it fails to\n * parse. The handler surfaces `.message` to the model so it can re-plan.\n */\nexport class SqlValidationError extends Error {\n  constructor(message: string) {\n    super(message);\n    this.name = 'SqlValidationError';\n  }\n}\n\n/** Statement types that are categorically rejected (anything that writes or runs code). */\nconst FORBIDDEN_AST_TYPES = new Set([\n  'insert',\n  'update',\n  'delete',\n  'replace',\n  'create',\n  'drop',\n  'alter',\n  'truncate',\n  'rename',\n  'load_data',\n  'lock',\n  'unlock',\n  'set',\n  'call',\n  'handler',\n  'use',\n  'grant',\n  'revoke',\n]);\n\nexport interface SqlValidationResult {\n  /** Every base table the statement references (CTEs, joins, subqueries walked). */\n  tables: string[];\n}\n\n/**\n * Parses SQL (MySQL dialect) and asserts it is a single SELECT, returning the\n * set of tables it touches. Domain-agnostic: it knows nothing about which\n * tables a caller may read — that is the `TableAccessPolicy`'s job.\n */\nexport class SqlValidator {\n  private readonly parser = new Parser();\n\n  /**\n   * Throws `SqlValidationError` unless `sql` is exactly one SELECT statement.\n   * On success returns the distinct base table names referenced.\n   */\n  validate(sql: string): SqlValidationResult {\n    let parsed: unknown;\n    try {\n      parsed = this.parser.astify(sql, { database: 'MySQL' });\n    } catch (err) {\n      throw new SqlValidationError(`Parse error: ${(err as Error).message}`);\n    }\n\n    const statements = Array.isArray(parsed) ? parsed : [parsed];\n    if (statements.length !== 1) {\n      throw new SqlValidationError('Only a single statement is allowed');\n    }\n\n    const statement = statements[0] as { type?: string } | undefined;\n    const type = (statement?.type ?? '').toLowerCase();\n\n    if (type !== 'select') {\n      if (['create', 'drop', 'alter', 'truncate', 'rename'].includes(type)) {\n        throw new SqlValidationError('DDL is not allowed; only SELECT statements are accepted');\n      }\n      if (FORBIDDEN_AST_TYPES.has(type)) {\n        throw new SqlValidationError(\n          `${type.toUpperCase()} is not allowed; only SELECT statements are accepted`,\n        );\n      }\n      throw new SqlValidationError(\n        `Statement type \"${type || 'unknown'}\" is not allowed; only SELECT statements are accepted`,\n      );\n    }\n\n    return { tables: this.extractReferencedTables(sql) };\n  }\n\n  /**\n   * Distinct base table names a statement touches — walking CTEs, subqueries,\n   * and joins. Backed by `node-sql-parser`'s `tableList`, which emits\n   * `mode::db::table` strings; the table name is the last segment.\n   */\n  private extractReferencedTables(sql: string): string[] {\n    const raw = this.parser.tableList(sql, { database: 'MySQL' });\n    const tables = new Set<string>();\n    for (const entry of raw) {\n      const parts = entry.split('::');\n      const table = parts[parts.length - 1];\n      if (table && table !== 'null') tables.add(table);\n    }\n    return Array.from(tables);\n  }\n}\n","/**\n * Decides whether a role may read a given table at all. This is the coarse,\n * table-level gate; per-row scoping (see `TenantScopeRewriter`) is a separate\n * layer applied at query time.\n *\n * Domain-agnostic: the host app supplies the role→group and group→table maps.\n */\nexport interface TableAccessPolicy {\n  /** True iff any of the caller's `roles` is permitted to read `table`. Fail-closed by contract. */\n  canAccess(roles: readonly string[], table: string): boolean;\n}\n\n/** Inputs for {@link GroupTableAccessPolicy}: roles map to groups, groups to tables. */\nexport interface GroupTableAccessConfig {\n  /** Role name → the table groups that role may read. */\n  roleGroups: Record<string, string[]>;\n  /**\n   * Group name → the tables in that group. Entries are exact table names or\n   * `prefix_*` patterns (e.g. `pribuy_*`).\n   */\n  tablesByGroup: Record<string, string[]>;\n}\n\n/**\n * Two-layer, data-driven table allowlist:\n *\n *   1. Every table is classified into a group (`tablesByGroup`).\n *   2. Every role lists the groups it can read (`roleGroups`).\n *\n * `canAccess(roles, table)` is then \"is the table's group in ANY of the roles'\n * group lists?\". **Fail-closed:** an unclassified table, unknown roles, or an\n * empty role set is denied — a forgotten table never accidentally leaks.\n */\nexport class GroupTableAccessPolicy implements TableAccessPolicy {\n  private readonly roleGroups: Record<string, string[]>;\n  private readonly tablesByGroup: Record<string, string[]>;\n\n  constructor(config: GroupTableAccessConfig) {\n    this.roleGroups = config.roleGroups;\n    this.tablesByGroup = config.tablesByGroup;\n  }\n\n  canAccess(roles: readonly string[], table: string): boolean {\n    const group = this.resolveGroup(table);\n    if (group === undefined) return false;\n\n    return roles.some((role) => this.roleGroups[role]?.includes(group) ?? false);\n  }\n\n  /** Resolve a table to its group, or `undefined` if unclassified (fail-closed). */\n  private resolveGroup(table: string): string | undefined {\n    for (const [group, patterns] of Object.entries(this.tablesByGroup)) {\n      for (const pattern of patterns) {\n        if (matchesPattern(table, pattern)) return group;\n      }\n    }\n    return undefined;\n  }\n}\n\nfunction matchesPattern(table: string, pattern: string): boolean {\n  if (pattern.endsWith('*')) {\n    return table.startsWith(pattern.slice(0, -1));\n  }\n  return table === pattern;\n}\n","import type { AST } from 'node-sql-parser';\nimport { Parser } from 'node-sql-parser';\n\n/** Configuration for {@link TenantScopeRewriter}. */\nexport interface TenantScopeConfig {\n  /** The column that carries the tenant key on every scoped table (e.g. `base_id`, `org_id`). */\n  tenantColumn: string;\n  /** Tables that must be constrained to the caller's tenant when referenced. */\n  scopedTables: string[];\n  /**\n   * What a query on a scoped table does when the caller has no `tenantRef`: `'reject'` (default)\n   * refuses it; `'passthrough'` runs it unscoped — the privileged path, for an app where a missing\n   * tenant means \"sees every tenant\" (an internal admin). A query that touches no scoped table runs\n   * either way.\n   */\n  onMissingTenant?: 'reject' | 'passthrough';\n}\n\ninterface FromEntry {\n  table?: string;\n  as?: string | null;\n  join?: string;\n  expr?: { ast?: unknown };\n}\n\ninterface SelectAst {\n  type: string;\n  with?: unknown;\n  from?: FromEntry[];\n  where?: unknown;\n  _next?: unknown;\n}\n\ninterface BinaryExpr {\n  type: 'binary_expr';\n  operator: string;\n  left: unknown;\n  right: unknown;\n}\n\ninterface ColumnRef {\n  type: 'column_ref';\n  table: string | null;\n  column: string;\n}\n\ninterface ExtractedPredicate {\n  tableAlias: string | null;\n  value: string;\n}\n\nconst STRING_LITERAL_TYPES = new Set(['string', 'single_quote_string', 'double_quote_string']);\n\n/**\n * Rewrites a SELECT so every reference to a scoped table is constrained to a\n * single tenant: the SELECT's WHERE is parenthesised whole and\n * `<alias>.<tenantColumn> = '<tenantRef>'` is AND-ed to it, for every scoped\n * table in its FROM — always, even when the query already names the tenant\n * (an `OR` can make such a predicate constrain nothing). This applies to each\n * SELECT that reads a scoped table — the top-level one and every\n * subquery (in the select list, WHERE, HAVING, a JOIN's ON …). An existing\n * predicate for a different tenant is rejected (no cross-tenant reads).\n *\n * `tenantRef === undefined` on a query that reads a scoped table is refused,\n * unless `onMissingTenant: 'passthrough'` makes it the privileged path that runs\n * the SQL unchanged.\n *\n * Scoped mode rejects CTEs, UNION/INTERSECT/EXCEPT, and subqueries in FROM, at\n * any depth: those make it impossible to statically guarantee every\n * tenant-bearing source is constrained, so we fail closed and ask the caller to\n * rephrase.\n */\nexport class TenantScopeRewriter {\n  private readonly parser = new Parser();\n  private readonly tenantColumn: string;\n  private readonly scopedTables: Set<string>;\n  private readonly onMissingTenant: 'reject' | 'passthrough';\n\n  constructor(config: TenantScopeConfig) {\n    this.tenantColumn = config.tenantColumn;\n    this.scopedTables = new Set(config.scopedTables);\n    this.onMissingTenant = config.onMissingTenant ?? 'reject';\n  }\n\n  /**\n   * Rewrite `sql` to constrain scoped tables to `tenantRef`. Undefined → refused when the query\n   * reads a scoped table, unless `onMissingTenant: 'passthrough'`.\n   */\n  rewrite(sql: string, tenantRef: string | undefined): string {\n    if (tenantRef === undefined && this.onMissingTenant === 'passthrough') return sql;\n\n    const parsed = this.parser.astify(sql, { database: 'MySQL' });\n    const ast = (Array.isArray(parsed) ? parsed[0] : parsed) as SelectAst;\n\n    if (ast.type !== 'select') {\n      throw new Error('tenant scope: only SELECT is supported');\n    }\n\n    const selects = collectSelects(ast);\n    const readsScoped = selects.some((select) =>\n      (select.from ?? []).some(\n        (entry) => typeof entry.table === 'string' && this.scopedTables.has(entry.table),\n      ),\n    );\n    if (!readsScoped) return sql;\n    if (tenantRef === undefined) {\n      throw new Error(\n        'tenant scope: no tenant for this session — a query on a tenant-scoped table needs one',\n      );\n    }\n\n    for (const select of selects) this.scopeSelect(select, tenantRef);\n    return this.parser.sqlify(ast as unknown as AST, { database: 'MySQL' });\n  }\n\n  /** Constrain one SELECT's own FROM — its subqueries are scoped on their own. */\n  private scopeSelect(ast: SelectAst, tenantRef: string): void {\n    if (ast.with) {\n      throw new Error(\n        'tenant scope: WITH (CTE) is not supported in scoped mode — rewrite using JOINs/subqueries in FROM',\n      );\n    }\n    if (ast._next) {\n      throw new Error(\n        'tenant scope: UNION/INTERSECT/EXCEPT is not supported in scoped mode — run each branch as a separate query',\n      );\n    }\n\n    const fromEntries = ast.from ?? [];\n    for (const entry of fromEntries) {\n      if (!entry.table && entry.expr?.ast) {\n        throw new Error('tenant scope: subqueries in FROM are not supported in scoped mode');\n      }\n    }\n\n    const scopedFrom = fromEntries.filter(\n      (entry): entry is FromEntry & { table: string } =>\n        typeof entry.table === 'string' && this.scopedTables.has(entry.table),\n    );\n    if (scopedFrom.length === 0) return;\n\n    // A literal naming another tenant anywhere in the WHERE is refused outright. One naming THIS\n    // tenant proves nothing — under an `OR` (`tenant = 'x' OR 1 = 1`) it constrains no row — so it\n    // never stands in for the constraint below.\n    for (const predicate of this.collectTenantPredicates(ast.where)) {\n      if (predicate.value !== tenantRef) {\n        throw new Error(\n          'tenant scope: tenant mismatch — query targets a tenant other than the current session',\n        );\n      }\n    }\n\n    // The caller's WHERE, parenthesised whole, AND every scoped table's own tenant equality —\n    // qualified by its alias, so a join or a correlated subquery cannot point it elsewhere.\n    let where: unknown = ast.where == null ? null : parenthesised(ast.where);\n    for (const entry of scopedFrom) {\n      where = this.andCondition(\n        where,\n        this.buildTenantEquality(entry.as ?? entry.table, tenantRef),\n      );\n    }\n    ast.where = where;\n  }\n\n  private collectTenantPredicates(where: unknown): ExtractedPredicate[] {\n    if (!isBinaryExpr(where)) return [];\n    if (where.operator === 'AND' || where.operator === 'OR') {\n      return [\n        ...this.collectTenantPredicates(where.left),\n        ...this.collectTenantPredicates(where.right),\n      ];\n    }\n    if (where.operator !== '=') return [];\n    const lhs = where.left;\n    const rhs = where.right;\n    if (!isColumnRef(lhs) || lhs.column !== this.tenantColumn) return [];\n    if (!isStringLiteral(rhs)) return [];\n    return [{ tableAlias: lhs.table ?? null, value: rhs.value }];\n  }\n\n  private buildTenantEquality(tableAlias: string, tenantRef: string): BinaryExpr {\n    return {\n      type: 'binary_expr',\n      operator: '=',\n      left: { type: 'column_ref', table: tableAlias, column: this.tenantColumn },\n      right: { type: 'single_quote_string', value: tenantRef },\n    };\n  }\n\n  private andCondition(existing: unknown, added: BinaryExpr): BinaryExpr {\n    if (existing == null) return added;\n    return {\n      type: 'binary_expr',\n      operator: 'AND',\n      left: existing,\n      right: added,\n    };\n  }\n}\n\n/**\n * Every SELECT in the tree, the root first: the statement itself and each subquery wherever it\n * sits — the select list, WHERE, HAVING, a JOIN's ON, a FROM (which `scopeSelect` then refuses).\n */\n/** `node` with the parser's own parentheses flag set, so it prints — and binds — as one term. */\nfunction parenthesised(node: unknown): unknown {\n  return typeof node === 'object' && node !== null ? { ...node, parentheses: true } : node;\n}\n\nfunction collectSelects(root: SelectAst): SelectAst[] {\n  const found: SelectAst[] = [];\n  const walk = (node: unknown): void => {\n    if (typeof node !== 'object' || node === null) return;\n    if (Array.isArray(node)) {\n      for (const item of node) walk(item);\n      return;\n    }\n    if ((node as { type?: unknown }).type === 'select') found.push(node as SelectAst);\n    for (const value of Object.values(node)) walk(value);\n  };\n  walk(root);\n  return found;\n}\n\nfunction isBinaryExpr(value: unknown): value is BinaryExpr {\n  return (\n    typeof value === 'object' &&\n    value !== null &&\n    (value as { type?: unknown }).type === 'binary_expr'\n  );\n}\n\nfunction isColumnRef(value: unknown): value is ColumnRef {\n  return (\n    typeof value === 'object' &&\n    value !== null &&\n    (value as { type?: unknown }).type === 'column_ref'\n  );\n}\n\nfunction isStringLiteral(value: unknown): value is { type: string; value: string } {\n  return (\n    typeof value === 'object' &&\n    value !== null &&\n    typeof (value as { type?: unknown }).type === 'string' &&\n    STRING_LITERAL_TYPES.has((value as { type: string }).type) &&\n    typeof (value as { value?: unknown }).value === 'string'\n  );\n}\n","import { Parser } from 'node-sql-parser';\n\nconst parser = new Parser();\n\n/**\n * Ensures a SELECT returns at most `max` rows. A statement whose own LIMIT is\n * already within `max` is returned unchanged; otherwise — no LIMIT, or one above\n * `max` — it is wrapped in a bounding subquery\n * (`SELECT * FROM (<sql>) AS subq LIMIT <max>`) so any ORDER BY / GROUP BY /\n * UNION / OFFSET inside `sql` is preserved.\n */\nexport function injectLimit(sql: string, max: number): string {\n  const trimmed = sql.trim().replace(/;\\s*$/, '');\n  const limit = rowLimit(trimmed);\n  if (limit !== undefined && limit <= max) return trimmed;\n  return `SELECT * FROM (${trimmed}) AS subq LIMIT ${max}`;\n}\n\n/** The row count the statement's own LIMIT allows; undefined when there is none or it can't tell. */\nfunction rowLimit(sql: string): number | undefined {\n  try {\n    const parsed = parser.astify(sql, { database: 'MySQL' });\n    const statement = (Array.isArray(parsed) ? parsed[0] : parsed) as\n      | { limit?: { seperator?: string; value?: Array<{ type?: string; value?: unknown }> } | null }\n      | undefined;\n    const limit = statement?.limit;\n    if (!limit || !Array.isArray(limit.value) || limit.value.length === 0) return undefined;\n    // `LIMIT <offset>, <count>` puts the count second; `LIMIT <count> [OFFSET <offset>]`, first.\n    const count = limit.seperator === ',' ? limit.value[1] : limit.value[0];\n    return count?.type === 'number' && typeof count.value === 'number' ? count.value : undefined;\n  } catch {\n    // If we can't parse it here, fall back to wrapping — the validator already\n    // ran and accepted it, so wrapping is the safe choice.\n    return undefined;\n  }\n}\n","import type { AiToolCtx, ToolHandler, ToolSpec } from '@dudousxd/nestjs-agent-core';\nimport { z } from 'zod';\nimport { injectLimit } from './limit.js';\nimport { SqlValidator } from './sql-validator.js';\nimport type { TableAccessPolicy } from './table-access.js';\nimport type { TenantScopeRewriter } from './tenant-scope.js';\n\n/** App-supplied runner over a read-only connection pool. The package never opens a DB. */\nexport interface QueryRunner {\n  run(sql: string): Promise<Record<string, unknown>[]>;\n}\n\n/** Dependencies for {@link createExecuteSqlTool}. */\nexport interface ExecuteSqlDeps {\n  /** Runs the (already validated, scoped, and limited) SQL against the read-only pool. */\n  runner: QueryRunner;\n  /** Single-SELECT validator. Defaults to a fresh {@link SqlValidator}. */\n  validator?: SqlValidator;\n  /** Coarse table-level allowlist, checked for every referenced table. */\n  tableAccess: TableAccessPolicy;\n  /** Optional per-row tenant constraint applied before the query runs. */\n  tenantScope?: TenantScopeRewriter;\n  /** Row cap: injected when the query has no LIMIT, enforced over a larger one. Defaults to 100. */\n  maxRows?: number;\n}\n\nexport interface ExecuteSqlResult {\n  rows: Record<string, unknown>[];\n  rowCount: number;\n  sql: string;\n  truncated?: true;\n}\n\nconst DEFAULT_MAX_ROWS = 100;\n\n/** Serialized rows above this size are truncated so a single tool result can't blow the context. */\nconst MAX_RESULT_BYTES = 256 * 1024;\n\nconst inputSchema = z.object({\n  sql: z.string().min(1).describe('A single read-only MySQL SELECT statement'),\n});\n\ntype ExecuteSqlInput = z.infer<typeof inputSchema>;\n\nconst DESCRIPTION =\n  'Execute a single read-only MySQL SELECT statement. Use this to answer questions about real ' +\n  'data. Only SELECT is allowed (no INSERT/UPDATE/DELETE/DDL). Access is restricted to the ' +\n  'tables your role is permitted to read, results are capped, and tenant-scoped tables are ' +\n  'automatically constrained to your current tenant.';\n\n/**\n * Builds the governed `executeSql` read tool. The pipeline per call is:\n * validate (single SELECT) → assert table access for every referenced table →\n * tenant-scope rewrite (if configured) → inject LIMIT → run → return rows,\n * truncating the payload if it would exceed ~256KB.\n */\nexport function createExecuteSqlTool(deps: ExecuteSqlDeps): {\n  spec: ToolSpec;\n  handler: ToolHandler<ExecuteSqlInput>;\n} {\n  const validator = deps.validator ?? new SqlValidator();\n  const maxRows = deps.maxRows ?? DEFAULT_MAX_ROWS;\n\n  const spec: ToolSpec = {\n    name: 'executeSql',\n    kind: 'read',\n    description: DESCRIPTION,\n    inputSchema,\n  };\n\n  const handler: ToolHandler<ExecuteSqlInput> = {\n    async execute(input: ExecuteSqlInput, ctx: AiToolCtx): Promise<ExecuteSqlResult> {\n      const { tables } = validator.validate(input.sql);\n\n      const roles = ctx.actor.roles ?? [];\n      const forbidden = tables.filter((table) => !deps.tableAccess.canAccess(roles, table));\n      if (forbidden.length > 0) {\n        const formatted = forbidden.map((table) => `\\`${table}\\``).join(', ');\n        const rolesLabel = roles.length > 0 ? roles.join(', ') : 'none';\n        throw new Error(`Your roles (${rolesLabel}) are not allowed to query ${formatted}.`);\n      }\n\n      let sql = input.sql;\n      if (deps.tenantScope) {\n        sql = deps.tenantScope.rewrite(sql, ctx.actor.tenantRef);\n      }\n      sql = injectLimit(sql, maxRows);\n\n      const rows = await deps.runner.run(sql);\n      return buildResult(rows, sql);\n    },\n  };\n\n  return { spec, handler };\n}\n\nfunction buildResult(rows: Record<string, unknown>[], sql: string): ExecuteSqlResult {\n  const rowCount = rows.length;\n  if (byteLength(rows) <= MAX_RESULT_BYTES) {\n    return { rows, rowCount, sql };\n  }\n\n  // Keep prefix rows until the serialized payload would exceed the cap.\n  const kept: Record<string, unknown>[] = [];\n  let size = 2; // opening + closing bracket of the JSON array\n  for (const row of rows) {\n    const rowSize = byteLength(row) + 1; // +1 for the joining comma\n    if (size + rowSize > MAX_RESULT_BYTES) break;\n    kept.push(row);\n    size += rowSize;\n  }\n\n  return { rows: kept, rowCount, sql, truncated: true };\n}\n\nfunction byteLength(value: unknown): number {\n  return Buffer.byteLength(JSON.stringify(value) ?? '', 'utf8');\n}\n"],"mappings":"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","names":["SqlValidationError","Error","message","name","FORBIDDEN_AST_TYPES","Set","SqlValidator","parser","Parser","validate","sql","parsed","astify","database","err","statements","Array","isArray","length","statement","type","toLowerCase","includes","has","toUpperCase","tables","extractReferencedTables","raw","tableList","entry","parts","split","table","add","from","GroupTableAccessPolicy","roleGroups","tablesByGroup","config","canAccess","roles","table","group","resolveGroup","undefined","some","role","includes","patterns","Object","entries","pattern","matchesPattern","endsWith","startsWith","slice","import_node_sql_parser","STRING_LITERAL_TYPES","Set","TenantScopeRewriter","parser","Parser","tenantColumn","scopedTables","onMissingTenant","config","rewrite","sql","tenantRef","undefined","parsed","astify","database","ast","Array","isArray","type","Error","selects","collectSelects","readsScoped","some","select","from","entry","table","has","scopeSelect","sqlify","with","_next","fromEntries","expr","scopedFrom","filter","length","predicate","collectTenantPredicates","where","value","parenthesised","andCondition","buildTenantEquality","as","isBinaryExpr","operator","left","right","lhs","rhs","isColumnRef","column","isStringLiteral","tableAlias","existing","added","node","parentheses","root","found","walk","item","push","Object","values","import_node_sql_parser","parser","Parser","injectLimit","sql","max","trimmed","trim","replace","limit","rowLimit","undefined","parsed","astify","database","statement","Array","isArray","value","length","count","seperator","type","DEFAULT_MAX_ROWS","MAX_RESULT_BYTES","inputSchema","z","object","sql","string","min","describe","DESCRIPTION","createExecuteSqlTool","deps","validator","SqlValidator","maxRows","spec","name","kind","description","handler","execute","input","ctx","tables","validate","roles","actor","forbidden","filter","table","tableAccess","canAccess","length","formatted","map","join","rolesLabel","Error","tenantScope","rewrite","tenantRef","injectLimit","rows","runner","run","buildResult","rowCount","byteLength","kept","size","row","rowSize","push","truncated","value","Buffer","JSON","stringify"]}