( pgSql: any, )
| 216 | * Returns setup/teardown SQL to run before/after the user's query. |
| 217 | */ |
| 218 | export async function buildScopingPostgres( |
| 219 | pgSql: any, |
| 220 | ): Promise<ScopingContext> { |
| 221 | const inactive: ScopingContext = { |
| 222 | setup: [], |
| 223 | teardown: [], |
| 224 | active: false, |
| 225 | userEmail: null, |
| 226 | orgId: null, |
| 227 | ownerEmailTables: new Set(), |
| 228 | orgIdTables: new Set(), |
| 229 | }; |
| 230 | |
| 231 | // Scoping is always active when there is a request user (dev, preview, and |
| 232 | // prod). Previously this short-circuited outside production, which created |
| 233 | // a cross-user read in dev mode. See audit 05-tools-sandbox.md (C3.d). |
| 234 | const userEmail = getUserEmail(); |
| 235 | if (!userEmail) return inactive; |
| 236 | |
| 237 | const orgId = getOrgId(); |
| 238 | const allColumns = await discoverColumnsPostgres(pgSql); |
| 239 | const scoped = buildScopedTables(allColumns, userEmail, orgId, true); |
| 240 | |
| 241 | // Track which tables have owner_email / org_id for INSERT injection |
| 242 | const columnsByTable = new Map<string, string[]>(); |
| 243 | for (const { table, column } of allColumns) { |
| 244 | const cols = columnsByTable.get(table) || []; |
| 245 | cols.push(column); |
| 246 | columnsByTable.set(table, cols); |
| 247 | } |
| 248 | const ownerEmailTables = new Set<string>(); |
| 249 | const orgIdTables = new Set<string>(); |
| 250 | for (const [table, columns] of columnsByTable) { |
| 251 | if (columns.includes(OWNER_COLUMN)) ownerEmailTables.add(table); |
| 252 | if (columns.includes(ORG_COLUMN)) orgIdTables.add(table); |
| 253 | } |
| 254 | |
| 255 | return { |
| 256 | setup: scoped.map((s) => s.viewSql), |
| 257 | teardown: scoped.map((s) => `DROP VIEW IF EXISTS pg_temp."${s.name}"`), |
| 258 | active: scoped.length > 0, |
| 259 | userEmail, |
| 260 | orgId, |
| 261 | ownerEmailTables, |
| 262 | orgIdTables, |
| 263 | }; |
| 264 | } |
| 265 | |
| 266 | /** |
| 267 | * Build scoping context for a SQLite/libsql connection. |
no test coverage detected