/** * cli:derive-lookup-grants — rbac-rows.ts * * Row-level rbac.md parsing (HUMAN rows only — the machine-owned block is * stripped first). Unlike derive-related-tabs' flat permission-token * collector, the derivation needs the `(actor × permission × portée)` * granularity: grantees are the actors holding create/update in the consumer * matrix, and the skip rule checks whether an actor already holds the * producer section's read/lookup. * * The generic parser lives in lib/ba-rbac-rows.ts (also consumed by * documentation/extract-doc) and is re-exported here so historical import * sites and tests stay untouched; only the derive-specific helpers * (listSections, holdsProducerSectionGrant) remain local. */ import { readdirSync } from 'node:fs' import { join } from 'node:path' import type { RbacRow } from '../../../../lib/ba-rbac-rows.js' export { ROW_RE, parseRbacRows, loadModuleRbacRows } from '../../../../lib/ba-rbac-rows.js' export type { RbacRow } from '../../../../lib/ba-rbac-rows.js' /** * Section folders of a module — lower-kebab directories under * `///` (apps/modules are UPPERCASE folders; sections and * resources are lower-kebab; `_`/`.` prefixes are workflow folders). */ export function listSections(baRoot: string, app: string, module: string): string[] { const moduleDir = join(baRoot, app, module) let entries try { entries = readdirSync(moduleDir, { withFileTypes: true }) } catch { return [] } return entries .filter( (e) => e.isDirectory() && !e.name.startsWith('_') && !e.name.startsWith('.') && /^[a-z]/.test(e.name), ) .map((e) => e.name) .sort() } /** * Does the actor already hold one of `actions` at the SECTION grain on the * producer? Only the section grain passes the platform's dual gate * (`[RequirePermission(x.lookup, x.read)]`), so module-grain (`mod.read`) and * resource-grain rows do NOT count. Accepted row shapes: * - 3-seg module-scoped: `{producerModule}.{producerSection}.{action}` * - 4-seg app-qualified: `{producerApp}.{producerModule}.{producerSection}.{action}` */ export function holdsProducerSectionGrant( rows: RbacRow[], actorCode: string, producerAppCode: string, producerModuleCode: string, producerSectionCode: string, actions: readonly string[], ): string | undefined { for (const row of rows) { if (row.actorCode !== actorCode) continue const parts = row.path.split('.') const action = parts[parts.length - 1] if (!actions.includes(action)) continue const matches3 = parts.length === 3 && parts[0] === producerModuleCode && parts[1] === producerSectionCode const matches4 = parts.length === 4 && parts[0] === producerAppCode && parts[1] === producerModuleCode && parts[2] === producerSectionCode if (matches3 || matches4) return action } return undefined }