@invect/rbac 0.1.0 → 0.1.1
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
CHANGED
|
@@ -17,7 +17,7 @@ pnpm add @invect/rbac
|
|
|
17
17
|
### Backend
|
|
18
18
|
|
|
19
19
|
```typescript
|
|
20
|
-
import {
|
|
20
|
+
import { userAuth } from '@invect/user-auth';
|
|
21
21
|
import { rbacPlugin } from '@invect/rbac';
|
|
22
22
|
|
|
23
23
|
app.use('/invect', createInvectRouter({
|
|
@@ -27,7 +27,7 @@ app.use('/invect', createInvectRouter({
|
|
|
27
27
|
useFlowAccessTable: true,
|
|
28
28
|
},
|
|
29
29
|
plugins: [
|
|
30
|
-
|
|
30
|
+
userAuth({ auth }), // Auth MUST be registered first
|
|
31
31
|
rbacPlugin({
|
|
32
32
|
useFlowAccessTable: true,
|
|
33
33
|
}),
|
package/dist/backend/index.cjs
CHANGED
|
@@ -9,7 +9,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
9
9
|
* ```ts
|
|
10
10
|
* import { resolveTeamIds } from '@invect/rbac/backend';
|
|
11
11
|
*
|
|
12
|
-
*
|
|
12
|
+
* userAuth({
|
|
13
13
|
* auth,
|
|
14
14
|
* mapUser: async (user, session) => ({
|
|
15
15
|
* id: user.id,
|
|
@@ -445,9 +445,9 @@ function rbacPlugin(options = {}) {
|
|
|
445
445
|
"rbac_scope_access"
|
|
446
446
|
] : []
|
|
447
447
|
],
|
|
448
|
-
setupInstructions: "The RBAC plugin requires better-auth tables (user, session). Make sure @invect/user-auth is configured, then run `npx invect generate` followed by `npx drizzle-kit push`.",
|
|
448
|
+
setupInstructions: "The RBAC plugin requires better-auth tables (user, session). Make sure @invect/user-auth is configured, then run `npx invect-cli generate` followed by `npx drizzle-kit push`.",
|
|
449
449
|
init: async (ctx) => {
|
|
450
|
-
if (!ctx.hasPlugin("better-auth")) ctx.logger.warn("RBAC plugin requires the @invect/user-auth plugin. RBAC will work with reduced functionality (no session resolution). Make sure
|
|
450
|
+
if (!ctx.hasPlugin("better-auth")) ctx.logger.warn("RBAC plugin requires the @invect/user-auth plugin. RBAC will work with reduced functionality (no session resolution). Make sure userAuth() is registered before rbacPlugin().");
|
|
451
451
|
ctx.logger.info("RBAC plugin initialized", { useFlowAccessTable });
|
|
452
452
|
},
|
|
453
453
|
endpoints: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../src/backend/plugin.ts"],"sourcesContent":["/**\n * @invect/rbac — Backend Plugin\n *\n * RBAC plugin that provides:\n * - Flow access management endpoints (grant/revoke/list)\n * - Auth info endpoints (/auth/me, /auth/roles)\n * - Authorization hooks (enforces flow-level ACLs)\n * - UI manifest for the frontend plugin to render\n *\n * Requires the @invect/user-auth (better-auth) plugin to be loaded first\n * for session resolution. This plugin handles the *authorization* layer\n * on top of that authentication.\n */\n\nimport type {\n InvectPlugin,\n InvectPluginContext,\n InvectPermission,\n InvectPluginSchema,\n InvectIdentity,\n PluginDatabaseApi,\n PluginEndpointContext,\n} from '@invect/core';\nimport type {\n EffectiveAccessRecord,\n FlowAccessPermission,\n FlowAccessRecord,\n MovePreviewAccessChange,\n PluginUIManifest,\n ScopeAccessRecord,\n ScopeTreeNode,\n Team,\n} from '../shared/types';\n\n// ─────────────────────────────────────────────────────────────\n// Team ID Resolution Helper\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Resolve team IDs for a user from the rbac_team_members table.\n * Use this in a custom `mapUser` for the auth plugin to populate\n * `identity.teamIds` automatically.\n *\n * @example\n * ```ts\n * import { resolveTeamIds } from '@invect/rbac/backend';\n *\n * betterAuthPlugin({\n * auth,\n * mapUser: async (user, session) => ({\n * id: user.id,\n * name: user.name ?? undefined,\n * role: user.role === 'admin' ? 'admin' : 'user',\n * teamIds: await resolveTeamIds(db, user.id),\n * }),\n * });\n * ```\n */\nexport async function resolveTeamIds(db: PluginDatabaseApi, userId: string): Promise<string[]> {\n const rows = await db.query<{ team_id: string }>(\n 'SELECT team_id FROM rbac_team_members WHERE user_id = ?',\n [userId],\n );\n return rows.map((r) => r.team_id);\n}\n\n// ─────────────────────────────────────────────────────────────\n// Types\n// ─────────────────────────────────────────────────────────────\n\nexport interface RbacPluginOptions {\n /**\n * Enable the flow access table for database-backed per-flow permissions.\n * When enabled, the plugin will check flow-level ACLs in addition to\n * role-based permissions. Requires `auth.useFlowAccessTable: true` in\n * the Invect config.\n *\n * @default true\n */\n useFlowAccessTable?: boolean;\n\n /**\n * Permission required to access the RBAC flows page.\n *\n * @default 'flow:read'\n */\n adminPermission?: InvectPermission;\n\n /**\n * Enable teams feature for grouping users.\n *\n * @default true\n */\n enableTeams?: boolean;\n}\n\nconst FLOW_RESOURCE_TYPES = new Set(['flow', 'flow-version', 'flow-run', 'node-execution']);\n\nconst FLOW_PERMISSION_LEVELS: Record<FlowAccessPermission, number> = {\n viewer: 1,\n operator: 2,\n editor: 3,\n owner: 4,\n};\n\nfunction isFlowAccessPermission(value: unknown): value is FlowAccessPermission {\n return value === 'viewer' || value === 'operator' || value === 'editor' || value === 'owner';\n}\n\nfunction toFlowAccessPermission(value: unknown): FlowAccessPermission | null {\n return isFlowAccessPermission(value) ? value : null;\n}\n\nfunction getHigherPermission(\n left: FlowAccessPermission | null,\n right: FlowAccessPermission | null,\n): FlowAccessPermission | null {\n if (!left) {\n return right;\n }\n if (!right) {\n return left;\n }\n return FLOW_PERMISSION_LEVELS[right] > FLOW_PERMISSION_LEVELS[left] ? right : left;\n}\n\nfunction createInClause(count: number): string {\n return Array.from({ length: count }, () => '?').join(', ');\n}\n\nfunction mapActionToRequiredPermission(action: string): FlowAccessPermission {\n if (action.includes('delete') || action.includes('share') || action.includes('admin')) {\n return 'owner';\n }\n if (action.includes('update') || action.includes('write') || action.includes('create')) {\n return 'editor';\n }\n if (action.includes('run') || action.includes('execute')) {\n return 'operator';\n }\n return 'viewer';\n}\n\nfunction normalizeFlowAccessRecord(row: Record<string, unknown>): FlowAccessRecord {\n return {\n id: String(row.id),\n flowId: String(row.flowId ?? row.flow_id),\n userId: (row.userId ?? row.user_id) ? String(row.userId ?? row.user_id) : null,\n teamId: (row.teamId ?? row.team_id) ? String(row.teamId ?? row.team_id) : null,\n permission: String(row.permission) as FlowAccessPermission,\n grantedBy: (row.grantedBy ?? row.granted_by) ? String(row.grantedBy ?? row.granted_by) : null,\n grantedAt: String(row.grantedAt ?? row.granted_at),\n expiresAt: (row.expiresAt ?? row.expires_at) ? String(row.expiresAt ?? row.expires_at) : null,\n };\n}\n\nfunction normalizeScopeAccessRecord(row: Record<string, unknown>): ScopeAccessRecord {\n return {\n id: String(row.id),\n scopeId: String(row.scopeId ?? row.scope_id),\n userId: (row.userId ?? row.user_id) ? String(row.userId ?? row.user_id) : null,\n teamId: (row.teamId ?? row.team_id) ? String(row.teamId ?? row.team_id) : null,\n permission: String(row.permission) as FlowAccessPermission,\n grantedBy: (row.grantedBy ?? row.granted_by) ? String(row.grantedBy ?? row.granted_by) : null,\n grantedAt: String(row.grantedAt ?? row.granted_at),\n };\n}\n\nfunction normalizeTeamRow(row: {\n id: string;\n name: string;\n description: string | null;\n parent_id?: string | null;\n created_by: string | null;\n created_at: string;\n updated_at: string | null;\n}): Team {\n return {\n id: row.id,\n name: row.name,\n description: row.description,\n parentId: row.parent_id ?? null,\n createdBy: row.created_by,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n };\n}\n\nasync function getFlowScopeId(db: PluginDatabaseApi, flowId: string): Promise<string | null> {\n const rows = await db.query<{ scope_id: string | null }>(\n 'SELECT scope_id FROM flows WHERE id = ?',\n [flowId],\n );\n return rows[0]?.scope_id ?? null;\n}\n\nasync function getAncestorScopeIds(db: PluginDatabaseApi, scopeId: string): Promise<string[]> {\n const rows = await db.query<{ id: string }>(\n `WITH RECURSIVE ancestors AS (\n SELECT id, parent_id FROM rbac_teams WHERE id = ?\n UNION ALL\n SELECT parent.id, parent.parent_id\n FROM rbac_teams parent\n INNER JOIN ancestors current ON parent.id = current.parent_id\n )\n SELECT id FROM ancestors`,\n [scopeId],\n );\n return rows.map((row) => row.id);\n}\n\nasync function getDescendantScopeIds(db: PluginDatabaseApi, scopeId: string): Promise<string[]> {\n const rows = await db.query<{ id: string }>(\n `WITH RECURSIVE descendants AS (\n SELECT id FROM rbac_teams WHERE id = ?\n UNION ALL\n SELECT child.id\n FROM rbac_teams child\n INNER JOIN descendants current ON child.parent_id = current.id\n )\n SELECT id FROM descendants`,\n [scopeId],\n );\n return rows.map((row) => row.id);\n}\n\nasync function listScopeAccessForScopeIds(\n db: PluginDatabaseApi,\n scopeIds: string[],\n userId: string,\n teamIds: string[],\n): Promise<ScopeAccessRecord[]> {\n if (scopeIds.length === 0) {\n return [];\n }\n\n const params: unknown[] = [...scopeIds, userId];\n let sql =\n `SELECT id, scope_id, user_id, team_id, permission, granted_by, granted_at ` +\n `FROM rbac_scope_access WHERE (scope_id IN (${createInClause(scopeIds.length)}) ` +\n `AND user_id = ?)`;\n\n if (teamIds.length > 0) {\n sql += ` OR (scope_id IN (${createInClause(scopeIds.length)}) AND team_id IN (${createInClause(teamIds.length)}))`;\n params.push(...scopeIds, ...teamIds);\n }\n\n const rows = await db.query<Record<string, unknown>>(sql, params);\n return rows.map(normalizeScopeAccessRecord);\n}\n\nasync function listDirectFlowAccessForIdentity(\n db: PluginDatabaseApi,\n flowId: string,\n userId: string,\n teamIds: string[],\n): Promise<FlowAccessRecord[]> {\n const params: unknown[] = [flowId, userId];\n let sql =\n 'SELECT id, flow_id, user_id, team_id, permission, granted_by, granted_at, expires_at ' +\n 'FROM flow_access WHERE (flow_id = ? AND user_id = ?)';\n\n if (teamIds.length > 0) {\n sql += ` OR (flow_id = ? AND team_id IN (${createInClause(teamIds.length)}))`;\n params.push(flowId, ...teamIds);\n }\n\n const rows = await db.query<Record<string, unknown>>(sql, params);\n const now = Date.now();\n return rows\n .map(normalizeFlowAccessRecord)\n .filter((record) => !record.expiresAt || new Date(record.expiresAt).getTime() > now);\n}\n\nasync function getEffectiveFlowAccessRecords(\n db: PluginDatabaseApi,\n flowId: string,\n userId: string,\n teamIds: string[],\n): Promise<EffectiveAccessRecord[]> {\n const directRecords = (\n await listDirectFlowAccessForIdentity(db, flowId, userId, teamIds)\n ).map<EffectiveAccessRecord>((record) => ({ ...record, source: 'direct' }));\n\n const scopeId = await getFlowScopeId(db, flowId);\n if (!scopeId) {\n return directRecords;\n }\n\n const ancestorIds = await getAncestorScopeIds(db, scopeId);\n const inheritedRows = await listScopeAccessForScopeIds(db, ancestorIds, userId, teamIds);\n if (inheritedRows.length === 0) {\n return directRecords;\n }\n\n const scopeRows = await db.query<{ id: string; name: string }>(\n `SELECT id, name FROM rbac_teams WHERE id IN (${createInClause(ancestorIds.length)})`,\n ancestorIds,\n );\n const scopeNames = new Map(scopeRows.map((row) => [row.id, row.name]));\n\n return [\n ...directRecords,\n ...inheritedRows.map<EffectiveAccessRecord>((record) => ({\n id: record.id,\n flowId,\n userId: record.userId,\n teamId: record.teamId,\n permission: record.permission,\n grantedBy: record.grantedBy,\n grantedAt: record.grantedAt,\n expiresAt: null,\n source: 'inherited',\n scopeId: record.scopeId,\n scopeName: scopeNames.get(record.scopeId) ?? null,\n })),\n ];\n}\n\nasync function getEffectiveFlowPermission(\n db: PluginDatabaseApi,\n flowId: string,\n identity: InvectIdentity,\n): Promise<FlowAccessPermission | null> {\n const records = await getEffectiveFlowAccessRecords(\n db,\n flowId,\n identity.id,\n identity.teamIds ?? [],\n );\n\n let highest: FlowAccessPermission | null = null;\n for (const record of records) {\n highest = getHigherPermission(highest, record.permission);\n }\n return highest;\n}\n\nasync function getCurrentUserAccessibleFlows(\n db: PluginDatabaseApi,\n identity: InvectIdentity,\n): Promise<{ flowIds: string[]; permissions: Record<string, FlowAccessPermission | null> }> {\n const rows = await db.query<{ id: string }>('SELECT id FROM flows');\n const permissions: Record<string, FlowAccessPermission | null> = {};\n\n await Promise.all(\n rows.map(async (row) => {\n permissions[row.id] = await getEffectiveFlowPermission(db, row.id, identity);\n }),\n );\n\n const flowIds = rows.map((row) => row.id).filter((flowId) => permissions[flowId]);\n return { flowIds, permissions };\n}\n\nasync function getScopePath(db: PluginDatabaseApi, scopeId: string | null): Promise<string[]> {\n if (!scopeId) {\n return [];\n }\n\n const rows = await db.query<{ id: string; name: string }>(\n `WITH RECURSIVE ancestors AS (\n SELECT id, name, parent_id, 0 AS depth FROM rbac_teams WHERE id = ?\n UNION ALL\n SELECT parent.id, parent.name, parent.parent_id, current.depth + 1\n FROM rbac_teams parent\n INNER JOIN ancestors current ON parent.id = current.parent_id\n )\n SELECT id, name FROM ancestors ORDER BY depth DESC`,\n [scopeId],\n );\n return rows.map((row) => row.name);\n}\n\nasync function listAllScopeAccessForScopeIds(\n db: PluginDatabaseApi,\n scopeIds: string[],\n): Promise<ScopeAccessRecord[]> {\n if (scopeIds.length === 0) {\n return [];\n }\n const rows = await db.query<Record<string, unknown>>(\n `SELECT id, scope_id, user_id, team_id, permission, granted_by, granted_at\n FROM rbac_scope_access\n WHERE scope_id IN (${createInClause(scopeIds.length)})`,\n scopeIds,\n );\n return rows.map(normalizeScopeAccessRecord);\n}\n\nasync function listAllDirectFlowAccess(\n db: PluginDatabaseApi,\n flowId: string,\n): Promise<FlowAccessRecord[]> {\n const rows = await db.query<Record<string, unknown>>(\n 'SELECT id, flow_id, user_id, team_id, permission, granted_by, granted_at, expires_at FROM flow_access WHERE flow_id = ?',\n [flowId],\n );\n const now = Date.now();\n return rows\n .map(normalizeFlowAccessRecord)\n .filter((record) => !record.expiresAt || new Date(record.expiresAt).getTime() > now);\n}\n\nasync function listAllEffectiveFlowAccessForPreview(\n db: PluginDatabaseApi,\n flowId: string,\n overrideScopeId?: string | null,\n): Promise<Array<FlowAccessRecord | ScopeAccessRecord>> {\n const direct = await listAllDirectFlowAccess(db, flowId);\n const scopeId =\n overrideScopeId === undefined ? await getFlowScopeId(db, flowId) : overrideScopeId;\n if (!scopeId) {\n return direct;\n }\n\n const ancestorIds = await getAncestorScopeIds(db, scopeId);\n const inherited = await listAllScopeAccessForScopeIds(db, ancestorIds);\n return [...direct, ...inherited];\n}\n\nasync function listAllEffectiveFlowAccessRecords(\n db: PluginDatabaseApi,\n flowId: string,\n): Promise<EffectiveAccessRecord[]> {\n const directRecords = (await listAllDirectFlowAccess(db, flowId)).map<EffectiveAccessRecord>(\n (record) => ({\n ...record,\n source: 'direct',\n }),\n );\n\n const scopeId = await getFlowScopeId(db, flowId);\n if (!scopeId) {\n return directRecords;\n }\n\n const ancestorIds = await getAncestorScopeIds(db, scopeId);\n const inheritedRows = await listAllScopeAccessForScopeIds(db, ancestorIds);\n if (inheritedRows.length === 0) {\n return directRecords;\n }\n\n const scopeRows = await db.query<{ id: string; name: string }>(\n `SELECT id, name FROM rbac_teams WHERE id IN (${createInClause(ancestorIds.length)})`,\n ancestorIds,\n );\n const scopeNames = new Map(scopeRows.map((row) => [row.id, row.name]));\n\n // For team-based inherited grants, expand into per-member user records so the\n // UI shows individual users rather than team entities.\n const teamIds = [\n ...new Set(inheritedRows.map((r) => r.teamId).filter((id): id is string => !!id)),\n ];\n const teamMembersByTeamId = new Map<string, string[]>();\n if (teamIds.length > 0) {\n const memberRows = await db.query<{ team_id: string; user_id: string }>(\n `SELECT team_id, user_id FROM rbac_team_members WHERE team_id IN (${createInClause(teamIds.length)})`,\n teamIds,\n );\n for (const row of memberRows) {\n const members = teamMembersByTeamId.get(row.team_id) ?? [];\n members.push(row.user_id);\n teamMembersByTeamId.set(row.team_id, members);\n }\n }\n\n const expandedInherited: EffectiveAccessRecord[] = [];\n for (const record of inheritedRows) {\n const scopeName = scopeNames.get(record.scopeId) ?? null;\n if (record.teamId) {\n // Expand team grant → one record per team member\n const members = teamMembersByTeamId.get(record.teamId) ?? [];\n for (const userId of members) {\n expandedInherited.push({\n id: `${record.id}:${userId}`,\n flowId,\n userId,\n teamId: null,\n permission: record.permission,\n grantedBy: record.grantedBy,\n grantedAt: record.grantedAt,\n expiresAt: null,\n source: 'inherited',\n scopeId: record.scopeId,\n scopeName,\n });\n }\n } else {\n expandedInherited.push({\n id: record.id,\n flowId,\n userId: record.userId,\n teamId: null,\n permission: record.permission,\n grantedBy: record.grantedBy,\n grantedAt: record.grantedAt,\n expiresAt: null,\n source: 'inherited',\n scopeId: record.scopeId,\n scopeName,\n });\n }\n }\n\n return [...directRecords, ...expandedInherited];\n}\n\nfunction buildAccessKey(record: { userId?: string | null; teamId?: string | null }): string {\n if (record.userId) {\n return `user:${record.userId}`;\n }\n if (record.teamId) {\n return `team:${record.teamId}`;\n }\n return 'unknown';\n}\n\nasync function resolveAccessChangeNames(\n db: PluginDatabaseApi,\n entries: Array<{\n userId?: string | null;\n teamId?: string | null;\n permission: FlowAccessPermission;\n source: string;\n }>,\n): Promise<MovePreviewAccessChange[]> {\n const userIds = Array.from(\n new Set(entries.map((entry) => entry.userId).filter(Boolean)),\n ) as string[];\n const teamIds = Array.from(\n new Set(entries.map((entry) => entry.teamId).filter(Boolean)),\n ) as string[];\n\n const [userRows, teamRows] = await Promise.all([\n userIds.length > 0\n ? db.query<{ id: string; name: string | null; email: string | null }>(\n `SELECT id, name, email FROM user WHERE id IN (${createInClause(userIds.length)})`,\n userIds,\n )\n : Promise.resolve([]),\n teamIds.length > 0\n ? db.query<{ id: string; name: string }>(\n `SELECT id, name FROM rbac_teams WHERE id IN (${createInClause(teamIds.length)})`,\n teamIds,\n )\n : Promise.resolve([]),\n ]);\n\n const userMap = new Map(userRows.map((row) => [row.id, row.name || row.email || row.id]));\n const teamMap = new Map(teamRows.map((row) => [row.id, row.name]));\n\n return entries.map((entry) => ({\n userId: entry.userId ?? undefined,\n teamId: entry.teamId ?? undefined,\n name: entry.userId\n ? (userMap.get(entry.userId) ?? entry.userId)\n : entry.teamId\n ? (teamMap.get(entry.teamId) ?? entry.teamId)\n : 'Unknown',\n permission: entry.permission,\n source: entry.source,\n }));\n}\n\n// ─────────────────────────────────────────────────────────────\n// Plugin factory\n// ─────────────────────────────────────────────────────────────\n\nexport function rbacPlugin(options: RbacPluginOptions = {}): InvectPlugin {\n const { useFlowAccessTable = true, adminPermission = 'flow:read', enableTeams = true } = options;\n\n // ── Plugin-owned tables ─────────────────────────────────────\n const teamsSchema: InvectPluginSchema = enableTeams\n ? {\n flows: {\n fields: {\n scope_id: {\n type: 'string',\n required: false,\n references: { table: 'rbac_teams', field: 'id', onDelete: 'set null' },\n index: true,\n },\n },\n },\n rbac_teams: {\n fields: {\n id: { type: 'string', primaryKey: true },\n name: { type: 'string', required: true },\n description: { type: 'text', required: false },\n parent_id: {\n type: 'string',\n required: false,\n references: { table: 'rbac_teams', field: 'id', onDelete: 'set null' },\n index: true,\n },\n created_by: {\n type: 'string',\n required: false,\n references: { table: 'user', field: 'id' },\n },\n created_at: { type: 'date', required: true, defaultValue: 'now()' },\n updated_at: { type: 'date', required: false },\n },\n },\n rbac_team_members: {\n fields: {\n id: { type: 'string', primaryKey: true },\n team_id: {\n type: 'string',\n required: true,\n references: { table: 'rbac_teams', field: 'id', onDelete: 'cascade' },\n index: true,\n },\n user_id: {\n type: 'string',\n required: true,\n references: { table: 'user', field: 'id', onDelete: 'cascade' },\n index: true,\n },\n created_at: { type: 'date', required: true, defaultValue: 'now()' },\n },\n },\n rbac_scope_access: {\n fields: {\n id: { type: 'string', primaryKey: true },\n scope_id: {\n type: 'string',\n required: true,\n references: { table: 'rbac_teams', field: 'id', onDelete: 'cascade' },\n index: true,\n },\n user_id: { type: 'string', required: false, index: true },\n team_id: { type: 'string', required: false, index: true },\n permission: {\n type: 'string',\n required: true,\n defaultValue: 'viewer',\n typeAnnotation: 'FlowAccessPermission',\n },\n granted_by: { type: 'string', required: false },\n granted_at: { type: 'date', required: true, defaultValue: 'now()' },\n },\n },\n }\n : {};\n\n // Mutable DB reference — captured from the first endpoint handler.\n // Used by the onRequest hook to resolve teamIds for the identity.\n let capturedDbApi: PluginDatabaseApi | null = null;\n\n // UI manifest — declares what the frontend should render.\n // Component IDs are resolved by the frontend plugin's component registry.\n const ui: PluginUIManifest = {\n sidebar: [\n {\n label: 'Access Control',\n icon: 'Shield',\n path: '/access',\n permission: adminPermission,\n },\n ...(enableTeams ? [] : []),\n ],\n pages: [\n {\n path: '/access',\n componentId: 'rbac.AccessControlPage',\n title: 'Access Control',\n },\n ...(enableTeams ? [] : []),\n ],\n panelTabs: [\n {\n context: 'flowEditor',\n label: 'Access',\n componentId: 'rbac.FlowAccessPanel',\n permission: 'flow:read',\n },\n ],\n headerActions: [\n {\n context: 'flowHeader',\n componentId: 'rbac.ShareButton',\n permission: 'flow:update',\n },\n ],\n };\n\n return {\n id: 'rbac',\n name: 'Role-Based Access Control',\n\n // Plugin-owned database tables\n schema: teamsSchema,\n\n // RBAC uses the core flow_access table (already checked by core),\n // but also depends on the auth plugin's tables for user identity.\n // Declaring them here ensures a clear error if the developer has the\n // RBAC plugin enabled but forgot the better-auth tables.\n requiredTables: [\n 'user',\n 'session',\n ...(enableTeams ? ['rbac_teams', 'rbac_team_members', 'rbac_scope_access'] : []),\n ],\n setupInstructions:\n 'The RBAC plugin requires better-auth tables (user, session). ' +\n 'Make sure @invect/user-auth is configured, then run ' +\n '`npx invect generate` followed by `npx drizzle-kit push`.',\n\n // ─── Initialization ───────────────────────────────────────\n\n init: async (ctx: InvectPluginContext) => {\n // Verify that the auth plugin is loaded\n if (!ctx.hasPlugin('better-auth')) {\n ctx.logger.warn(\n 'RBAC plugin requires the @invect/user-auth plugin. ' +\n 'RBAC will work with reduced functionality (no session resolution). ' +\n 'Make sure betterAuthPlugin() is registered before rbacPlugin().',\n );\n }\n\n ctx.logger.info('RBAC plugin initialized', {\n useFlowAccessTable,\n });\n },\n\n // ─── Plugin Endpoints ─────────────────────────────────────\n\n endpoints: [\n // ── Auth Info ──\n\n {\n method: 'GET',\n path: '/rbac/me',\n isPublic: false,\n handler: async (ctx) => {\n // Capture DB API on first endpoint call for the onRequest hook\n if (!capturedDbApi && enableTeams) {\n capturedDbApi = ctx.database;\n }\n\n const identity = ctx.identity;\n const permissions = ctx.core.getPermissions(identity);\n const resolvedRole = identity ? ctx.core.getResolvedRole(identity) : null;\n\n return {\n status: 200,\n body: {\n identity: identity\n ? {\n id: identity.id,\n name: identity.name,\n role: identity.role,\n resolvedRole,\n }\n : null,\n permissions,\n isAuthenticated: !!identity,\n },\n };\n },\n },\n\n {\n method: 'GET',\n path: '/rbac/roles',\n isPublic: false,\n permission: 'flow:read',\n handler: async (ctx) => {\n const roles = ctx.core.getAvailableRoles();\n return { status: 200, body: { roles } };\n },\n },\n\n // ── UI Manifest ──\n\n {\n method: 'GET',\n path: '/rbac/ui-manifest',\n isPublic: true,\n handler: async (_ctx) => {\n return {\n status: 200,\n body: {\n id: 'rbac',\n ...ui,\n },\n };\n },\n },\n\n // ── Flow Access Management ──\n\n {\n method: 'GET',\n path: '/rbac/flows/:flowId/access',\n permission: 'flow:read',\n handler: async (ctx) => {\n const flowId = ctx.params.flowId;\n if (!flowId) {\n return { status: 400, body: { error: 'Missing flowId parameter' } };\n }\n\n if (!ctx.core.isFlowAccessTableEnabled()) {\n return {\n status: 501,\n body: {\n error: 'Not Implemented',\n message:\n 'Flow access table not enabled. Set auth.useFlowAccessTable: true in config.',\n },\n };\n }\n\n if (!ctx.identity) {\n return {\n status: 401,\n body: { error: 'Unauthorized', message: 'Authentication required' },\n };\n }\n\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n const callerPermission = isAdmin\n ? 'owner'\n : await getEffectiveFlowPermission(ctx.database, flowId, ctx.identity);\n\n if (!callerPermission) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'No access to this flow' },\n };\n }\n\n const access = await ctx.core.listFlowAccess(flowId);\n return { status: 200, body: { access } };\n },\n },\n\n {\n method: 'POST',\n path: '/rbac/flows/:flowId/access',\n permission: 'flow:read',\n handler: async (ctx) => {\n const flowId = ctx.params.flowId;\n if (!flowId) {\n return { status: 400, body: { error: 'Missing flowId parameter' } };\n }\n\n if (!ctx.core.isFlowAccessTableEnabled()) {\n return {\n status: 501,\n body: {\n error: 'Not Implemented',\n message:\n 'Flow access table not enabled. Set auth.useFlowAccessTable: true in config.',\n },\n };\n }\n\n const { userId, teamId, permission, expiresAt } = ctx.body as {\n userId?: string;\n teamId?: string;\n permission?: string;\n expiresAt?: string;\n };\n\n if (!userId && !teamId) {\n return {\n status: 400,\n body: { error: 'Either userId or teamId must be provided' },\n };\n }\n\n if (!ctx.identity) {\n return {\n status: 401,\n body: { error: 'Unauthorized', message: 'Authentication required' },\n };\n }\n\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n const callerPermission = isAdmin\n ? 'owner'\n : await getEffectiveFlowPermission(ctx.database, flowId, ctx.identity);\n\n if (callerPermission !== 'owner') {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Owner access is required to manage sharing' },\n };\n }\n\n if (!permission || !['owner', 'editor', 'operator', 'viewer'].includes(permission)) {\n return {\n status: 400,\n body: { error: 'permission must be one of: owner, editor, operator, viewer' },\n };\n }\n\n const access = await ctx.core.grantFlowAccess({\n flowId,\n userId,\n teamId,\n permission: permission as 'owner' | 'editor' | 'operator' | 'viewer',\n grantedBy: ctx.identity?.id,\n expiresAt,\n });\n return { status: 201, body: access };\n },\n },\n\n {\n method: 'DELETE',\n path: '/rbac/flows/:flowId/access/:accessId',\n permission: 'flow:read',\n handler: async (ctx) => {\n const { flowId, accessId } = ctx.params;\n if (!flowId || !accessId) {\n return { status: 400, body: { error: 'Missing flowId or accessId parameter' } };\n }\n\n if (!ctx.core.isFlowAccessTableEnabled()) {\n return {\n status: 501,\n body: {\n error: 'Not Implemented',\n message:\n 'Flow access table not enabled. Set auth.useFlowAccessTable: true in config.',\n },\n };\n }\n\n if (!ctx.identity) {\n return {\n status: 401,\n body: { error: 'Unauthorized', message: 'Authentication required' },\n };\n }\n\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n const callerPermission = isAdmin\n ? 'owner'\n : await getEffectiveFlowPermission(ctx.database, flowId, ctx.identity);\n\n if (callerPermission !== 'owner') {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Owner access is required to manage sharing' },\n };\n }\n\n await ctx.core.revokeFlowAccess(accessId);\n return { status: 204, body: null };\n },\n },\n\n {\n method: 'GET',\n path: '/rbac/flows/accessible',\n isPublic: false,\n handler: async (ctx) => {\n if (!ctx.core.isFlowAccessTableEnabled()) {\n return {\n status: 501,\n body: {\n error: 'Not Implemented',\n message:\n 'Flow access table not enabled. Set auth.useFlowAccessTable: true in config.',\n },\n };\n }\n\n const identity = ctx.identity;\n if (!identity) {\n return {\n status: 401,\n body: { error: 'Unauthorized', message: 'Authentication required' },\n };\n }\n\n // Admins see all flows with implicit owner permission\n const isAdmin = ctx.core.getPermissions(identity).includes('admin:*');\n if (isAdmin) {\n return {\n status: 200,\n body: { flowIds: [], permissions: {}, isAdmin: true },\n };\n }\n\n const { flowIds, permissions } = await getCurrentUserAccessibleFlows(\n ctx.database,\n identity,\n );\n\n return {\n status: 200,\n body: {\n flowIds,\n permissions,\n isAdmin: false,\n },\n };\n },\n },\n\n {\n method: 'GET',\n path: '/rbac/flows/:flowId/effective-access',\n permission: 'flow:read',\n handler: async (ctx) => {\n const flowId = ctx.params.flowId;\n if (!flowId) {\n return { status: 400, body: { error: 'Missing flowId parameter' } };\n }\n if (!ctx.identity) {\n return {\n status: 401,\n body: { error: 'Unauthorized', message: 'Authentication required' },\n };\n }\n\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n const callerPermission = isAdmin\n ? 'owner'\n : await getEffectiveFlowPermission(ctx.database, flowId, ctx.identity);\n\n if (!callerPermission) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'No access to this flow' },\n };\n }\n\n const records = await listAllEffectiveFlowAccessRecords(ctx.database, flowId);\n\n return {\n status: 200,\n body: {\n flowId,\n scopeId: await getFlowScopeId(ctx.database, flowId),\n records,\n },\n };\n },\n },\n\n {\n method: 'PUT',\n path: '/rbac/flows/:flowId/scope',\n permission: 'flow:update',\n handler: async (ctx) => {\n const flowId = ctx.params.flowId;\n const { scopeId } = ctx.body as { scopeId?: string | null };\n if (!flowId) {\n return { status: 400, body: { error: 'Missing flowId parameter' } };\n }\n if (!ctx.identity) {\n return {\n status: 401,\n body: { error: 'Unauthorized', message: 'Authentication required' },\n };\n }\n\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n const callerPermission = isAdmin\n ? 'owner'\n : await getEffectiveFlowPermission(ctx.database, flowId, ctx.identity);\n if (callerPermission !== 'owner') {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Owner access is required to move flows' },\n };\n }\n\n if (scopeId) {\n const scopeRows = await ctx.database.query<{ id: string }>(\n 'SELECT id FROM rbac_teams WHERE id = ?',\n [scopeId],\n );\n if (scopeRows.length === 0) {\n return { status: 404, body: { error: 'Scope not found' } };\n }\n }\n\n await ctx.database.execute('UPDATE flows SET scope_id = ? WHERE id = ?', [\n scopeId ?? null,\n flowId,\n ]);\n return { status: 200, body: { success: true, flowId, scopeId: scopeId ?? null } };\n },\n },\n\n // ── Teams Management ──\n\n ...(enableTeams\n ? [\n {\n method: 'GET' as const,\n path: '/rbac/scopes/tree',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n\n const [teamRows, flowRows, memberRows, accessRows, teamRoleRows] =\n await Promise.all([\n ctx.database.query<{\n id: string;\n name: string;\n description: string | null;\n parent_id: string | null;\n created_by: string | null;\n created_at: string;\n updated_at: string | null;\n }>(\n 'SELECT id, name, description, parent_id, created_by, created_at, updated_at FROM rbac_teams ORDER BY name',\n ),\n ctx.database.query<{ id: string; name: string; scope_id: string | null }>(\n 'SELECT id, name, scope_id FROM flows ORDER BY name',\n ),\n ctx.database.query<{ team_id: string; member_count: number }>(\n 'SELECT team_id, COUNT(*) AS member_count FROM rbac_team_members GROUP BY team_id',\n ),\n ctx.database.query<{ scope_id: string; access_count: number }>(\n 'SELECT scope_id, COUNT(*) AS access_count FROM rbac_scope_access GROUP BY scope_id',\n ),\n ctx.database.query<{ scope_id: string; permission: FlowAccessPermission }>(\n 'SELECT scope_id, permission FROM rbac_scope_access WHERE team_id = scope_id AND team_id IS NOT NULL',\n ),\n ]);\n\n const memberCounts = new Map(\n memberRows.map((row) => [row.team_id, Number(row.member_count)]),\n );\n const accessCounts = new Map(\n accessRows.map((row) => [row.scope_id, Number(row.access_count)]),\n );\n const teamPermissions = new Map(\n teamRoleRows.map((row) => [row.scope_id, row.permission]),\n );\n\n const nodeMap = new Map<string, ScopeTreeNode>();\n for (const row of teamRows) {\n nodeMap.set(row.id, {\n ...normalizeTeamRow(row),\n children: [],\n flows: [],\n directAccessCount: accessCounts.get(row.id) ?? 0,\n memberCount: memberCounts.get(row.id) ?? 0,\n teamPermission: teamPermissions.get(row.id) ?? null,\n });\n }\n\n const roots: ScopeTreeNode[] = [];\n for (const node of nodeMap.values()) {\n if (node.parentId && nodeMap.has(node.parentId)) {\n nodeMap.get(node.parentId)?.children.push(node);\n } else {\n roots.push(node);\n }\n }\n\n const unscopedFlows: Array<{ id: string; name: string; scopeId: string | null }> =\n [];\n for (const flow of flowRows) {\n const mapped = { id: flow.id, name: flow.name, scopeId: flow.scope_id };\n if (flow.scope_id && nodeMap.has(flow.scope_id)) {\n nodeMap.get(flow.scope_id)?.flows.push(mapped);\n } else {\n unscopedFlows.push(mapped);\n }\n }\n\n return { status: 200, body: { scopes: roots, unscopedFlows } };\n },\n },\n\n {\n method: 'GET' as const,\n path: '/rbac/scopes/:scopeId/access',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n\n const rows = await ctx.database.query<Record<string, unknown>>(\n 'SELECT id, scope_id, user_id, team_id, permission, granted_by, granted_at FROM rbac_scope_access WHERE scope_id = ?',\n [ctx.params.scopeId],\n );\n return { status: 200, body: { access: rows.map(normalizeScopeAccessRecord) } };\n },\n },\n\n {\n method: 'POST' as const,\n path: '/rbac/scopes/:scopeId/access',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n const { scopeId } = ctx.params;\n const { userId, teamId, permission } = ctx.body as {\n userId?: string;\n teamId?: string;\n permission?: string;\n };\n\n if (!scopeId) {\n return { status: 400, body: { error: 'Missing scopeId parameter' } };\n }\n if (!userId && !teamId) {\n return {\n status: 400,\n body: { error: 'Either userId or teamId must be provided' },\n };\n }\n if (userId && teamId) {\n return {\n status: 400,\n body: { error: 'Provide either userId or teamId, not both' },\n };\n }\n if (teamId && teamId !== scopeId) {\n return {\n status: 400,\n body: { error: 'Teams can only hold a role on their own scope' },\n };\n }\n if (!isFlowAccessPermission(permission)) {\n return {\n status: 400,\n body: { error: 'permission must be one of: owner, editor, operator, viewer' },\n };\n }\n\n const existing = await ctx.database.query<{ id: string }>(\n 'SELECT id FROM rbac_scope_access WHERE scope_id = ? AND user_id IS ? AND team_id IS ?',\n [scopeId, userId ?? null, teamId ?? null],\n );\n\n const now = new Date().toISOString();\n if (existing[0]) {\n await ctx.database.execute(\n 'UPDATE rbac_scope_access SET permission = ?, granted_by = ?, granted_at = ? WHERE id = ?',\n [permission, ctx.identity.id, now, existing[0].id],\n );\n return {\n status: 200,\n body: {\n id: existing[0].id,\n scopeId,\n userId: userId ?? null,\n teamId: teamId ?? null,\n permission,\n grantedBy: ctx.identity.id,\n grantedAt: now,\n },\n };\n }\n\n const id = crypto.randomUUID();\n await ctx.database.execute(\n 'INSERT INTO rbac_scope_access (id, scope_id, user_id, team_id, permission, granted_by, granted_at) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [id, scopeId, userId ?? null, teamId ?? null, permission, ctx.identity.id, now],\n );\n\n return {\n status: 201,\n body: {\n id,\n scopeId,\n userId: userId ?? null,\n teamId: teamId ?? null,\n permission,\n grantedBy: ctx.identity.id,\n grantedAt: now,\n },\n };\n },\n },\n\n {\n method: 'DELETE' as const,\n path: '/rbac/scopes/:scopeId/access/:accessId',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n await ctx.database.execute('DELETE FROM rbac_scope_access WHERE id = ?', [\n ctx.params.accessId,\n ]);\n return { status: 204, body: null };\n },\n },\n\n {\n method: 'POST' as const,\n path: '/rbac/preview-move',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n const { type, id, targetScopeId } = ctx.body as {\n type?: 'flow' | 'scope';\n id?: string;\n targetScopeId?: string | null;\n };\n\n if (!type || !id) {\n return { status: 400, body: { error: 'type and id are required' } };\n }\n\n let affectedFlowIds: string[] = [];\n let itemName = id;\n if (type === 'flow') {\n const rows = await ctx.database.query<{ id: string; name: string }>(\n 'SELECT id, name FROM flows WHERE id = ?',\n [id],\n );\n if (!rows[0]) {\n return { status: 404, body: { error: 'Flow not found' } };\n }\n itemName = rows[0].name;\n affectedFlowIds = [id];\n } else {\n const scopeRows = await ctx.database.query<{ id: string; name: string }>(\n 'SELECT id, name FROM rbac_teams WHERE id = ?',\n [id],\n );\n if (!scopeRows[0]) {\n return { status: 404, body: { error: 'Scope not found' } };\n }\n itemName = scopeRows[0].name;\n const descendantScopeIds = await getDescendantScopeIds(ctx.database, id);\n if (targetScopeId && descendantScopeIds.includes(targetScopeId)) {\n return {\n status: 400,\n body: { error: 'Cannot move a scope into itself or its descendant' },\n };\n }\n const flowRows = await ctx.database.query<{ id: string }>(\n `SELECT id FROM flows WHERE scope_id IN (${createInClause(descendantScopeIds.length)})`,\n descendantScopeIds,\n );\n affectedFlowIds = flowRows.map((row) => row.id);\n }\n\n const targetPath = await getScopePath(ctx.database, targetScopeId ?? null);\n const targetAncestorIds = targetScopeId\n ? await getAncestorScopeIds(ctx.database, targetScopeId)\n : [];\n const targetScopeAccess = await listAllScopeAccessForScopeIds(\n ctx.database,\n targetAncestorIds,\n );\n\n const gainedEntries = new Map<\n string,\n {\n userId?: string | null;\n teamId?: string | null;\n permission: FlowAccessPermission;\n source: string;\n }\n >();\n let unchanged = 0;\n\n for (const flowId of affectedFlowIds) {\n const currentRecords = await listAllEffectiveFlowAccessForPreview(\n ctx.database,\n flowId,\n );\n const currentPermissions = new Map<string, FlowAccessPermission>();\n for (const record of currentRecords) {\n const permission = toFlowAccessPermission(record.permission);\n if (!permission) {\n continue;\n }\n const key = buildAccessKey(record);\n currentPermissions.set(\n key,\n getHigherPermission(currentPermissions.get(key) ?? null, permission) ??\n permission,\n );\n }\n\n for (const record of targetScopeAccess) {\n const key = buildAccessKey(record);\n const existingPermission = currentPermissions.get(key) ?? null;\n if (\n existingPermission &&\n FLOW_PERMISSION_LEVELS[existingPermission] >=\n FLOW_PERMISSION_LEVELS[record.permission]\n ) {\n unchanged += 1;\n continue;\n }\n gainedEntries.set(key, {\n userId: record.userId,\n teamId: record.teamId,\n permission: record.permission,\n source: `from ${targetPath.at(-1) ?? 'root'}`,\n });\n }\n }\n\n const gained = await resolveAccessChangeNames(\n ctx.database,\n Array.from(gainedEntries.values()),\n );\n\n return {\n status: 200,\n body: {\n item: { id, name: itemName, type },\n target: {\n id: targetScopeId ?? null,\n name: targetPath.at(-1) ?? 'Unscoped',\n path: targetPath,\n },\n affectedFlows: affectedFlowIds.length,\n accessChanges: {\n gained,\n unchanged,\n },\n },\n };\n },\n },\n\n // List all teams\n {\n method: 'GET' as const,\n path: '/rbac/teams',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n\n const rows = await ctx.database.query<{\n id: string;\n name: string;\n description: string | null;\n parent_id: string | null;\n created_by: string | null;\n created_at: string;\n updated_at: string | null;\n }>(\n 'SELECT id, name, description, parent_id, created_by, created_at, updated_at FROM rbac_teams ORDER BY name',\n );\n\n const teams = rows.map((r) => normalizeTeamRow(r));\n\n return { status: 200, body: { teams } };\n },\n },\n\n // Create a team (admin only)\n {\n method: 'POST' as const,\n path: '/rbac/teams',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n const { name, description, parentId } = ctx.body as {\n name?: string;\n description?: string;\n parentId?: string | null;\n };\n if (!name?.trim()) {\n return { status: 400, body: { error: 'Team name is required' } };\n }\n\n if (parentId) {\n const parentRows = await ctx.database.query<{ id: string }>(\n 'SELECT id FROM rbac_teams WHERE id = ?',\n [parentId],\n );\n if (parentRows.length === 0) {\n return { status: 404, body: { error: 'Parent scope not found' } };\n }\n }\n\n const id = crypto.randomUUID();\n const now = new Date().toISOString();\n await ctx.database.execute(\n 'INSERT INTO rbac_teams (id, name, description, parent_id, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [\n id,\n name.trim(),\n description?.trim() || null,\n parentId ?? null,\n ctx.identity.id,\n now,\n now,\n ],\n );\n\n return {\n status: 201,\n body: {\n id,\n name: name.trim(),\n description: description?.trim() || null,\n parentId: parentId ?? null,\n createdBy: ctx.identity.id,\n createdAt: now,\n updatedAt: now,\n },\n };\n },\n },\n\n // Update a team (admin only)\n {\n method: 'PUT' as const,\n path: '/rbac/teams/:teamId',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n const { teamId } = ctx.params;\n const { name, description, parentId } = ctx.body as {\n name?: string;\n description?: string;\n parentId?: string | null;\n };\n\n const existing = await ctx.database.query<{ id: string }>(\n 'SELECT id FROM rbac_teams WHERE id = ?',\n [teamId],\n );\n if (existing.length === 0) {\n return { status: 404, body: { error: 'Team not found' } };\n }\n\n if (parentId === teamId) {\n return { status: 400, body: { error: 'A scope cannot be its own parent' } };\n }\n if (parentId) {\n const parentRows = await ctx.database.query<{ id: string }>(\n 'SELECT id FROM rbac_teams WHERE id = ?',\n [parentId],\n );\n if (parentRows.length === 0) {\n return { status: 404, body: { error: 'Parent scope not found' } };\n }\n const descendantScopeIds = await getDescendantScopeIds(ctx.database, teamId);\n if (descendantScopeIds.includes(parentId)) {\n return {\n status: 400,\n body: { error: 'Cannot move a scope into itself or its descendant' },\n };\n }\n }\n\n const updates: string[] = [];\n const values: unknown[] = [];\n if (name !== undefined) {\n if (!name.trim()) {\n return { status: 400, body: { error: 'Team name cannot be empty' } };\n }\n updates.push('name = ?');\n values.push(name.trim());\n }\n if (description !== undefined) {\n updates.push('description = ?');\n values.push(description?.trim() || null);\n }\n if (parentId !== undefined) {\n updates.push('parent_id = ?');\n values.push(parentId ?? null);\n }\n if (updates.length === 0) {\n return { status: 400, body: { error: 'No fields to update' } };\n }\n\n updates.push('updated_at = ?');\n values.push(new Date().toISOString());\n values.push(teamId);\n\n await ctx.database.execute(\n `UPDATE rbac_teams SET ${updates.join(', ')} WHERE id = ?`,\n values,\n );\n\n return { status: 200, body: { success: true } };\n },\n },\n\n // Delete a team (admin only)\n {\n method: 'DELETE' as const,\n path: '/rbac/teams/:teamId',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n const { teamId } = ctx.params;\n const teams = await ctx.database.query<{ id: string; parent_id: string | null }>(\n 'SELECT id, parent_id FROM rbac_teams WHERE id = ?',\n [teamId],\n );\n if (teams.length === 0) {\n return { status: 404, body: { error: 'Team not found' } };\n }\n\n const parentId = teams[0].parent_id ?? null;\n await ctx.database.execute('UPDATE flows SET scope_id = ? WHERE scope_id = ?', [\n parentId,\n teamId,\n ]);\n\n // CASCADE on team_members handles cleanup\n await ctx.database.execute('DELETE FROM rbac_teams WHERE id = ?', [teamId]);\n return { status: 204, body: null };\n },\n },\n\n // Get team with members\n {\n method: 'GET' as const,\n path: '/rbac/teams/:teamId',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n\n const { teamId } = ctx.params;\n const teams = await ctx.database.query<{\n id: string;\n name: string;\n description: string | null;\n parent_id: string | null;\n created_by: string | null;\n created_at: string;\n updated_at: string | null;\n }>(\n 'SELECT id, name, description, parent_id, created_by, created_at, updated_at FROM rbac_teams WHERE id = ?',\n [teamId],\n );\n\n if (teams.length === 0) {\n return { status: 404, body: { error: 'Team not found' } };\n }\n\n const members = await ctx.database.query<{\n id: string;\n team_id: string;\n user_id: string;\n created_at: string;\n }>(\n 'SELECT id, team_id, user_id, created_at FROM rbac_team_members WHERE team_id = ?',\n [teamId],\n );\n\n const team = teams[0];\n return {\n status: 200,\n body: {\n ...normalizeTeamRow(team),\n members: members.map((m) => ({\n id: m.id,\n teamId: m.team_id,\n userId: m.user_id,\n createdAt: m.created_at,\n })),\n },\n };\n },\n },\n\n // Add member to team (admin only)\n {\n method: 'POST' as const,\n path: '/rbac/teams/:teamId/members',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n const { teamId } = ctx.params;\n const { userId } = ctx.body as { userId?: string };\n if (!userId?.trim()) {\n return { status: 400, body: { error: 'userId is required' } };\n }\n\n // Check team exists\n const teams = await ctx.database.query<{ id: string }>(\n 'SELECT id FROM rbac_teams WHERE id = ?',\n [teamId],\n );\n if (teams.length === 0) {\n return { status: 404, body: { error: 'Team not found' } };\n }\n\n // Check not already a member\n const existing = await ctx.database.query<{ id: string }>(\n 'SELECT id FROM rbac_team_members WHERE team_id = ? AND user_id = ?',\n [teamId, userId.trim()],\n );\n if (existing.length > 0) {\n return { status: 409, body: { error: 'User is already a member of this team' } };\n }\n\n const id = crypto.randomUUID();\n const now = new Date().toISOString();\n await ctx.database.execute(\n 'INSERT INTO rbac_team_members (id, team_id, user_id, created_at) VALUES (?, ?, ?, ?)',\n [id, teamId, userId.trim(), now],\n );\n\n return {\n status: 201,\n body: { id, teamId, userId: userId.trim(), createdAt: now },\n };\n },\n },\n\n // Remove member from team (admin only)\n {\n method: 'DELETE' as const,\n path: '/rbac/teams/:teamId/members/:userId',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n const { teamId, userId } = ctx.params;\n await ctx.database.execute(\n 'DELETE FROM rbac_team_members WHERE team_id = ? AND user_id = ?',\n [teamId, userId],\n );\n return { status: 204, body: null };\n },\n },\n\n // Get teams for current user (for identity resolution)\n {\n method: 'GET' as const,\n path: '/rbac/my-teams',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n\n const rows = await ctx.database.query<{\n id: string;\n name: string;\n description: string | null;\n parent_id: string | null;\n created_by: string | null;\n created_at: string;\n updated_at: string | null;\n }>(\n 'SELECT t.id, t.name, t.description, t.parent_id, t.created_by, t.created_at, t.updated_at ' +\n 'FROM rbac_teams t INNER JOIN rbac_team_members tm ON t.id = tm.team_id ' +\n 'WHERE tm.user_id = ? ORDER BY t.name',\n [ctx.identity.id],\n );\n\n const teams = rows.map((r) => normalizeTeamRow(r));\n\n return { status: 200, body: { teams } };\n },\n },\n ]\n : []),\n ],\n\n // ─── Lifecycle Hooks ──────────────────────────────────────\n\n hooks: {\n // Enrich identity with teamIds after auth plugin resolves the session.\n // This runs after the auth plugin's onRequest hook since RBAC is registered\n // after auth. We query rbac_team_members to get the user's team IDs.\n onRequest: enableTeams\n ? async (\n _request: Request,\n context: { path: string; method: string; identity: InvectIdentity | null },\n ) => {\n if (!context.identity || !capturedDbApi) {\n return;\n }\n // Skip if teamIds already populated (custom mapUser)\n if (context.identity.teamIds && context.identity.teamIds.length > 0) {\n return;\n }\n\n try {\n const rows = await capturedDbApi.query<{ team_id: string }>(\n 'SELECT team_id FROM rbac_team_members WHERE user_id = ?',\n [context.identity.id],\n );\n if (rows.length > 0) {\n context.identity = {\n ...context.identity,\n teamIds: rows.map((r) => r.team_id),\n };\n }\n } catch {\n // Silently ignore — table may not exist yet during initial setup\n }\n }\n : undefined,\n\n // Enforce flow-level ACLs on authorization checks\n onAuthorize: async (context) => {\n if (!useFlowAccessTable) {\n return; // Defer to default RBAC\n }\n\n const { identity, resource, action } = context;\n\n // Only enforce for flow-related resources with specific IDs\n if (!identity || !resource?.id) {\n return;\n }\n\n if (!FLOW_RESOURCE_TYPES.has(resource.type)) {\n return;\n }\n\n if (identity.permissions?.includes('admin:*') || identity.role === 'admin') {\n return { allowed: true };\n }\n\n const database = context.database ?? capturedDbApi;\n if (!database) {\n return;\n }\n\n const effectivePermission = await getEffectiveFlowPermission(\n database,\n resource.id,\n identity,\n );\n if (!effectivePermission) {\n return { allowed: false };\n }\n\n const requiredPermission = mapActionToRequiredPermission(action);\n return {\n allowed:\n FLOW_PERMISSION_LEVELS[effectivePermission] >=\n FLOW_PERMISSION_LEVELS[requiredPermission],\n };\n },\n\n // Auto-grant owner access when a flow is created\n afterFlowRun: async (_context) => {\n // This hook is for flow *runs*, not flow creation.\n // Flow creation auto-grant is handled by the core when\n // auth.useFlowAccessTable is true.\n },\n },\n\n // ─── Error Codes ──────────────────────────────────────────\n\n $ERROR_CODES: {\n 'rbac:no_access': {\n message: 'You do not have access to this flow.',\n status: 403,\n },\n 'rbac:insufficient_permission': {\n message: 'Your access level is insufficient for this operation.',\n status: 403,\n },\n 'rbac:auth_required': {\n message: 'Authentication is required. Please sign in.',\n status: 401,\n },\n 'rbac:plugin_missing': {\n message: 'The RBAC plugin requires the @invect/user-auth plugin.',\n status: 500,\n },\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../src/backend/plugin.ts"],"sourcesContent":["/**\n * @invect/rbac — Backend Plugin\n *\n * RBAC plugin that provides:\n * - Flow access management endpoints (grant/revoke/list)\n * - Auth info endpoints (/auth/me, /auth/roles)\n * - Authorization hooks (enforces flow-level ACLs)\n * - UI manifest for the frontend plugin to render\n *\n * Requires the @invect/user-auth (better-auth) plugin to be loaded first\n * for session resolution. This plugin handles the *authorization* layer\n * on top of that authentication.\n */\n\nimport type {\n InvectPlugin,\n InvectPluginContext,\n InvectPermission,\n InvectPluginSchema,\n InvectIdentity,\n PluginDatabaseApi,\n PluginEndpointContext,\n} from '@invect/core';\nimport type {\n EffectiveAccessRecord,\n FlowAccessPermission,\n FlowAccessRecord,\n MovePreviewAccessChange,\n PluginUIManifest,\n ScopeAccessRecord,\n ScopeTreeNode,\n Team,\n} from '../shared/types';\n\n// ─────────────────────────────────────────────────────────────\n// Team ID Resolution Helper\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Resolve team IDs for a user from the rbac_team_members table.\n * Use this in a custom `mapUser` for the auth plugin to populate\n * `identity.teamIds` automatically.\n *\n * @example\n * ```ts\n * import { resolveTeamIds } from '@invect/rbac/backend';\n *\n * userAuth({\n * auth,\n * mapUser: async (user, session) => ({\n * id: user.id,\n * name: user.name ?? undefined,\n * role: user.role === 'admin' ? 'admin' : 'user',\n * teamIds: await resolveTeamIds(db, user.id),\n * }),\n * });\n * ```\n */\nexport async function resolveTeamIds(db: PluginDatabaseApi, userId: string): Promise<string[]> {\n const rows = await db.query<{ team_id: string }>(\n 'SELECT team_id FROM rbac_team_members WHERE user_id = ?',\n [userId],\n );\n return rows.map((r) => r.team_id);\n}\n\n// ─────────────────────────────────────────────────────────────\n// Types\n// ─────────────────────────────────────────────────────────────\n\nexport interface RbacPluginOptions {\n /**\n * Enable the flow access table for database-backed per-flow permissions.\n * When enabled, the plugin will check flow-level ACLs in addition to\n * role-based permissions. Requires `auth.useFlowAccessTable: true` in\n * the Invect config.\n *\n * @default true\n */\n useFlowAccessTable?: boolean;\n\n /**\n * Permission required to access the RBAC flows page.\n *\n * @default 'flow:read'\n */\n adminPermission?: InvectPermission;\n\n /**\n * Enable teams feature for grouping users.\n *\n * @default true\n */\n enableTeams?: boolean;\n}\n\nconst FLOW_RESOURCE_TYPES = new Set(['flow', 'flow-version', 'flow-run', 'node-execution']);\n\nconst FLOW_PERMISSION_LEVELS: Record<FlowAccessPermission, number> = {\n viewer: 1,\n operator: 2,\n editor: 3,\n owner: 4,\n};\n\nfunction isFlowAccessPermission(value: unknown): value is FlowAccessPermission {\n return value === 'viewer' || value === 'operator' || value === 'editor' || value === 'owner';\n}\n\nfunction toFlowAccessPermission(value: unknown): FlowAccessPermission | null {\n return isFlowAccessPermission(value) ? value : null;\n}\n\nfunction getHigherPermission(\n left: FlowAccessPermission | null,\n right: FlowAccessPermission | null,\n): FlowAccessPermission | null {\n if (!left) {\n return right;\n }\n if (!right) {\n return left;\n }\n return FLOW_PERMISSION_LEVELS[right] > FLOW_PERMISSION_LEVELS[left] ? right : left;\n}\n\nfunction createInClause(count: number): string {\n return Array.from({ length: count }, () => '?').join(', ');\n}\n\nfunction mapActionToRequiredPermission(action: string): FlowAccessPermission {\n if (action.includes('delete') || action.includes('share') || action.includes('admin')) {\n return 'owner';\n }\n if (action.includes('update') || action.includes('write') || action.includes('create')) {\n return 'editor';\n }\n if (action.includes('run') || action.includes('execute')) {\n return 'operator';\n }\n return 'viewer';\n}\n\nfunction normalizeFlowAccessRecord(row: Record<string, unknown>): FlowAccessRecord {\n return {\n id: String(row.id),\n flowId: String(row.flowId ?? row.flow_id),\n userId: (row.userId ?? row.user_id) ? String(row.userId ?? row.user_id) : null,\n teamId: (row.teamId ?? row.team_id) ? String(row.teamId ?? row.team_id) : null,\n permission: String(row.permission) as FlowAccessPermission,\n grantedBy: (row.grantedBy ?? row.granted_by) ? String(row.grantedBy ?? row.granted_by) : null,\n grantedAt: String(row.grantedAt ?? row.granted_at),\n expiresAt: (row.expiresAt ?? row.expires_at) ? String(row.expiresAt ?? row.expires_at) : null,\n };\n}\n\nfunction normalizeScopeAccessRecord(row: Record<string, unknown>): ScopeAccessRecord {\n return {\n id: String(row.id),\n scopeId: String(row.scopeId ?? row.scope_id),\n userId: (row.userId ?? row.user_id) ? String(row.userId ?? row.user_id) : null,\n teamId: (row.teamId ?? row.team_id) ? String(row.teamId ?? row.team_id) : null,\n permission: String(row.permission) as FlowAccessPermission,\n grantedBy: (row.grantedBy ?? row.granted_by) ? String(row.grantedBy ?? row.granted_by) : null,\n grantedAt: String(row.grantedAt ?? row.granted_at),\n };\n}\n\nfunction normalizeTeamRow(row: {\n id: string;\n name: string;\n description: string | null;\n parent_id?: string | null;\n created_by: string | null;\n created_at: string;\n updated_at: string | null;\n}): Team {\n return {\n id: row.id,\n name: row.name,\n description: row.description,\n parentId: row.parent_id ?? null,\n createdBy: row.created_by,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n };\n}\n\nasync function getFlowScopeId(db: PluginDatabaseApi, flowId: string): Promise<string | null> {\n const rows = await db.query<{ scope_id: string | null }>(\n 'SELECT scope_id FROM flows WHERE id = ?',\n [flowId],\n );\n return rows[0]?.scope_id ?? null;\n}\n\nasync function getAncestorScopeIds(db: PluginDatabaseApi, scopeId: string): Promise<string[]> {\n const rows = await db.query<{ id: string }>(\n `WITH RECURSIVE ancestors AS (\n SELECT id, parent_id FROM rbac_teams WHERE id = ?\n UNION ALL\n SELECT parent.id, parent.parent_id\n FROM rbac_teams parent\n INNER JOIN ancestors current ON parent.id = current.parent_id\n )\n SELECT id FROM ancestors`,\n [scopeId],\n );\n return rows.map((row) => row.id);\n}\n\nasync function getDescendantScopeIds(db: PluginDatabaseApi, scopeId: string): Promise<string[]> {\n const rows = await db.query<{ id: string }>(\n `WITH RECURSIVE descendants AS (\n SELECT id FROM rbac_teams WHERE id = ?\n UNION ALL\n SELECT child.id\n FROM rbac_teams child\n INNER JOIN descendants current ON child.parent_id = current.id\n )\n SELECT id FROM descendants`,\n [scopeId],\n );\n return rows.map((row) => row.id);\n}\n\nasync function listScopeAccessForScopeIds(\n db: PluginDatabaseApi,\n scopeIds: string[],\n userId: string,\n teamIds: string[],\n): Promise<ScopeAccessRecord[]> {\n if (scopeIds.length === 0) {\n return [];\n }\n\n const params: unknown[] = [...scopeIds, userId];\n let sql =\n `SELECT id, scope_id, user_id, team_id, permission, granted_by, granted_at ` +\n `FROM rbac_scope_access WHERE (scope_id IN (${createInClause(scopeIds.length)}) ` +\n `AND user_id = ?)`;\n\n if (teamIds.length > 0) {\n sql += ` OR (scope_id IN (${createInClause(scopeIds.length)}) AND team_id IN (${createInClause(teamIds.length)}))`;\n params.push(...scopeIds, ...teamIds);\n }\n\n const rows = await db.query<Record<string, unknown>>(sql, params);\n return rows.map(normalizeScopeAccessRecord);\n}\n\nasync function listDirectFlowAccessForIdentity(\n db: PluginDatabaseApi,\n flowId: string,\n userId: string,\n teamIds: string[],\n): Promise<FlowAccessRecord[]> {\n const params: unknown[] = [flowId, userId];\n let sql =\n 'SELECT id, flow_id, user_id, team_id, permission, granted_by, granted_at, expires_at ' +\n 'FROM flow_access WHERE (flow_id = ? AND user_id = ?)';\n\n if (teamIds.length > 0) {\n sql += ` OR (flow_id = ? AND team_id IN (${createInClause(teamIds.length)}))`;\n params.push(flowId, ...teamIds);\n }\n\n const rows = await db.query<Record<string, unknown>>(sql, params);\n const now = Date.now();\n return rows\n .map(normalizeFlowAccessRecord)\n .filter((record) => !record.expiresAt || new Date(record.expiresAt).getTime() > now);\n}\n\nasync function getEffectiveFlowAccessRecords(\n db: PluginDatabaseApi,\n flowId: string,\n userId: string,\n teamIds: string[],\n): Promise<EffectiveAccessRecord[]> {\n const directRecords = (\n await listDirectFlowAccessForIdentity(db, flowId, userId, teamIds)\n ).map<EffectiveAccessRecord>((record) => ({ ...record, source: 'direct' }));\n\n const scopeId = await getFlowScopeId(db, flowId);\n if (!scopeId) {\n return directRecords;\n }\n\n const ancestorIds = await getAncestorScopeIds(db, scopeId);\n const inheritedRows = await listScopeAccessForScopeIds(db, ancestorIds, userId, teamIds);\n if (inheritedRows.length === 0) {\n return directRecords;\n }\n\n const scopeRows = await db.query<{ id: string; name: string }>(\n `SELECT id, name FROM rbac_teams WHERE id IN (${createInClause(ancestorIds.length)})`,\n ancestorIds,\n );\n const scopeNames = new Map(scopeRows.map((row) => [row.id, row.name]));\n\n return [\n ...directRecords,\n ...inheritedRows.map<EffectiveAccessRecord>((record) => ({\n id: record.id,\n flowId,\n userId: record.userId,\n teamId: record.teamId,\n permission: record.permission,\n grantedBy: record.grantedBy,\n grantedAt: record.grantedAt,\n expiresAt: null,\n source: 'inherited',\n scopeId: record.scopeId,\n scopeName: scopeNames.get(record.scopeId) ?? null,\n })),\n ];\n}\n\nasync function getEffectiveFlowPermission(\n db: PluginDatabaseApi,\n flowId: string,\n identity: InvectIdentity,\n): Promise<FlowAccessPermission | null> {\n const records = await getEffectiveFlowAccessRecords(\n db,\n flowId,\n identity.id,\n identity.teamIds ?? [],\n );\n\n let highest: FlowAccessPermission | null = null;\n for (const record of records) {\n highest = getHigherPermission(highest, record.permission);\n }\n return highest;\n}\n\nasync function getCurrentUserAccessibleFlows(\n db: PluginDatabaseApi,\n identity: InvectIdentity,\n): Promise<{ flowIds: string[]; permissions: Record<string, FlowAccessPermission | null> }> {\n const rows = await db.query<{ id: string }>('SELECT id FROM flows');\n const permissions: Record<string, FlowAccessPermission | null> = {};\n\n await Promise.all(\n rows.map(async (row) => {\n permissions[row.id] = await getEffectiveFlowPermission(db, row.id, identity);\n }),\n );\n\n const flowIds = rows.map((row) => row.id).filter((flowId) => permissions[flowId]);\n return { flowIds, permissions };\n}\n\nasync function getScopePath(db: PluginDatabaseApi, scopeId: string | null): Promise<string[]> {\n if (!scopeId) {\n return [];\n }\n\n const rows = await db.query<{ id: string; name: string }>(\n `WITH RECURSIVE ancestors AS (\n SELECT id, name, parent_id, 0 AS depth FROM rbac_teams WHERE id = ?\n UNION ALL\n SELECT parent.id, parent.name, parent.parent_id, current.depth + 1\n FROM rbac_teams parent\n INNER JOIN ancestors current ON parent.id = current.parent_id\n )\n SELECT id, name FROM ancestors ORDER BY depth DESC`,\n [scopeId],\n );\n return rows.map((row) => row.name);\n}\n\nasync function listAllScopeAccessForScopeIds(\n db: PluginDatabaseApi,\n scopeIds: string[],\n): Promise<ScopeAccessRecord[]> {\n if (scopeIds.length === 0) {\n return [];\n }\n const rows = await db.query<Record<string, unknown>>(\n `SELECT id, scope_id, user_id, team_id, permission, granted_by, granted_at\n FROM rbac_scope_access\n WHERE scope_id IN (${createInClause(scopeIds.length)})`,\n scopeIds,\n );\n return rows.map(normalizeScopeAccessRecord);\n}\n\nasync function listAllDirectFlowAccess(\n db: PluginDatabaseApi,\n flowId: string,\n): Promise<FlowAccessRecord[]> {\n const rows = await db.query<Record<string, unknown>>(\n 'SELECT id, flow_id, user_id, team_id, permission, granted_by, granted_at, expires_at FROM flow_access WHERE flow_id = ?',\n [flowId],\n );\n const now = Date.now();\n return rows\n .map(normalizeFlowAccessRecord)\n .filter((record) => !record.expiresAt || new Date(record.expiresAt).getTime() > now);\n}\n\nasync function listAllEffectiveFlowAccessForPreview(\n db: PluginDatabaseApi,\n flowId: string,\n overrideScopeId?: string | null,\n): Promise<Array<FlowAccessRecord | ScopeAccessRecord>> {\n const direct = await listAllDirectFlowAccess(db, flowId);\n const scopeId =\n overrideScopeId === undefined ? await getFlowScopeId(db, flowId) : overrideScopeId;\n if (!scopeId) {\n return direct;\n }\n\n const ancestorIds = await getAncestorScopeIds(db, scopeId);\n const inherited = await listAllScopeAccessForScopeIds(db, ancestorIds);\n return [...direct, ...inherited];\n}\n\nasync function listAllEffectiveFlowAccessRecords(\n db: PluginDatabaseApi,\n flowId: string,\n): Promise<EffectiveAccessRecord[]> {\n const directRecords = (await listAllDirectFlowAccess(db, flowId)).map<EffectiveAccessRecord>(\n (record) => ({\n ...record,\n source: 'direct',\n }),\n );\n\n const scopeId = await getFlowScopeId(db, flowId);\n if (!scopeId) {\n return directRecords;\n }\n\n const ancestorIds = await getAncestorScopeIds(db, scopeId);\n const inheritedRows = await listAllScopeAccessForScopeIds(db, ancestorIds);\n if (inheritedRows.length === 0) {\n return directRecords;\n }\n\n const scopeRows = await db.query<{ id: string; name: string }>(\n `SELECT id, name FROM rbac_teams WHERE id IN (${createInClause(ancestorIds.length)})`,\n ancestorIds,\n );\n const scopeNames = new Map(scopeRows.map((row) => [row.id, row.name]));\n\n // For team-based inherited grants, expand into per-member user records so the\n // UI shows individual users rather than team entities.\n const teamIds = [\n ...new Set(inheritedRows.map((r) => r.teamId).filter((id): id is string => !!id)),\n ];\n const teamMembersByTeamId = new Map<string, string[]>();\n if (teamIds.length > 0) {\n const memberRows = await db.query<{ team_id: string; user_id: string }>(\n `SELECT team_id, user_id FROM rbac_team_members WHERE team_id IN (${createInClause(teamIds.length)})`,\n teamIds,\n );\n for (const row of memberRows) {\n const members = teamMembersByTeamId.get(row.team_id) ?? [];\n members.push(row.user_id);\n teamMembersByTeamId.set(row.team_id, members);\n }\n }\n\n const expandedInherited: EffectiveAccessRecord[] = [];\n for (const record of inheritedRows) {\n const scopeName = scopeNames.get(record.scopeId) ?? null;\n if (record.teamId) {\n // Expand team grant → one record per team member\n const members = teamMembersByTeamId.get(record.teamId) ?? [];\n for (const userId of members) {\n expandedInherited.push({\n id: `${record.id}:${userId}`,\n flowId,\n userId,\n teamId: null,\n permission: record.permission,\n grantedBy: record.grantedBy,\n grantedAt: record.grantedAt,\n expiresAt: null,\n source: 'inherited',\n scopeId: record.scopeId,\n scopeName,\n });\n }\n } else {\n expandedInherited.push({\n id: record.id,\n flowId,\n userId: record.userId,\n teamId: null,\n permission: record.permission,\n grantedBy: record.grantedBy,\n grantedAt: record.grantedAt,\n expiresAt: null,\n source: 'inherited',\n scopeId: record.scopeId,\n scopeName,\n });\n }\n }\n\n return [...directRecords, ...expandedInherited];\n}\n\nfunction buildAccessKey(record: { userId?: string | null; teamId?: string | null }): string {\n if (record.userId) {\n return `user:${record.userId}`;\n }\n if (record.teamId) {\n return `team:${record.teamId}`;\n }\n return 'unknown';\n}\n\nasync function resolveAccessChangeNames(\n db: PluginDatabaseApi,\n entries: Array<{\n userId?: string | null;\n teamId?: string | null;\n permission: FlowAccessPermission;\n source: string;\n }>,\n): Promise<MovePreviewAccessChange[]> {\n const userIds = Array.from(\n new Set(entries.map((entry) => entry.userId).filter(Boolean)),\n ) as string[];\n const teamIds = Array.from(\n new Set(entries.map((entry) => entry.teamId).filter(Boolean)),\n ) as string[];\n\n const [userRows, teamRows] = await Promise.all([\n userIds.length > 0\n ? db.query<{ id: string; name: string | null; email: string | null }>(\n `SELECT id, name, email FROM user WHERE id IN (${createInClause(userIds.length)})`,\n userIds,\n )\n : Promise.resolve([]),\n teamIds.length > 0\n ? db.query<{ id: string; name: string }>(\n `SELECT id, name FROM rbac_teams WHERE id IN (${createInClause(teamIds.length)})`,\n teamIds,\n )\n : Promise.resolve([]),\n ]);\n\n const userMap = new Map(userRows.map((row) => [row.id, row.name || row.email || row.id]));\n const teamMap = new Map(teamRows.map((row) => [row.id, row.name]));\n\n return entries.map((entry) => ({\n userId: entry.userId ?? undefined,\n teamId: entry.teamId ?? undefined,\n name: entry.userId\n ? (userMap.get(entry.userId) ?? entry.userId)\n : entry.teamId\n ? (teamMap.get(entry.teamId) ?? entry.teamId)\n : 'Unknown',\n permission: entry.permission,\n source: entry.source,\n }));\n}\n\n// ─────────────────────────────────────────────────────────────\n// Plugin factory\n// ─────────────────────────────────────────────────────────────\n\nexport function rbacPlugin(options: RbacPluginOptions = {}): InvectPlugin {\n const { useFlowAccessTable = true, adminPermission = 'flow:read', enableTeams = true } = options;\n\n // ── Plugin-owned tables ─────────────────────────────────────\n const teamsSchema: InvectPluginSchema = enableTeams\n ? {\n flows: {\n fields: {\n scope_id: {\n type: 'string',\n required: false,\n references: { table: 'rbac_teams', field: 'id', onDelete: 'set null' },\n index: true,\n },\n },\n },\n rbac_teams: {\n fields: {\n id: { type: 'string', primaryKey: true },\n name: { type: 'string', required: true },\n description: { type: 'text', required: false },\n parent_id: {\n type: 'string',\n required: false,\n references: { table: 'rbac_teams', field: 'id', onDelete: 'set null' },\n index: true,\n },\n created_by: {\n type: 'string',\n required: false,\n references: { table: 'user', field: 'id' },\n },\n created_at: { type: 'date', required: true, defaultValue: 'now()' },\n updated_at: { type: 'date', required: false },\n },\n },\n rbac_team_members: {\n fields: {\n id: { type: 'string', primaryKey: true },\n team_id: {\n type: 'string',\n required: true,\n references: { table: 'rbac_teams', field: 'id', onDelete: 'cascade' },\n index: true,\n },\n user_id: {\n type: 'string',\n required: true,\n references: { table: 'user', field: 'id', onDelete: 'cascade' },\n index: true,\n },\n created_at: { type: 'date', required: true, defaultValue: 'now()' },\n },\n },\n rbac_scope_access: {\n fields: {\n id: { type: 'string', primaryKey: true },\n scope_id: {\n type: 'string',\n required: true,\n references: { table: 'rbac_teams', field: 'id', onDelete: 'cascade' },\n index: true,\n },\n user_id: { type: 'string', required: false, index: true },\n team_id: { type: 'string', required: false, index: true },\n permission: {\n type: 'string',\n required: true,\n defaultValue: 'viewer',\n typeAnnotation: 'FlowAccessPermission',\n },\n granted_by: { type: 'string', required: false },\n granted_at: { type: 'date', required: true, defaultValue: 'now()' },\n },\n },\n }\n : {};\n\n // Mutable DB reference — captured from the first endpoint handler.\n // Used by the onRequest hook to resolve teamIds for the identity.\n let capturedDbApi: PluginDatabaseApi | null = null;\n\n // UI manifest — declares what the frontend should render.\n // Component IDs are resolved by the frontend plugin's component registry.\n const ui: PluginUIManifest = {\n sidebar: [\n {\n label: 'Access Control',\n icon: 'Shield',\n path: '/access',\n permission: adminPermission,\n },\n ...(enableTeams ? [] : []),\n ],\n pages: [\n {\n path: '/access',\n componentId: 'rbac.AccessControlPage',\n title: 'Access Control',\n },\n ...(enableTeams ? [] : []),\n ],\n panelTabs: [\n {\n context: 'flowEditor',\n label: 'Access',\n componentId: 'rbac.FlowAccessPanel',\n permission: 'flow:read',\n },\n ],\n headerActions: [\n {\n context: 'flowHeader',\n componentId: 'rbac.ShareButton',\n permission: 'flow:update',\n },\n ],\n };\n\n return {\n id: 'rbac',\n name: 'Role-Based Access Control',\n\n // Plugin-owned database tables\n schema: teamsSchema,\n\n // RBAC uses the core flow_access table (already checked by core),\n // but also depends on the auth plugin's tables for user identity.\n // Declaring them here ensures a clear error if the developer has the\n // RBAC plugin enabled but forgot the better-auth tables.\n requiredTables: [\n 'user',\n 'session',\n ...(enableTeams ? ['rbac_teams', 'rbac_team_members', 'rbac_scope_access'] : []),\n ],\n setupInstructions:\n 'The RBAC plugin requires better-auth tables (user, session). ' +\n 'Make sure @invect/user-auth is configured, then run ' +\n '`npx invect-cli generate` followed by `npx drizzle-kit push`.',\n\n // ─── Initialization ───────────────────────────────────────\n\n init: async (ctx: InvectPluginContext) => {\n // Verify that the auth plugin is loaded\n if (!ctx.hasPlugin('better-auth')) {\n ctx.logger.warn(\n 'RBAC plugin requires the @invect/user-auth plugin. ' +\n 'RBAC will work with reduced functionality (no session resolution). ' +\n 'Make sure userAuth() is registered before rbacPlugin().',\n );\n }\n\n ctx.logger.info('RBAC plugin initialized', {\n useFlowAccessTable,\n });\n },\n\n // ─── Plugin Endpoints ─────────────────────────────────────\n\n endpoints: [\n // ── Auth Info ──\n\n {\n method: 'GET',\n path: '/rbac/me',\n isPublic: false,\n handler: async (ctx) => {\n // Capture DB API on first endpoint call for the onRequest hook\n if (!capturedDbApi && enableTeams) {\n capturedDbApi = ctx.database;\n }\n\n const identity = ctx.identity;\n const permissions = ctx.core.getPermissions(identity);\n const resolvedRole = identity ? ctx.core.getResolvedRole(identity) : null;\n\n return {\n status: 200,\n body: {\n identity: identity\n ? {\n id: identity.id,\n name: identity.name,\n role: identity.role,\n resolvedRole,\n }\n : null,\n permissions,\n isAuthenticated: !!identity,\n },\n };\n },\n },\n\n {\n method: 'GET',\n path: '/rbac/roles',\n isPublic: false,\n permission: 'flow:read',\n handler: async (ctx) => {\n const roles = ctx.core.getAvailableRoles();\n return { status: 200, body: { roles } };\n },\n },\n\n // ── UI Manifest ──\n\n {\n method: 'GET',\n path: '/rbac/ui-manifest',\n isPublic: true,\n handler: async (_ctx) => {\n return {\n status: 200,\n body: {\n id: 'rbac',\n ...ui,\n },\n };\n },\n },\n\n // ── Flow Access Management ──\n\n {\n method: 'GET',\n path: '/rbac/flows/:flowId/access',\n permission: 'flow:read',\n handler: async (ctx) => {\n const flowId = ctx.params.flowId;\n if (!flowId) {\n return { status: 400, body: { error: 'Missing flowId parameter' } };\n }\n\n if (!ctx.core.isFlowAccessTableEnabled()) {\n return {\n status: 501,\n body: {\n error: 'Not Implemented',\n message:\n 'Flow access table not enabled. Set auth.useFlowAccessTable: true in config.',\n },\n };\n }\n\n if (!ctx.identity) {\n return {\n status: 401,\n body: { error: 'Unauthorized', message: 'Authentication required' },\n };\n }\n\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n const callerPermission = isAdmin\n ? 'owner'\n : await getEffectiveFlowPermission(ctx.database, flowId, ctx.identity);\n\n if (!callerPermission) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'No access to this flow' },\n };\n }\n\n const access = await ctx.core.listFlowAccess(flowId);\n return { status: 200, body: { access } };\n },\n },\n\n {\n method: 'POST',\n path: '/rbac/flows/:flowId/access',\n permission: 'flow:read',\n handler: async (ctx) => {\n const flowId = ctx.params.flowId;\n if (!flowId) {\n return { status: 400, body: { error: 'Missing flowId parameter' } };\n }\n\n if (!ctx.core.isFlowAccessTableEnabled()) {\n return {\n status: 501,\n body: {\n error: 'Not Implemented',\n message:\n 'Flow access table not enabled. Set auth.useFlowAccessTable: true in config.',\n },\n };\n }\n\n const { userId, teamId, permission, expiresAt } = ctx.body as {\n userId?: string;\n teamId?: string;\n permission?: string;\n expiresAt?: string;\n };\n\n if (!userId && !teamId) {\n return {\n status: 400,\n body: { error: 'Either userId or teamId must be provided' },\n };\n }\n\n if (!ctx.identity) {\n return {\n status: 401,\n body: { error: 'Unauthorized', message: 'Authentication required' },\n };\n }\n\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n const callerPermission = isAdmin\n ? 'owner'\n : await getEffectiveFlowPermission(ctx.database, flowId, ctx.identity);\n\n if (callerPermission !== 'owner') {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Owner access is required to manage sharing' },\n };\n }\n\n if (!permission || !['owner', 'editor', 'operator', 'viewer'].includes(permission)) {\n return {\n status: 400,\n body: { error: 'permission must be one of: owner, editor, operator, viewer' },\n };\n }\n\n const access = await ctx.core.grantFlowAccess({\n flowId,\n userId,\n teamId,\n permission: permission as 'owner' | 'editor' | 'operator' | 'viewer',\n grantedBy: ctx.identity?.id,\n expiresAt,\n });\n return { status: 201, body: access };\n },\n },\n\n {\n method: 'DELETE',\n path: '/rbac/flows/:flowId/access/:accessId',\n permission: 'flow:read',\n handler: async (ctx) => {\n const { flowId, accessId } = ctx.params;\n if (!flowId || !accessId) {\n return { status: 400, body: { error: 'Missing flowId or accessId parameter' } };\n }\n\n if (!ctx.core.isFlowAccessTableEnabled()) {\n return {\n status: 501,\n body: {\n error: 'Not Implemented',\n message:\n 'Flow access table not enabled. Set auth.useFlowAccessTable: true in config.',\n },\n };\n }\n\n if (!ctx.identity) {\n return {\n status: 401,\n body: { error: 'Unauthorized', message: 'Authentication required' },\n };\n }\n\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n const callerPermission = isAdmin\n ? 'owner'\n : await getEffectiveFlowPermission(ctx.database, flowId, ctx.identity);\n\n if (callerPermission !== 'owner') {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Owner access is required to manage sharing' },\n };\n }\n\n await ctx.core.revokeFlowAccess(accessId);\n return { status: 204, body: null };\n },\n },\n\n {\n method: 'GET',\n path: '/rbac/flows/accessible',\n isPublic: false,\n handler: async (ctx) => {\n if (!ctx.core.isFlowAccessTableEnabled()) {\n return {\n status: 501,\n body: {\n error: 'Not Implemented',\n message:\n 'Flow access table not enabled. Set auth.useFlowAccessTable: true in config.',\n },\n };\n }\n\n const identity = ctx.identity;\n if (!identity) {\n return {\n status: 401,\n body: { error: 'Unauthorized', message: 'Authentication required' },\n };\n }\n\n // Admins see all flows with implicit owner permission\n const isAdmin = ctx.core.getPermissions(identity).includes('admin:*');\n if (isAdmin) {\n return {\n status: 200,\n body: { flowIds: [], permissions: {}, isAdmin: true },\n };\n }\n\n const { flowIds, permissions } = await getCurrentUserAccessibleFlows(\n ctx.database,\n identity,\n );\n\n return {\n status: 200,\n body: {\n flowIds,\n permissions,\n isAdmin: false,\n },\n };\n },\n },\n\n {\n method: 'GET',\n path: '/rbac/flows/:flowId/effective-access',\n permission: 'flow:read',\n handler: async (ctx) => {\n const flowId = ctx.params.flowId;\n if (!flowId) {\n return { status: 400, body: { error: 'Missing flowId parameter' } };\n }\n if (!ctx.identity) {\n return {\n status: 401,\n body: { error: 'Unauthorized', message: 'Authentication required' },\n };\n }\n\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n const callerPermission = isAdmin\n ? 'owner'\n : await getEffectiveFlowPermission(ctx.database, flowId, ctx.identity);\n\n if (!callerPermission) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'No access to this flow' },\n };\n }\n\n const records = await listAllEffectiveFlowAccessRecords(ctx.database, flowId);\n\n return {\n status: 200,\n body: {\n flowId,\n scopeId: await getFlowScopeId(ctx.database, flowId),\n records,\n },\n };\n },\n },\n\n {\n method: 'PUT',\n path: '/rbac/flows/:flowId/scope',\n permission: 'flow:update',\n handler: async (ctx) => {\n const flowId = ctx.params.flowId;\n const { scopeId } = ctx.body as { scopeId?: string | null };\n if (!flowId) {\n return { status: 400, body: { error: 'Missing flowId parameter' } };\n }\n if (!ctx.identity) {\n return {\n status: 401,\n body: { error: 'Unauthorized', message: 'Authentication required' },\n };\n }\n\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n const callerPermission = isAdmin\n ? 'owner'\n : await getEffectiveFlowPermission(ctx.database, flowId, ctx.identity);\n if (callerPermission !== 'owner') {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Owner access is required to move flows' },\n };\n }\n\n if (scopeId) {\n const scopeRows = await ctx.database.query<{ id: string }>(\n 'SELECT id FROM rbac_teams WHERE id = ?',\n [scopeId],\n );\n if (scopeRows.length === 0) {\n return { status: 404, body: { error: 'Scope not found' } };\n }\n }\n\n await ctx.database.execute('UPDATE flows SET scope_id = ? WHERE id = ?', [\n scopeId ?? null,\n flowId,\n ]);\n return { status: 200, body: { success: true, flowId, scopeId: scopeId ?? null } };\n },\n },\n\n // ── Teams Management ──\n\n ...(enableTeams\n ? [\n {\n method: 'GET' as const,\n path: '/rbac/scopes/tree',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n\n const [teamRows, flowRows, memberRows, accessRows, teamRoleRows] =\n await Promise.all([\n ctx.database.query<{\n id: string;\n name: string;\n description: string | null;\n parent_id: string | null;\n created_by: string | null;\n created_at: string;\n updated_at: string | null;\n }>(\n 'SELECT id, name, description, parent_id, created_by, created_at, updated_at FROM rbac_teams ORDER BY name',\n ),\n ctx.database.query<{ id: string; name: string; scope_id: string | null }>(\n 'SELECT id, name, scope_id FROM flows ORDER BY name',\n ),\n ctx.database.query<{ team_id: string; member_count: number }>(\n 'SELECT team_id, COUNT(*) AS member_count FROM rbac_team_members GROUP BY team_id',\n ),\n ctx.database.query<{ scope_id: string; access_count: number }>(\n 'SELECT scope_id, COUNT(*) AS access_count FROM rbac_scope_access GROUP BY scope_id',\n ),\n ctx.database.query<{ scope_id: string; permission: FlowAccessPermission }>(\n 'SELECT scope_id, permission FROM rbac_scope_access WHERE team_id = scope_id AND team_id IS NOT NULL',\n ),\n ]);\n\n const memberCounts = new Map(\n memberRows.map((row) => [row.team_id, Number(row.member_count)]),\n );\n const accessCounts = new Map(\n accessRows.map((row) => [row.scope_id, Number(row.access_count)]),\n );\n const teamPermissions = new Map(\n teamRoleRows.map((row) => [row.scope_id, row.permission]),\n );\n\n const nodeMap = new Map<string, ScopeTreeNode>();\n for (const row of teamRows) {\n nodeMap.set(row.id, {\n ...normalizeTeamRow(row),\n children: [],\n flows: [],\n directAccessCount: accessCounts.get(row.id) ?? 0,\n memberCount: memberCounts.get(row.id) ?? 0,\n teamPermission: teamPermissions.get(row.id) ?? null,\n });\n }\n\n const roots: ScopeTreeNode[] = [];\n for (const node of nodeMap.values()) {\n if (node.parentId && nodeMap.has(node.parentId)) {\n nodeMap.get(node.parentId)?.children.push(node);\n } else {\n roots.push(node);\n }\n }\n\n const unscopedFlows: Array<{ id: string; name: string; scopeId: string | null }> =\n [];\n for (const flow of flowRows) {\n const mapped = { id: flow.id, name: flow.name, scopeId: flow.scope_id };\n if (flow.scope_id && nodeMap.has(flow.scope_id)) {\n nodeMap.get(flow.scope_id)?.flows.push(mapped);\n } else {\n unscopedFlows.push(mapped);\n }\n }\n\n return { status: 200, body: { scopes: roots, unscopedFlows } };\n },\n },\n\n {\n method: 'GET' as const,\n path: '/rbac/scopes/:scopeId/access',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n\n const rows = await ctx.database.query<Record<string, unknown>>(\n 'SELECT id, scope_id, user_id, team_id, permission, granted_by, granted_at FROM rbac_scope_access WHERE scope_id = ?',\n [ctx.params.scopeId],\n );\n return { status: 200, body: { access: rows.map(normalizeScopeAccessRecord) } };\n },\n },\n\n {\n method: 'POST' as const,\n path: '/rbac/scopes/:scopeId/access',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n const { scopeId } = ctx.params;\n const { userId, teamId, permission } = ctx.body as {\n userId?: string;\n teamId?: string;\n permission?: string;\n };\n\n if (!scopeId) {\n return { status: 400, body: { error: 'Missing scopeId parameter' } };\n }\n if (!userId && !teamId) {\n return {\n status: 400,\n body: { error: 'Either userId or teamId must be provided' },\n };\n }\n if (userId && teamId) {\n return {\n status: 400,\n body: { error: 'Provide either userId or teamId, not both' },\n };\n }\n if (teamId && teamId !== scopeId) {\n return {\n status: 400,\n body: { error: 'Teams can only hold a role on their own scope' },\n };\n }\n if (!isFlowAccessPermission(permission)) {\n return {\n status: 400,\n body: { error: 'permission must be one of: owner, editor, operator, viewer' },\n };\n }\n\n const existing = await ctx.database.query<{ id: string }>(\n 'SELECT id FROM rbac_scope_access WHERE scope_id = ? AND user_id IS ? AND team_id IS ?',\n [scopeId, userId ?? null, teamId ?? null],\n );\n\n const now = new Date().toISOString();\n if (existing[0]) {\n await ctx.database.execute(\n 'UPDATE rbac_scope_access SET permission = ?, granted_by = ?, granted_at = ? WHERE id = ?',\n [permission, ctx.identity.id, now, existing[0].id],\n );\n return {\n status: 200,\n body: {\n id: existing[0].id,\n scopeId,\n userId: userId ?? null,\n teamId: teamId ?? null,\n permission,\n grantedBy: ctx.identity.id,\n grantedAt: now,\n },\n };\n }\n\n const id = crypto.randomUUID();\n await ctx.database.execute(\n 'INSERT INTO rbac_scope_access (id, scope_id, user_id, team_id, permission, granted_by, granted_at) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [id, scopeId, userId ?? null, teamId ?? null, permission, ctx.identity.id, now],\n );\n\n return {\n status: 201,\n body: {\n id,\n scopeId,\n userId: userId ?? null,\n teamId: teamId ?? null,\n permission,\n grantedBy: ctx.identity.id,\n grantedAt: now,\n },\n };\n },\n },\n\n {\n method: 'DELETE' as const,\n path: '/rbac/scopes/:scopeId/access/:accessId',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n await ctx.database.execute('DELETE FROM rbac_scope_access WHERE id = ?', [\n ctx.params.accessId,\n ]);\n return { status: 204, body: null };\n },\n },\n\n {\n method: 'POST' as const,\n path: '/rbac/preview-move',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n const { type, id, targetScopeId } = ctx.body as {\n type?: 'flow' | 'scope';\n id?: string;\n targetScopeId?: string | null;\n };\n\n if (!type || !id) {\n return { status: 400, body: { error: 'type and id are required' } };\n }\n\n let affectedFlowIds: string[] = [];\n let itemName = id;\n if (type === 'flow') {\n const rows = await ctx.database.query<{ id: string; name: string }>(\n 'SELECT id, name FROM flows WHERE id = ?',\n [id],\n );\n if (!rows[0]) {\n return { status: 404, body: { error: 'Flow not found' } };\n }\n itemName = rows[0].name;\n affectedFlowIds = [id];\n } else {\n const scopeRows = await ctx.database.query<{ id: string; name: string }>(\n 'SELECT id, name FROM rbac_teams WHERE id = ?',\n [id],\n );\n if (!scopeRows[0]) {\n return { status: 404, body: { error: 'Scope not found' } };\n }\n itemName = scopeRows[0].name;\n const descendantScopeIds = await getDescendantScopeIds(ctx.database, id);\n if (targetScopeId && descendantScopeIds.includes(targetScopeId)) {\n return {\n status: 400,\n body: { error: 'Cannot move a scope into itself or its descendant' },\n };\n }\n const flowRows = await ctx.database.query<{ id: string }>(\n `SELECT id FROM flows WHERE scope_id IN (${createInClause(descendantScopeIds.length)})`,\n descendantScopeIds,\n );\n affectedFlowIds = flowRows.map((row) => row.id);\n }\n\n const targetPath = await getScopePath(ctx.database, targetScopeId ?? null);\n const targetAncestorIds = targetScopeId\n ? await getAncestorScopeIds(ctx.database, targetScopeId)\n : [];\n const targetScopeAccess = await listAllScopeAccessForScopeIds(\n ctx.database,\n targetAncestorIds,\n );\n\n const gainedEntries = new Map<\n string,\n {\n userId?: string | null;\n teamId?: string | null;\n permission: FlowAccessPermission;\n source: string;\n }\n >();\n let unchanged = 0;\n\n for (const flowId of affectedFlowIds) {\n const currentRecords = await listAllEffectiveFlowAccessForPreview(\n ctx.database,\n flowId,\n );\n const currentPermissions = new Map<string, FlowAccessPermission>();\n for (const record of currentRecords) {\n const permission = toFlowAccessPermission(record.permission);\n if (!permission) {\n continue;\n }\n const key = buildAccessKey(record);\n currentPermissions.set(\n key,\n getHigherPermission(currentPermissions.get(key) ?? null, permission) ??\n permission,\n );\n }\n\n for (const record of targetScopeAccess) {\n const key = buildAccessKey(record);\n const existingPermission = currentPermissions.get(key) ?? null;\n if (\n existingPermission &&\n FLOW_PERMISSION_LEVELS[existingPermission] >=\n FLOW_PERMISSION_LEVELS[record.permission]\n ) {\n unchanged += 1;\n continue;\n }\n gainedEntries.set(key, {\n userId: record.userId,\n teamId: record.teamId,\n permission: record.permission,\n source: `from ${targetPath.at(-1) ?? 'root'}`,\n });\n }\n }\n\n const gained = await resolveAccessChangeNames(\n ctx.database,\n Array.from(gainedEntries.values()),\n );\n\n return {\n status: 200,\n body: {\n item: { id, name: itemName, type },\n target: {\n id: targetScopeId ?? null,\n name: targetPath.at(-1) ?? 'Unscoped',\n path: targetPath,\n },\n affectedFlows: affectedFlowIds.length,\n accessChanges: {\n gained,\n unchanged,\n },\n },\n };\n },\n },\n\n // List all teams\n {\n method: 'GET' as const,\n path: '/rbac/teams',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n\n const rows = await ctx.database.query<{\n id: string;\n name: string;\n description: string | null;\n parent_id: string | null;\n created_by: string | null;\n created_at: string;\n updated_at: string | null;\n }>(\n 'SELECT id, name, description, parent_id, created_by, created_at, updated_at FROM rbac_teams ORDER BY name',\n );\n\n const teams = rows.map((r) => normalizeTeamRow(r));\n\n return { status: 200, body: { teams } };\n },\n },\n\n // Create a team (admin only)\n {\n method: 'POST' as const,\n path: '/rbac/teams',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n const { name, description, parentId } = ctx.body as {\n name?: string;\n description?: string;\n parentId?: string | null;\n };\n if (!name?.trim()) {\n return { status: 400, body: { error: 'Team name is required' } };\n }\n\n if (parentId) {\n const parentRows = await ctx.database.query<{ id: string }>(\n 'SELECT id FROM rbac_teams WHERE id = ?',\n [parentId],\n );\n if (parentRows.length === 0) {\n return { status: 404, body: { error: 'Parent scope not found' } };\n }\n }\n\n const id = crypto.randomUUID();\n const now = new Date().toISOString();\n await ctx.database.execute(\n 'INSERT INTO rbac_teams (id, name, description, parent_id, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [\n id,\n name.trim(),\n description?.trim() || null,\n parentId ?? null,\n ctx.identity.id,\n now,\n now,\n ],\n );\n\n return {\n status: 201,\n body: {\n id,\n name: name.trim(),\n description: description?.trim() || null,\n parentId: parentId ?? null,\n createdBy: ctx.identity.id,\n createdAt: now,\n updatedAt: now,\n },\n };\n },\n },\n\n // Update a team (admin only)\n {\n method: 'PUT' as const,\n path: '/rbac/teams/:teamId',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n const { teamId } = ctx.params;\n const { name, description, parentId } = ctx.body as {\n name?: string;\n description?: string;\n parentId?: string | null;\n };\n\n const existing = await ctx.database.query<{ id: string }>(\n 'SELECT id FROM rbac_teams WHERE id = ?',\n [teamId],\n );\n if (existing.length === 0) {\n return { status: 404, body: { error: 'Team not found' } };\n }\n\n if (parentId === teamId) {\n return { status: 400, body: { error: 'A scope cannot be its own parent' } };\n }\n if (parentId) {\n const parentRows = await ctx.database.query<{ id: string }>(\n 'SELECT id FROM rbac_teams WHERE id = ?',\n [parentId],\n );\n if (parentRows.length === 0) {\n return { status: 404, body: { error: 'Parent scope not found' } };\n }\n const descendantScopeIds = await getDescendantScopeIds(ctx.database, teamId);\n if (descendantScopeIds.includes(parentId)) {\n return {\n status: 400,\n body: { error: 'Cannot move a scope into itself or its descendant' },\n };\n }\n }\n\n const updates: string[] = [];\n const values: unknown[] = [];\n if (name !== undefined) {\n if (!name.trim()) {\n return { status: 400, body: { error: 'Team name cannot be empty' } };\n }\n updates.push('name = ?');\n values.push(name.trim());\n }\n if (description !== undefined) {\n updates.push('description = ?');\n values.push(description?.trim() || null);\n }\n if (parentId !== undefined) {\n updates.push('parent_id = ?');\n values.push(parentId ?? null);\n }\n if (updates.length === 0) {\n return { status: 400, body: { error: 'No fields to update' } };\n }\n\n updates.push('updated_at = ?');\n values.push(new Date().toISOString());\n values.push(teamId);\n\n await ctx.database.execute(\n `UPDATE rbac_teams SET ${updates.join(', ')} WHERE id = ?`,\n values,\n );\n\n return { status: 200, body: { success: true } };\n },\n },\n\n // Delete a team (admin only)\n {\n method: 'DELETE' as const,\n path: '/rbac/teams/:teamId',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n const { teamId } = ctx.params;\n const teams = await ctx.database.query<{ id: string; parent_id: string | null }>(\n 'SELECT id, parent_id FROM rbac_teams WHERE id = ?',\n [teamId],\n );\n if (teams.length === 0) {\n return { status: 404, body: { error: 'Team not found' } };\n }\n\n const parentId = teams[0].parent_id ?? null;\n await ctx.database.execute('UPDATE flows SET scope_id = ? WHERE scope_id = ?', [\n parentId,\n teamId,\n ]);\n\n // CASCADE on team_members handles cleanup\n await ctx.database.execute('DELETE FROM rbac_teams WHERE id = ?', [teamId]);\n return { status: 204, body: null };\n },\n },\n\n // Get team with members\n {\n method: 'GET' as const,\n path: '/rbac/teams/:teamId',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n\n const { teamId } = ctx.params;\n const teams = await ctx.database.query<{\n id: string;\n name: string;\n description: string | null;\n parent_id: string | null;\n created_by: string | null;\n created_at: string;\n updated_at: string | null;\n }>(\n 'SELECT id, name, description, parent_id, created_by, created_at, updated_at FROM rbac_teams WHERE id = ?',\n [teamId],\n );\n\n if (teams.length === 0) {\n return { status: 404, body: { error: 'Team not found' } };\n }\n\n const members = await ctx.database.query<{\n id: string;\n team_id: string;\n user_id: string;\n created_at: string;\n }>(\n 'SELECT id, team_id, user_id, created_at FROM rbac_team_members WHERE team_id = ?',\n [teamId],\n );\n\n const team = teams[0];\n return {\n status: 200,\n body: {\n ...normalizeTeamRow(team),\n members: members.map((m) => ({\n id: m.id,\n teamId: m.team_id,\n userId: m.user_id,\n createdAt: m.created_at,\n })),\n },\n };\n },\n },\n\n // Add member to team (admin only)\n {\n method: 'POST' as const,\n path: '/rbac/teams/:teamId/members',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n const { teamId } = ctx.params;\n const { userId } = ctx.body as { userId?: string };\n if (!userId?.trim()) {\n return { status: 400, body: { error: 'userId is required' } };\n }\n\n // Check team exists\n const teams = await ctx.database.query<{ id: string }>(\n 'SELECT id FROM rbac_teams WHERE id = ?',\n [teamId],\n );\n if (teams.length === 0) {\n return { status: 404, body: { error: 'Team not found' } };\n }\n\n // Check not already a member\n const existing = await ctx.database.query<{ id: string }>(\n 'SELECT id FROM rbac_team_members WHERE team_id = ? AND user_id = ?',\n [teamId, userId.trim()],\n );\n if (existing.length > 0) {\n return { status: 409, body: { error: 'User is already a member of this team' } };\n }\n\n const id = crypto.randomUUID();\n const now = new Date().toISOString();\n await ctx.database.execute(\n 'INSERT INTO rbac_team_members (id, team_id, user_id, created_at) VALUES (?, ?, ?, ?)',\n [id, teamId, userId.trim(), now],\n );\n\n return {\n status: 201,\n body: { id, teamId, userId: userId.trim(), createdAt: now },\n };\n },\n },\n\n // Remove member from team (admin only)\n {\n method: 'DELETE' as const,\n path: '/rbac/teams/:teamId/members/:userId',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n const { teamId, userId } = ctx.params;\n await ctx.database.execute(\n 'DELETE FROM rbac_team_members WHERE team_id = ? AND user_id = ?',\n [teamId, userId],\n );\n return { status: 204, body: null };\n },\n },\n\n // Get teams for current user (for identity resolution)\n {\n method: 'GET' as const,\n path: '/rbac/my-teams',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n\n const rows = await ctx.database.query<{\n id: string;\n name: string;\n description: string | null;\n parent_id: string | null;\n created_by: string | null;\n created_at: string;\n updated_at: string | null;\n }>(\n 'SELECT t.id, t.name, t.description, t.parent_id, t.created_by, t.created_at, t.updated_at ' +\n 'FROM rbac_teams t INNER JOIN rbac_team_members tm ON t.id = tm.team_id ' +\n 'WHERE tm.user_id = ? ORDER BY t.name',\n [ctx.identity.id],\n );\n\n const teams = rows.map((r) => normalizeTeamRow(r));\n\n return { status: 200, body: { teams } };\n },\n },\n ]\n : []),\n ],\n\n // ─── Lifecycle Hooks ──────────────────────────────────────\n\n hooks: {\n // Enrich identity with teamIds after auth plugin resolves the session.\n // This runs after the auth plugin's onRequest hook since RBAC is registered\n // after auth. We query rbac_team_members to get the user's team IDs.\n onRequest: enableTeams\n ? async (\n _request: Request,\n context: { path: string; method: string; identity: InvectIdentity | null },\n ) => {\n if (!context.identity || !capturedDbApi) {\n return;\n }\n // Skip if teamIds already populated (custom mapUser)\n if (context.identity.teamIds && context.identity.teamIds.length > 0) {\n return;\n }\n\n try {\n const rows = await capturedDbApi.query<{ team_id: string }>(\n 'SELECT team_id FROM rbac_team_members WHERE user_id = ?',\n [context.identity.id],\n );\n if (rows.length > 0) {\n context.identity = {\n ...context.identity,\n teamIds: rows.map((r) => r.team_id),\n };\n }\n } catch {\n // Silently ignore — table may not exist yet during initial setup\n }\n }\n : undefined,\n\n // Enforce flow-level ACLs on authorization checks\n onAuthorize: async (context) => {\n if (!useFlowAccessTable) {\n return; // Defer to default RBAC\n }\n\n const { identity, resource, action } = context;\n\n // Only enforce for flow-related resources with specific IDs\n if (!identity || !resource?.id) {\n return;\n }\n\n if (!FLOW_RESOURCE_TYPES.has(resource.type)) {\n return;\n }\n\n if (identity.permissions?.includes('admin:*') || identity.role === 'admin') {\n return { allowed: true };\n }\n\n const database = context.database ?? capturedDbApi;\n if (!database) {\n return;\n }\n\n const effectivePermission = await getEffectiveFlowPermission(\n database,\n resource.id,\n identity,\n );\n if (!effectivePermission) {\n return { allowed: false };\n }\n\n const requiredPermission = mapActionToRequiredPermission(action);\n return {\n allowed:\n FLOW_PERMISSION_LEVELS[effectivePermission] >=\n FLOW_PERMISSION_LEVELS[requiredPermission],\n };\n },\n\n // Auto-grant owner access when a flow is created\n afterFlowRun: async (_context) => {\n // This hook is for flow *runs*, not flow creation.\n // Flow creation auto-grant is handled by the core when\n // auth.useFlowAccessTable is true.\n },\n },\n\n // ─── Error Codes ──────────────────────────────────────────\n\n $ERROR_CODES: {\n 'rbac:no_access': {\n message: 'You do not have access to this flow.',\n status: 403,\n },\n 'rbac:insufficient_permission': {\n message: 'Your access level is insufficient for this operation.',\n status: 403,\n },\n 'rbac:auth_required': {\n message: 'Authentication is required. Please sign in.',\n status: 401,\n },\n 'rbac:plugin_missing': {\n message: 'The RBAC plugin requires the @invect/user-auth plugin.',\n status: 500,\n },\n },\n };\n}\n"],"mappings":"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"}
|
package/dist/backend/index.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* ```ts
|
|
9
9
|
* import { resolveTeamIds } from '@invect/rbac/backend';
|
|
10
10
|
*
|
|
11
|
-
*
|
|
11
|
+
* userAuth({
|
|
12
12
|
* auth,
|
|
13
13
|
* mapUser: async (user, session) => ({
|
|
14
14
|
* id: user.id,
|
|
@@ -444,9 +444,9 @@ function rbacPlugin(options = {}) {
|
|
|
444
444
|
"rbac_scope_access"
|
|
445
445
|
] : []
|
|
446
446
|
],
|
|
447
|
-
setupInstructions: "The RBAC plugin requires better-auth tables (user, session). Make sure @invect/user-auth is configured, then run `npx invect generate` followed by `npx drizzle-kit push`.",
|
|
447
|
+
setupInstructions: "The RBAC plugin requires better-auth tables (user, session). Make sure @invect/user-auth is configured, then run `npx invect-cli generate` followed by `npx drizzle-kit push`.",
|
|
448
448
|
init: async (ctx) => {
|
|
449
|
-
if (!ctx.hasPlugin("better-auth")) ctx.logger.warn("RBAC plugin requires the @invect/user-auth plugin. RBAC will work with reduced functionality (no session resolution). Make sure
|
|
449
|
+
if (!ctx.hasPlugin("better-auth")) ctx.logger.warn("RBAC plugin requires the @invect/user-auth plugin. RBAC will work with reduced functionality (no session resolution). Make sure userAuth() is registered before rbacPlugin().");
|
|
450
450
|
ctx.logger.info("RBAC plugin initialized", { useFlowAccessTable });
|
|
451
451
|
},
|
|
452
452
|
endpoints: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/backend/plugin.ts"],"sourcesContent":["/**\n * @invect/rbac — Backend Plugin\n *\n * RBAC plugin that provides:\n * - Flow access management endpoints (grant/revoke/list)\n * - Auth info endpoints (/auth/me, /auth/roles)\n * - Authorization hooks (enforces flow-level ACLs)\n * - UI manifest for the frontend plugin to render\n *\n * Requires the @invect/user-auth (better-auth) plugin to be loaded first\n * for session resolution. This plugin handles the *authorization* layer\n * on top of that authentication.\n */\n\nimport type {\n InvectPlugin,\n InvectPluginContext,\n InvectPermission,\n InvectPluginSchema,\n InvectIdentity,\n PluginDatabaseApi,\n PluginEndpointContext,\n} from '@invect/core';\nimport type {\n EffectiveAccessRecord,\n FlowAccessPermission,\n FlowAccessRecord,\n MovePreviewAccessChange,\n PluginUIManifest,\n ScopeAccessRecord,\n ScopeTreeNode,\n Team,\n} from '../shared/types';\n\n// ─────────────────────────────────────────────────────────────\n// Team ID Resolution Helper\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Resolve team IDs for a user from the rbac_team_members table.\n * Use this in a custom `mapUser` for the auth plugin to populate\n * `identity.teamIds` automatically.\n *\n * @example\n * ```ts\n * import { resolveTeamIds } from '@invect/rbac/backend';\n *\n * betterAuthPlugin({\n * auth,\n * mapUser: async (user, session) => ({\n * id: user.id,\n * name: user.name ?? undefined,\n * role: user.role === 'admin' ? 'admin' : 'user',\n * teamIds: await resolveTeamIds(db, user.id),\n * }),\n * });\n * ```\n */\nexport async function resolveTeamIds(db: PluginDatabaseApi, userId: string): Promise<string[]> {\n const rows = await db.query<{ team_id: string }>(\n 'SELECT team_id FROM rbac_team_members WHERE user_id = ?',\n [userId],\n );\n return rows.map((r) => r.team_id);\n}\n\n// ─────────────────────────────────────────────────────────────\n// Types\n// ─────────────────────────────────────────────────────────────\n\nexport interface RbacPluginOptions {\n /**\n * Enable the flow access table for database-backed per-flow permissions.\n * When enabled, the plugin will check flow-level ACLs in addition to\n * role-based permissions. Requires `auth.useFlowAccessTable: true` in\n * the Invect config.\n *\n * @default true\n */\n useFlowAccessTable?: boolean;\n\n /**\n * Permission required to access the RBAC flows page.\n *\n * @default 'flow:read'\n */\n adminPermission?: InvectPermission;\n\n /**\n * Enable teams feature for grouping users.\n *\n * @default true\n */\n enableTeams?: boolean;\n}\n\nconst FLOW_RESOURCE_TYPES = new Set(['flow', 'flow-version', 'flow-run', 'node-execution']);\n\nconst FLOW_PERMISSION_LEVELS: Record<FlowAccessPermission, number> = {\n viewer: 1,\n operator: 2,\n editor: 3,\n owner: 4,\n};\n\nfunction isFlowAccessPermission(value: unknown): value is FlowAccessPermission {\n return value === 'viewer' || value === 'operator' || value === 'editor' || value === 'owner';\n}\n\nfunction toFlowAccessPermission(value: unknown): FlowAccessPermission | null {\n return isFlowAccessPermission(value) ? value : null;\n}\n\nfunction getHigherPermission(\n left: FlowAccessPermission | null,\n right: FlowAccessPermission | null,\n): FlowAccessPermission | null {\n if (!left) {\n return right;\n }\n if (!right) {\n return left;\n }\n return FLOW_PERMISSION_LEVELS[right] > FLOW_PERMISSION_LEVELS[left] ? right : left;\n}\n\nfunction createInClause(count: number): string {\n return Array.from({ length: count }, () => '?').join(', ');\n}\n\nfunction mapActionToRequiredPermission(action: string): FlowAccessPermission {\n if (action.includes('delete') || action.includes('share') || action.includes('admin')) {\n return 'owner';\n }\n if (action.includes('update') || action.includes('write') || action.includes('create')) {\n return 'editor';\n }\n if (action.includes('run') || action.includes('execute')) {\n return 'operator';\n }\n return 'viewer';\n}\n\nfunction normalizeFlowAccessRecord(row: Record<string, unknown>): FlowAccessRecord {\n return {\n id: String(row.id),\n flowId: String(row.flowId ?? row.flow_id),\n userId: (row.userId ?? row.user_id) ? String(row.userId ?? row.user_id) : null,\n teamId: (row.teamId ?? row.team_id) ? String(row.teamId ?? row.team_id) : null,\n permission: String(row.permission) as FlowAccessPermission,\n grantedBy: (row.grantedBy ?? row.granted_by) ? String(row.grantedBy ?? row.granted_by) : null,\n grantedAt: String(row.grantedAt ?? row.granted_at),\n expiresAt: (row.expiresAt ?? row.expires_at) ? String(row.expiresAt ?? row.expires_at) : null,\n };\n}\n\nfunction normalizeScopeAccessRecord(row: Record<string, unknown>): ScopeAccessRecord {\n return {\n id: String(row.id),\n scopeId: String(row.scopeId ?? row.scope_id),\n userId: (row.userId ?? row.user_id) ? String(row.userId ?? row.user_id) : null,\n teamId: (row.teamId ?? row.team_id) ? String(row.teamId ?? row.team_id) : null,\n permission: String(row.permission) as FlowAccessPermission,\n grantedBy: (row.grantedBy ?? row.granted_by) ? String(row.grantedBy ?? row.granted_by) : null,\n grantedAt: String(row.grantedAt ?? row.granted_at),\n };\n}\n\nfunction normalizeTeamRow(row: {\n id: string;\n name: string;\n description: string | null;\n parent_id?: string | null;\n created_by: string | null;\n created_at: string;\n updated_at: string | null;\n}): Team {\n return {\n id: row.id,\n name: row.name,\n description: row.description,\n parentId: row.parent_id ?? null,\n createdBy: row.created_by,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n };\n}\n\nasync function getFlowScopeId(db: PluginDatabaseApi, flowId: string): Promise<string | null> {\n const rows = await db.query<{ scope_id: string | null }>(\n 'SELECT scope_id FROM flows WHERE id = ?',\n [flowId],\n );\n return rows[0]?.scope_id ?? null;\n}\n\nasync function getAncestorScopeIds(db: PluginDatabaseApi, scopeId: string): Promise<string[]> {\n const rows = await db.query<{ id: string }>(\n `WITH RECURSIVE ancestors AS (\n SELECT id, parent_id FROM rbac_teams WHERE id = ?\n UNION ALL\n SELECT parent.id, parent.parent_id\n FROM rbac_teams parent\n INNER JOIN ancestors current ON parent.id = current.parent_id\n )\n SELECT id FROM ancestors`,\n [scopeId],\n );\n return rows.map((row) => row.id);\n}\n\nasync function getDescendantScopeIds(db: PluginDatabaseApi, scopeId: string): Promise<string[]> {\n const rows = await db.query<{ id: string }>(\n `WITH RECURSIVE descendants AS (\n SELECT id FROM rbac_teams WHERE id = ?\n UNION ALL\n SELECT child.id\n FROM rbac_teams child\n INNER JOIN descendants current ON child.parent_id = current.id\n )\n SELECT id FROM descendants`,\n [scopeId],\n );\n return rows.map((row) => row.id);\n}\n\nasync function listScopeAccessForScopeIds(\n db: PluginDatabaseApi,\n scopeIds: string[],\n userId: string,\n teamIds: string[],\n): Promise<ScopeAccessRecord[]> {\n if (scopeIds.length === 0) {\n return [];\n }\n\n const params: unknown[] = [...scopeIds, userId];\n let sql =\n `SELECT id, scope_id, user_id, team_id, permission, granted_by, granted_at ` +\n `FROM rbac_scope_access WHERE (scope_id IN (${createInClause(scopeIds.length)}) ` +\n `AND user_id = ?)`;\n\n if (teamIds.length > 0) {\n sql += ` OR (scope_id IN (${createInClause(scopeIds.length)}) AND team_id IN (${createInClause(teamIds.length)}))`;\n params.push(...scopeIds, ...teamIds);\n }\n\n const rows = await db.query<Record<string, unknown>>(sql, params);\n return rows.map(normalizeScopeAccessRecord);\n}\n\nasync function listDirectFlowAccessForIdentity(\n db: PluginDatabaseApi,\n flowId: string,\n userId: string,\n teamIds: string[],\n): Promise<FlowAccessRecord[]> {\n const params: unknown[] = [flowId, userId];\n let sql =\n 'SELECT id, flow_id, user_id, team_id, permission, granted_by, granted_at, expires_at ' +\n 'FROM flow_access WHERE (flow_id = ? AND user_id = ?)';\n\n if (teamIds.length > 0) {\n sql += ` OR (flow_id = ? AND team_id IN (${createInClause(teamIds.length)}))`;\n params.push(flowId, ...teamIds);\n }\n\n const rows = await db.query<Record<string, unknown>>(sql, params);\n const now = Date.now();\n return rows\n .map(normalizeFlowAccessRecord)\n .filter((record) => !record.expiresAt || new Date(record.expiresAt).getTime() > now);\n}\n\nasync function getEffectiveFlowAccessRecords(\n db: PluginDatabaseApi,\n flowId: string,\n userId: string,\n teamIds: string[],\n): Promise<EffectiveAccessRecord[]> {\n const directRecords = (\n await listDirectFlowAccessForIdentity(db, flowId, userId, teamIds)\n ).map<EffectiveAccessRecord>((record) => ({ ...record, source: 'direct' }));\n\n const scopeId = await getFlowScopeId(db, flowId);\n if (!scopeId) {\n return directRecords;\n }\n\n const ancestorIds = await getAncestorScopeIds(db, scopeId);\n const inheritedRows = await listScopeAccessForScopeIds(db, ancestorIds, userId, teamIds);\n if (inheritedRows.length === 0) {\n return directRecords;\n }\n\n const scopeRows = await db.query<{ id: string; name: string }>(\n `SELECT id, name FROM rbac_teams WHERE id IN (${createInClause(ancestorIds.length)})`,\n ancestorIds,\n );\n const scopeNames = new Map(scopeRows.map((row) => [row.id, row.name]));\n\n return [\n ...directRecords,\n ...inheritedRows.map<EffectiveAccessRecord>((record) => ({\n id: record.id,\n flowId,\n userId: record.userId,\n teamId: record.teamId,\n permission: record.permission,\n grantedBy: record.grantedBy,\n grantedAt: record.grantedAt,\n expiresAt: null,\n source: 'inherited',\n scopeId: record.scopeId,\n scopeName: scopeNames.get(record.scopeId) ?? null,\n })),\n ];\n}\n\nasync function getEffectiveFlowPermission(\n db: PluginDatabaseApi,\n flowId: string,\n identity: InvectIdentity,\n): Promise<FlowAccessPermission | null> {\n const records = await getEffectiveFlowAccessRecords(\n db,\n flowId,\n identity.id,\n identity.teamIds ?? [],\n );\n\n let highest: FlowAccessPermission | null = null;\n for (const record of records) {\n highest = getHigherPermission(highest, record.permission);\n }\n return highest;\n}\n\nasync function getCurrentUserAccessibleFlows(\n db: PluginDatabaseApi,\n identity: InvectIdentity,\n): Promise<{ flowIds: string[]; permissions: Record<string, FlowAccessPermission | null> }> {\n const rows = await db.query<{ id: string }>('SELECT id FROM flows');\n const permissions: Record<string, FlowAccessPermission | null> = {};\n\n await Promise.all(\n rows.map(async (row) => {\n permissions[row.id] = await getEffectiveFlowPermission(db, row.id, identity);\n }),\n );\n\n const flowIds = rows.map((row) => row.id).filter((flowId) => permissions[flowId]);\n return { flowIds, permissions };\n}\n\nasync function getScopePath(db: PluginDatabaseApi, scopeId: string | null): Promise<string[]> {\n if (!scopeId) {\n return [];\n }\n\n const rows = await db.query<{ id: string; name: string }>(\n `WITH RECURSIVE ancestors AS (\n SELECT id, name, parent_id, 0 AS depth FROM rbac_teams WHERE id = ?\n UNION ALL\n SELECT parent.id, parent.name, parent.parent_id, current.depth + 1\n FROM rbac_teams parent\n INNER JOIN ancestors current ON parent.id = current.parent_id\n )\n SELECT id, name FROM ancestors ORDER BY depth DESC`,\n [scopeId],\n );\n return rows.map((row) => row.name);\n}\n\nasync function listAllScopeAccessForScopeIds(\n db: PluginDatabaseApi,\n scopeIds: string[],\n): Promise<ScopeAccessRecord[]> {\n if (scopeIds.length === 0) {\n return [];\n }\n const rows = await db.query<Record<string, unknown>>(\n `SELECT id, scope_id, user_id, team_id, permission, granted_by, granted_at\n FROM rbac_scope_access\n WHERE scope_id IN (${createInClause(scopeIds.length)})`,\n scopeIds,\n );\n return rows.map(normalizeScopeAccessRecord);\n}\n\nasync function listAllDirectFlowAccess(\n db: PluginDatabaseApi,\n flowId: string,\n): Promise<FlowAccessRecord[]> {\n const rows = await db.query<Record<string, unknown>>(\n 'SELECT id, flow_id, user_id, team_id, permission, granted_by, granted_at, expires_at FROM flow_access WHERE flow_id = ?',\n [flowId],\n );\n const now = Date.now();\n return rows\n .map(normalizeFlowAccessRecord)\n .filter((record) => !record.expiresAt || new Date(record.expiresAt).getTime() > now);\n}\n\nasync function listAllEffectiveFlowAccessForPreview(\n db: PluginDatabaseApi,\n flowId: string,\n overrideScopeId?: string | null,\n): Promise<Array<FlowAccessRecord | ScopeAccessRecord>> {\n const direct = await listAllDirectFlowAccess(db, flowId);\n const scopeId =\n overrideScopeId === undefined ? await getFlowScopeId(db, flowId) : overrideScopeId;\n if (!scopeId) {\n return direct;\n }\n\n const ancestorIds = await getAncestorScopeIds(db, scopeId);\n const inherited = await listAllScopeAccessForScopeIds(db, ancestorIds);\n return [...direct, ...inherited];\n}\n\nasync function listAllEffectiveFlowAccessRecords(\n db: PluginDatabaseApi,\n flowId: string,\n): Promise<EffectiveAccessRecord[]> {\n const directRecords = (await listAllDirectFlowAccess(db, flowId)).map<EffectiveAccessRecord>(\n (record) => ({\n ...record,\n source: 'direct',\n }),\n );\n\n const scopeId = await getFlowScopeId(db, flowId);\n if (!scopeId) {\n return directRecords;\n }\n\n const ancestorIds = await getAncestorScopeIds(db, scopeId);\n const inheritedRows = await listAllScopeAccessForScopeIds(db, ancestorIds);\n if (inheritedRows.length === 0) {\n return directRecords;\n }\n\n const scopeRows = await db.query<{ id: string; name: string }>(\n `SELECT id, name FROM rbac_teams WHERE id IN (${createInClause(ancestorIds.length)})`,\n ancestorIds,\n );\n const scopeNames = new Map(scopeRows.map((row) => [row.id, row.name]));\n\n // For team-based inherited grants, expand into per-member user records so the\n // UI shows individual users rather than team entities.\n const teamIds = [\n ...new Set(inheritedRows.map((r) => r.teamId).filter((id): id is string => !!id)),\n ];\n const teamMembersByTeamId = new Map<string, string[]>();\n if (teamIds.length > 0) {\n const memberRows = await db.query<{ team_id: string; user_id: string }>(\n `SELECT team_id, user_id FROM rbac_team_members WHERE team_id IN (${createInClause(teamIds.length)})`,\n teamIds,\n );\n for (const row of memberRows) {\n const members = teamMembersByTeamId.get(row.team_id) ?? [];\n members.push(row.user_id);\n teamMembersByTeamId.set(row.team_id, members);\n }\n }\n\n const expandedInherited: EffectiveAccessRecord[] = [];\n for (const record of inheritedRows) {\n const scopeName = scopeNames.get(record.scopeId) ?? null;\n if (record.teamId) {\n // Expand team grant → one record per team member\n const members = teamMembersByTeamId.get(record.teamId) ?? [];\n for (const userId of members) {\n expandedInherited.push({\n id: `${record.id}:${userId}`,\n flowId,\n userId,\n teamId: null,\n permission: record.permission,\n grantedBy: record.grantedBy,\n grantedAt: record.grantedAt,\n expiresAt: null,\n source: 'inherited',\n scopeId: record.scopeId,\n scopeName,\n });\n }\n } else {\n expandedInherited.push({\n id: record.id,\n flowId,\n userId: record.userId,\n teamId: null,\n permission: record.permission,\n grantedBy: record.grantedBy,\n grantedAt: record.grantedAt,\n expiresAt: null,\n source: 'inherited',\n scopeId: record.scopeId,\n scopeName,\n });\n }\n }\n\n return [...directRecords, ...expandedInherited];\n}\n\nfunction buildAccessKey(record: { userId?: string | null; teamId?: string | null }): string {\n if (record.userId) {\n return `user:${record.userId}`;\n }\n if (record.teamId) {\n return `team:${record.teamId}`;\n }\n return 'unknown';\n}\n\nasync function resolveAccessChangeNames(\n db: PluginDatabaseApi,\n entries: Array<{\n userId?: string | null;\n teamId?: string | null;\n permission: FlowAccessPermission;\n source: string;\n }>,\n): Promise<MovePreviewAccessChange[]> {\n const userIds = Array.from(\n new Set(entries.map((entry) => entry.userId).filter(Boolean)),\n ) as string[];\n const teamIds = Array.from(\n new Set(entries.map((entry) => entry.teamId).filter(Boolean)),\n ) as string[];\n\n const [userRows, teamRows] = await Promise.all([\n userIds.length > 0\n ? db.query<{ id: string; name: string | null; email: string | null }>(\n `SELECT id, name, email FROM user WHERE id IN (${createInClause(userIds.length)})`,\n userIds,\n )\n : Promise.resolve([]),\n teamIds.length > 0\n ? db.query<{ id: string; name: string }>(\n `SELECT id, name FROM rbac_teams WHERE id IN (${createInClause(teamIds.length)})`,\n teamIds,\n )\n : Promise.resolve([]),\n ]);\n\n const userMap = new Map(userRows.map((row) => [row.id, row.name || row.email || row.id]));\n const teamMap = new Map(teamRows.map((row) => [row.id, row.name]));\n\n return entries.map((entry) => ({\n userId: entry.userId ?? undefined,\n teamId: entry.teamId ?? undefined,\n name: entry.userId\n ? (userMap.get(entry.userId) ?? entry.userId)\n : entry.teamId\n ? (teamMap.get(entry.teamId) ?? entry.teamId)\n : 'Unknown',\n permission: entry.permission,\n source: entry.source,\n }));\n}\n\n// ─────────────────────────────────────────────────────────────\n// Plugin factory\n// ─────────────────────────────────────────────────────────────\n\nexport function rbacPlugin(options: RbacPluginOptions = {}): InvectPlugin {\n const { useFlowAccessTable = true, adminPermission = 'flow:read', enableTeams = true } = options;\n\n // ── Plugin-owned tables ─────────────────────────────────────\n const teamsSchema: InvectPluginSchema = enableTeams\n ? {\n flows: {\n fields: {\n scope_id: {\n type: 'string',\n required: false,\n references: { table: 'rbac_teams', field: 'id', onDelete: 'set null' },\n index: true,\n },\n },\n },\n rbac_teams: {\n fields: {\n id: { type: 'string', primaryKey: true },\n name: { type: 'string', required: true },\n description: { type: 'text', required: false },\n parent_id: {\n type: 'string',\n required: false,\n references: { table: 'rbac_teams', field: 'id', onDelete: 'set null' },\n index: true,\n },\n created_by: {\n type: 'string',\n required: false,\n references: { table: 'user', field: 'id' },\n },\n created_at: { type: 'date', required: true, defaultValue: 'now()' },\n updated_at: { type: 'date', required: false },\n },\n },\n rbac_team_members: {\n fields: {\n id: { type: 'string', primaryKey: true },\n team_id: {\n type: 'string',\n required: true,\n references: { table: 'rbac_teams', field: 'id', onDelete: 'cascade' },\n index: true,\n },\n user_id: {\n type: 'string',\n required: true,\n references: { table: 'user', field: 'id', onDelete: 'cascade' },\n index: true,\n },\n created_at: { type: 'date', required: true, defaultValue: 'now()' },\n },\n },\n rbac_scope_access: {\n fields: {\n id: { type: 'string', primaryKey: true },\n scope_id: {\n type: 'string',\n required: true,\n references: { table: 'rbac_teams', field: 'id', onDelete: 'cascade' },\n index: true,\n },\n user_id: { type: 'string', required: false, index: true },\n team_id: { type: 'string', required: false, index: true },\n permission: {\n type: 'string',\n required: true,\n defaultValue: 'viewer',\n typeAnnotation: 'FlowAccessPermission',\n },\n granted_by: { type: 'string', required: false },\n granted_at: { type: 'date', required: true, defaultValue: 'now()' },\n },\n },\n }\n : {};\n\n // Mutable DB reference — captured from the first endpoint handler.\n // Used by the onRequest hook to resolve teamIds for the identity.\n let capturedDbApi: PluginDatabaseApi | null = null;\n\n // UI manifest — declares what the frontend should render.\n // Component IDs are resolved by the frontend plugin's component registry.\n const ui: PluginUIManifest = {\n sidebar: [\n {\n label: 'Access Control',\n icon: 'Shield',\n path: '/access',\n permission: adminPermission,\n },\n ...(enableTeams ? [] : []),\n ],\n pages: [\n {\n path: '/access',\n componentId: 'rbac.AccessControlPage',\n title: 'Access Control',\n },\n ...(enableTeams ? [] : []),\n ],\n panelTabs: [\n {\n context: 'flowEditor',\n label: 'Access',\n componentId: 'rbac.FlowAccessPanel',\n permission: 'flow:read',\n },\n ],\n headerActions: [\n {\n context: 'flowHeader',\n componentId: 'rbac.ShareButton',\n permission: 'flow:update',\n },\n ],\n };\n\n return {\n id: 'rbac',\n name: 'Role-Based Access Control',\n\n // Plugin-owned database tables\n schema: teamsSchema,\n\n // RBAC uses the core flow_access table (already checked by core),\n // but also depends on the auth plugin's tables for user identity.\n // Declaring them here ensures a clear error if the developer has the\n // RBAC plugin enabled but forgot the better-auth tables.\n requiredTables: [\n 'user',\n 'session',\n ...(enableTeams ? ['rbac_teams', 'rbac_team_members', 'rbac_scope_access'] : []),\n ],\n setupInstructions:\n 'The RBAC plugin requires better-auth tables (user, session). ' +\n 'Make sure @invect/user-auth is configured, then run ' +\n '`npx invect generate` followed by `npx drizzle-kit push`.',\n\n // ─── Initialization ───────────────────────────────────────\n\n init: async (ctx: InvectPluginContext) => {\n // Verify that the auth plugin is loaded\n if (!ctx.hasPlugin('better-auth')) {\n ctx.logger.warn(\n 'RBAC plugin requires the @invect/user-auth plugin. ' +\n 'RBAC will work with reduced functionality (no session resolution). ' +\n 'Make sure betterAuthPlugin() is registered before rbacPlugin().',\n );\n }\n\n ctx.logger.info('RBAC plugin initialized', {\n useFlowAccessTable,\n });\n },\n\n // ─── Plugin Endpoints ─────────────────────────────────────\n\n endpoints: [\n // ── Auth Info ──\n\n {\n method: 'GET',\n path: '/rbac/me',\n isPublic: false,\n handler: async (ctx) => {\n // Capture DB API on first endpoint call for the onRequest hook\n if (!capturedDbApi && enableTeams) {\n capturedDbApi = ctx.database;\n }\n\n const identity = ctx.identity;\n const permissions = ctx.core.getPermissions(identity);\n const resolvedRole = identity ? ctx.core.getResolvedRole(identity) : null;\n\n return {\n status: 200,\n body: {\n identity: identity\n ? {\n id: identity.id,\n name: identity.name,\n role: identity.role,\n resolvedRole,\n }\n : null,\n permissions,\n isAuthenticated: !!identity,\n },\n };\n },\n },\n\n {\n method: 'GET',\n path: '/rbac/roles',\n isPublic: false,\n permission: 'flow:read',\n handler: async (ctx) => {\n const roles = ctx.core.getAvailableRoles();\n return { status: 200, body: { roles } };\n },\n },\n\n // ── UI Manifest ──\n\n {\n method: 'GET',\n path: '/rbac/ui-manifest',\n isPublic: true,\n handler: async (_ctx) => {\n return {\n status: 200,\n body: {\n id: 'rbac',\n ...ui,\n },\n };\n },\n },\n\n // ── Flow Access Management ──\n\n {\n method: 'GET',\n path: '/rbac/flows/:flowId/access',\n permission: 'flow:read',\n handler: async (ctx) => {\n const flowId = ctx.params.flowId;\n if (!flowId) {\n return { status: 400, body: { error: 'Missing flowId parameter' } };\n }\n\n if (!ctx.core.isFlowAccessTableEnabled()) {\n return {\n status: 501,\n body: {\n error: 'Not Implemented',\n message:\n 'Flow access table not enabled. Set auth.useFlowAccessTable: true in config.',\n },\n };\n }\n\n if (!ctx.identity) {\n return {\n status: 401,\n body: { error: 'Unauthorized', message: 'Authentication required' },\n };\n }\n\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n const callerPermission = isAdmin\n ? 'owner'\n : await getEffectiveFlowPermission(ctx.database, flowId, ctx.identity);\n\n if (!callerPermission) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'No access to this flow' },\n };\n }\n\n const access = await ctx.core.listFlowAccess(flowId);\n return { status: 200, body: { access } };\n },\n },\n\n {\n method: 'POST',\n path: '/rbac/flows/:flowId/access',\n permission: 'flow:read',\n handler: async (ctx) => {\n const flowId = ctx.params.flowId;\n if (!flowId) {\n return { status: 400, body: { error: 'Missing flowId parameter' } };\n }\n\n if (!ctx.core.isFlowAccessTableEnabled()) {\n return {\n status: 501,\n body: {\n error: 'Not Implemented',\n message:\n 'Flow access table not enabled. Set auth.useFlowAccessTable: true in config.',\n },\n };\n }\n\n const { userId, teamId, permission, expiresAt } = ctx.body as {\n userId?: string;\n teamId?: string;\n permission?: string;\n expiresAt?: string;\n };\n\n if (!userId && !teamId) {\n return {\n status: 400,\n body: { error: 'Either userId or teamId must be provided' },\n };\n }\n\n if (!ctx.identity) {\n return {\n status: 401,\n body: { error: 'Unauthorized', message: 'Authentication required' },\n };\n }\n\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n const callerPermission = isAdmin\n ? 'owner'\n : await getEffectiveFlowPermission(ctx.database, flowId, ctx.identity);\n\n if (callerPermission !== 'owner') {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Owner access is required to manage sharing' },\n };\n }\n\n if (!permission || !['owner', 'editor', 'operator', 'viewer'].includes(permission)) {\n return {\n status: 400,\n body: { error: 'permission must be one of: owner, editor, operator, viewer' },\n };\n }\n\n const access = await ctx.core.grantFlowAccess({\n flowId,\n userId,\n teamId,\n permission: permission as 'owner' | 'editor' | 'operator' | 'viewer',\n grantedBy: ctx.identity?.id,\n expiresAt,\n });\n return { status: 201, body: access };\n },\n },\n\n {\n method: 'DELETE',\n path: '/rbac/flows/:flowId/access/:accessId',\n permission: 'flow:read',\n handler: async (ctx) => {\n const { flowId, accessId } = ctx.params;\n if (!flowId || !accessId) {\n return { status: 400, body: { error: 'Missing flowId or accessId parameter' } };\n }\n\n if (!ctx.core.isFlowAccessTableEnabled()) {\n return {\n status: 501,\n body: {\n error: 'Not Implemented',\n message:\n 'Flow access table not enabled. Set auth.useFlowAccessTable: true in config.',\n },\n };\n }\n\n if (!ctx.identity) {\n return {\n status: 401,\n body: { error: 'Unauthorized', message: 'Authentication required' },\n };\n }\n\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n const callerPermission = isAdmin\n ? 'owner'\n : await getEffectiveFlowPermission(ctx.database, flowId, ctx.identity);\n\n if (callerPermission !== 'owner') {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Owner access is required to manage sharing' },\n };\n }\n\n await ctx.core.revokeFlowAccess(accessId);\n return { status: 204, body: null };\n },\n },\n\n {\n method: 'GET',\n path: '/rbac/flows/accessible',\n isPublic: false,\n handler: async (ctx) => {\n if (!ctx.core.isFlowAccessTableEnabled()) {\n return {\n status: 501,\n body: {\n error: 'Not Implemented',\n message:\n 'Flow access table not enabled. Set auth.useFlowAccessTable: true in config.',\n },\n };\n }\n\n const identity = ctx.identity;\n if (!identity) {\n return {\n status: 401,\n body: { error: 'Unauthorized', message: 'Authentication required' },\n };\n }\n\n // Admins see all flows with implicit owner permission\n const isAdmin = ctx.core.getPermissions(identity).includes('admin:*');\n if (isAdmin) {\n return {\n status: 200,\n body: { flowIds: [], permissions: {}, isAdmin: true },\n };\n }\n\n const { flowIds, permissions } = await getCurrentUserAccessibleFlows(\n ctx.database,\n identity,\n );\n\n return {\n status: 200,\n body: {\n flowIds,\n permissions,\n isAdmin: false,\n },\n };\n },\n },\n\n {\n method: 'GET',\n path: '/rbac/flows/:flowId/effective-access',\n permission: 'flow:read',\n handler: async (ctx) => {\n const flowId = ctx.params.flowId;\n if (!flowId) {\n return { status: 400, body: { error: 'Missing flowId parameter' } };\n }\n if (!ctx.identity) {\n return {\n status: 401,\n body: { error: 'Unauthorized', message: 'Authentication required' },\n };\n }\n\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n const callerPermission = isAdmin\n ? 'owner'\n : await getEffectiveFlowPermission(ctx.database, flowId, ctx.identity);\n\n if (!callerPermission) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'No access to this flow' },\n };\n }\n\n const records = await listAllEffectiveFlowAccessRecords(ctx.database, flowId);\n\n return {\n status: 200,\n body: {\n flowId,\n scopeId: await getFlowScopeId(ctx.database, flowId),\n records,\n },\n };\n },\n },\n\n {\n method: 'PUT',\n path: '/rbac/flows/:flowId/scope',\n permission: 'flow:update',\n handler: async (ctx) => {\n const flowId = ctx.params.flowId;\n const { scopeId } = ctx.body as { scopeId?: string | null };\n if (!flowId) {\n return { status: 400, body: { error: 'Missing flowId parameter' } };\n }\n if (!ctx.identity) {\n return {\n status: 401,\n body: { error: 'Unauthorized', message: 'Authentication required' },\n };\n }\n\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n const callerPermission = isAdmin\n ? 'owner'\n : await getEffectiveFlowPermission(ctx.database, flowId, ctx.identity);\n if (callerPermission !== 'owner') {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Owner access is required to move flows' },\n };\n }\n\n if (scopeId) {\n const scopeRows = await ctx.database.query<{ id: string }>(\n 'SELECT id FROM rbac_teams WHERE id = ?',\n [scopeId],\n );\n if (scopeRows.length === 0) {\n return { status: 404, body: { error: 'Scope not found' } };\n }\n }\n\n await ctx.database.execute('UPDATE flows SET scope_id = ? WHERE id = ?', [\n scopeId ?? null,\n flowId,\n ]);\n return { status: 200, body: { success: true, flowId, scopeId: scopeId ?? null } };\n },\n },\n\n // ── Teams Management ──\n\n ...(enableTeams\n ? [\n {\n method: 'GET' as const,\n path: '/rbac/scopes/tree',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n\n const [teamRows, flowRows, memberRows, accessRows, teamRoleRows] =\n await Promise.all([\n ctx.database.query<{\n id: string;\n name: string;\n description: string | null;\n parent_id: string | null;\n created_by: string | null;\n created_at: string;\n updated_at: string | null;\n }>(\n 'SELECT id, name, description, parent_id, created_by, created_at, updated_at FROM rbac_teams ORDER BY name',\n ),\n ctx.database.query<{ id: string; name: string; scope_id: string | null }>(\n 'SELECT id, name, scope_id FROM flows ORDER BY name',\n ),\n ctx.database.query<{ team_id: string; member_count: number }>(\n 'SELECT team_id, COUNT(*) AS member_count FROM rbac_team_members GROUP BY team_id',\n ),\n ctx.database.query<{ scope_id: string; access_count: number }>(\n 'SELECT scope_id, COUNT(*) AS access_count FROM rbac_scope_access GROUP BY scope_id',\n ),\n ctx.database.query<{ scope_id: string; permission: FlowAccessPermission }>(\n 'SELECT scope_id, permission FROM rbac_scope_access WHERE team_id = scope_id AND team_id IS NOT NULL',\n ),\n ]);\n\n const memberCounts = new Map(\n memberRows.map((row) => [row.team_id, Number(row.member_count)]),\n );\n const accessCounts = new Map(\n accessRows.map((row) => [row.scope_id, Number(row.access_count)]),\n );\n const teamPermissions = new Map(\n teamRoleRows.map((row) => [row.scope_id, row.permission]),\n );\n\n const nodeMap = new Map<string, ScopeTreeNode>();\n for (const row of teamRows) {\n nodeMap.set(row.id, {\n ...normalizeTeamRow(row),\n children: [],\n flows: [],\n directAccessCount: accessCounts.get(row.id) ?? 0,\n memberCount: memberCounts.get(row.id) ?? 0,\n teamPermission: teamPermissions.get(row.id) ?? null,\n });\n }\n\n const roots: ScopeTreeNode[] = [];\n for (const node of nodeMap.values()) {\n if (node.parentId && nodeMap.has(node.parentId)) {\n nodeMap.get(node.parentId)?.children.push(node);\n } else {\n roots.push(node);\n }\n }\n\n const unscopedFlows: Array<{ id: string; name: string; scopeId: string | null }> =\n [];\n for (const flow of flowRows) {\n const mapped = { id: flow.id, name: flow.name, scopeId: flow.scope_id };\n if (flow.scope_id && nodeMap.has(flow.scope_id)) {\n nodeMap.get(flow.scope_id)?.flows.push(mapped);\n } else {\n unscopedFlows.push(mapped);\n }\n }\n\n return { status: 200, body: { scopes: roots, unscopedFlows } };\n },\n },\n\n {\n method: 'GET' as const,\n path: '/rbac/scopes/:scopeId/access',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n\n const rows = await ctx.database.query<Record<string, unknown>>(\n 'SELECT id, scope_id, user_id, team_id, permission, granted_by, granted_at FROM rbac_scope_access WHERE scope_id = ?',\n [ctx.params.scopeId],\n );\n return { status: 200, body: { access: rows.map(normalizeScopeAccessRecord) } };\n },\n },\n\n {\n method: 'POST' as const,\n path: '/rbac/scopes/:scopeId/access',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n const { scopeId } = ctx.params;\n const { userId, teamId, permission } = ctx.body as {\n userId?: string;\n teamId?: string;\n permission?: string;\n };\n\n if (!scopeId) {\n return { status: 400, body: { error: 'Missing scopeId parameter' } };\n }\n if (!userId && !teamId) {\n return {\n status: 400,\n body: { error: 'Either userId or teamId must be provided' },\n };\n }\n if (userId && teamId) {\n return {\n status: 400,\n body: { error: 'Provide either userId or teamId, not both' },\n };\n }\n if (teamId && teamId !== scopeId) {\n return {\n status: 400,\n body: { error: 'Teams can only hold a role on their own scope' },\n };\n }\n if (!isFlowAccessPermission(permission)) {\n return {\n status: 400,\n body: { error: 'permission must be one of: owner, editor, operator, viewer' },\n };\n }\n\n const existing = await ctx.database.query<{ id: string }>(\n 'SELECT id FROM rbac_scope_access WHERE scope_id = ? AND user_id IS ? AND team_id IS ?',\n [scopeId, userId ?? null, teamId ?? null],\n );\n\n const now = new Date().toISOString();\n if (existing[0]) {\n await ctx.database.execute(\n 'UPDATE rbac_scope_access SET permission = ?, granted_by = ?, granted_at = ? WHERE id = ?',\n [permission, ctx.identity.id, now, existing[0].id],\n );\n return {\n status: 200,\n body: {\n id: existing[0].id,\n scopeId,\n userId: userId ?? null,\n teamId: teamId ?? null,\n permission,\n grantedBy: ctx.identity.id,\n grantedAt: now,\n },\n };\n }\n\n const id = crypto.randomUUID();\n await ctx.database.execute(\n 'INSERT INTO rbac_scope_access (id, scope_id, user_id, team_id, permission, granted_by, granted_at) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [id, scopeId, userId ?? null, teamId ?? null, permission, ctx.identity.id, now],\n );\n\n return {\n status: 201,\n body: {\n id,\n scopeId,\n userId: userId ?? null,\n teamId: teamId ?? null,\n permission,\n grantedBy: ctx.identity.id,\n grantedAt: now,\n },\n };\n },\n },\n\n {\n method: 'DELETE' as const,\n path: '/rbac/scopes/:scopeId/access/:accessId',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n await ctx.database.execute('DELETE FROM rbac_scope_access WHERE id = ?', [\n ctx.params.accessId,\n ]);\n return { status: 204, body: null };\n },\n },\n\n {\n method: 'POST' as const,\n path: '/rbac/preview-move',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n const { type, id, targetScopeId } = ctx.body as {\n type?: 'flow' | 'scope';\n id?: string;\n targetScopeId?: string | null;\n };\n\n if (!type || !id) {\n return { status: 400, body: { error: 'type and id are required' } };\n }\n\n let affectedFlowIds: string[] = [];\n let itemName = id;\n if (type === 'flow') {\n const rows = await ctx.database.query<{ id: string; name: string }>(\n 'SELECT id, name FROM flows WHERE id = ?',\n [id],\n );\n if (!rows[0]) {\n return { status: 404, body: { error: 'Flow not found' } };\n }\n itemName = rows[0].name;\n affectedFlowIds = [id];\n } else {\n const scopeRows = await ctx.database.query<{ id: string; name: string }>(\n 'SELECT id, name FROM rbac_teams WHERE id = ?',\n [id],\n );\n if (!scopeRows[0]) {\n return { status: 404, body: { error: 'Scope not found' } };\n }\n itemName = scopeRows[0].name;\n const descendantScopeIds = await getDescendantScopeIds(ctx.database, id);\n if (targetScopeId && descendantScopeIds.includes(targetScopeId)) {\n return {\n status: 400,\n body: { error: 'Cannot move a scope into itself or its descendant' },\n };\n }\n const flowRows = await ctx.database.query<{ id: string }>(\n `SELECT id FROM flows WHERE scope_id IN (${createInClause(descendantScopeIds.length)})`,\n descendantScopeIds,\n );\n affectedFlowIds = flowRows.map((row) => row.id);\n }\n\n const targetPath = await getScopePath(ctx.database, targetScopeId ?? null);\n const targetAncestorIds = targetScopeId\n ? await getAncestorScopeIds(ctx.database, targetScopeId)\n : [];\n const targetScopeAccess = await listAllScopeAccessForScopeIds(\n ctx.database,\n targetAncestorIds,\n );\n\n const gainedEntries = new Map<\n string,\n {\n userId?: string | null;\n teamId?: string | null;\n permission: FlowAccessPermission;\n source: string;\n }\n >();\n let unchanged = 0;\n\n for (const flowId of affectedFlowIds) {\n const currentRecords = await listAllEffectiveFlowAccessForPreview(\n ctx.database,\n flowId,\n );\n const currentPermissions = new Map<string, FlowAccessPermission>();\n for (const record of currentRecords) {\n const permission = toFlowAccessPermission(record.permission);\n if (!permission) {\n continue;\n }\n const key = buildAccessKey(record);\n currentPermissions.set(\n key,\n getHigherPermission(currentPermissions.get(key) ?? null, permission) ??\n permission,\n );\n }\n\n for (const record of targetScopeAccess) {\n const key = buildAccessKey(record);\n const existingPermission = currentPermissions.get(key) ?? null;\n if (\n existingPermission &&\n FLOW_PERMISSION_LEVELS[existingPermission] >=\n FLOW_PERMISSION_LEVELS[record.permission]\n ) {\n unchanged += 1;\n continue;\n }\n gainedEntries.set(key, {\n userId: record.userId,\n teamId: record.teamId,\n permission: record.permission,\n source: `from ${targetPath.at(-1) ?? 'root'}`,\n });\n }\n }\n\n const gained = await resolveAccessChangeNames(\n ctx.database,\n Array.from(gainedEntries.values()),\n );\n\n return {\n status: 200,\n body: {\n item: { id, name: itemName, type },\n target: {\n id: targetScopeId ?? null,\n name: targetPath.at(-1) ?? 'Unscoped',\n path: targetPath,\n },\n affectedFlows: affectedFlowIds.length,\n accessChanges: {\n gained,\n unchanged,\n },\n },\n };\n },\n },\n\n // List all teams\n {\n method: 'GET' as const,\n path: '/rbac/teams',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n\n const rows = await ctx.database.query<{\n id: string;\n name: string;\n description: string | null;\n parent_id: string | null;\n created_by: string | null;\n created_at: string;\n updated_at: string | null;\n }>(\n 'SELECT id, name, description, parent_id, created_by, created_at, updated_at FROM rbac_teams ORDER BY name',\n );\n\n const teams = rows.map((r) => normalizeTeamRow(r));\n\n return { status: 200, body: { teams } };\n },\n },\n\n // Create a team (admin only)\n {\n method: 'POST' as const,\n path: '/rbac/teams',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n const { name, description, parentId } = ctx.body as {\n name?: string;\n description?: string;\n parentId?: string | null;\n };\n if (!name?.trim()) {\n return { status: 400, body: { error: 'Team name is required' } };\n }\n\n if (parentId) {\n const parentRows = await ctx.database.query<{ id: string }>(\n 'SELECT id FROM rbac_teams WHERE id = ?',\n [parentId],\n );\n if (parentRows.length === 0) {\n return { status: 404, body: { error: 'Parent scope not found' } };\n }\n }\n\n const id = crypto.randomUUID();\n const now = new Date().toISOString();\n await ctx.database.execute(\n 'INSERT INTO rbac_teams (id, name, description, parent_id, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [\n id,\n name.trim(),\n description?.trim() || null,\n parentId ?? null,\n ctx.identity.id,\n now,\n now,\n ],\n );\n\n return {\n status: 201,\n body: {\n id,\n name: name.trim(),\n description: description?.trim() || null,\n parentId: parentId ?? null,\n createdBy: ctx.identity.id,\n createdAt: now,\n updatedAt: now,\n },\n };\n },\n },\n\n // Update a team (admin only)\n {\n method: 'PUT' as const,\n path: '/rbac/teams/:teamId',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n const { teamId } = ctx.params;\n const { name, description, parentId } = ctx.body as {\n name?: string;\n description?: string;\n parentId?: string | null;\n };\n\n const existing = await ctx.database.query<{ id: string }>(\n 'SELECT id FROM rbac_teams WHERE id = ?',\n [teamId],\n );\n if (existing.length === 0) {\n return { status: 404, body: { error: 'Team not found' } };\n }\n\n if (parentId === teamId) {\n return { status: 400, body: { error: 'A scope cannot be its own parent' } };\n }\n if (parentId) {\n const parentRows = await ctx.database.query<{ id: string }>(\n 'SELECT id FROM rbac_teams WHERE id = ?',\n [parentId],\n );\n if (parentRows.length === 0) {\n return { status: 404, body: { error: 'Parent scope not found' } };\n }\n const descendantScopeIds = await getDescendantScopeIds(ctx.database, teamId);\n if (descendantScopeIds.includes(parentId)) {\n return {\n status: 400,\n body: { error: 'Cannot move a scope into itself or its descendant' },\n };\n }\n }\n\n const updates: string[] = [];\n const values: unknown[] = [];\n if (name !== undefined) {\n if (!name.trim()) {\n return { status: 400, body: { error: 'Team name cannot be empty' } };\n }\n updates.push('name = ?');\n values.push(name.trim());\n }\n if (description !== undefined) {\n updates.push('description = ?');\n values.push(description?.trim() || null);\n }\n if (parentId !== undefined) {\n updates.push('parent_id = ?');\n values.push(parentId ?? null);\n }\n if (updates.length === 0) {\n return { status: 400, body: { error: 'No fields to update' } };\n }\n\n updates.push('updated_at = ?');\n values.push(new Date().toISOString());\n values.push(teamId);\n\n await ctx.database.execute(\n `UPDATE rbac_teams SET ${updates.join(', ')} WHERE id = ?`,\n values,\n );\n\n return { status: 200, body: { success: true } };\n },\n },\n\n // Delete a team (admin only)\n {\n method: 'DELETE' as const,\n path: '/rbac/teams/:teamId',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n const { teamId } = ctx.params;\n const teams = await ctx.database.query<{ id: string; parent_id: string | null }>(\n 'SELECT id, parent_id FROM rbac_teams WHERE id = ?',\n [teamId],\n );\n if (teams.length === 0) {\n return { status: 404, body: { error: 'Team not found' } };\n }\n\n const parentId = teams[0].parent_id ?? null;\n await ctx.database.execute('UPDATE flows SET scope_id = ? WHERE scope_id = ?', [\n parentId,\n teamId,\n ]);\n\n // CASCADE on team_members handles cleanup\n await ctx.database.execute('DELETE FROM rbac_teams WHERE id = ?', [teamId]);\n return { status: 204, body: null };\n },\n },\n\n // Get team with members\n {\n method: 'GET' as const,\n path: '/rbac/teams/:teamId',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n\n const { teamId } = ctx.params;\n const teams = await ctx.database.query<{\n id: string;\n name: string;\n description: string | null;\n parent_id: string | null;\n created_by: string | null;\n created_at: string;\n updated_at: string | null;\n }>(\n 'SELECT id, name, description, parent_id, created_by, created_at, updated_at FROM rbac_teams WHERE id = ?',\n [teamId],\n );\n\n if (teams.length === 0) {\n return { status: 404, body: { error: 'Team not found' } };\n }\n\n const members = await ctx.database.query<{\n id: string;\n team_id: string;\n user_id: string;\n created_at: string;\n }>(\n 'SELECT id, team_id, user_id, created_at FROM rbac_team_members WHERE team_id = ?',\n [teamId],\n );\n\n const team = teams[0];\n return {\n status: 200,\n body: {\n ...normalizeTeamRow(team),\n members: members.map((m) => ({\n id: m.id,\n teamId: m.team_id,\n userId: m.user_id,\n createdAt: m.created_at,\n })),\n },\n };\n },\n },\n\n // Add member to team (admin only)\n {\n method: 'POST' as const,\n path: '/rbac/teams/:teamId/members',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n const { teamId } = ctx.params;\n const { userId } = ctx.body as { userId?: string };\n if (!userId?.trim()) {\n return { status: 400, body: { error: 'userId is required' } };\n }\n\n // Check team exists\n const teams = await ctx.database.query<{ id: string }>(\n 'SELECT id FROM rbac_teams WHERE id = ?',\n [teamId],\n );\n if (teams.length === 0) {\n return { status: 404, body: { error: 'Team not found' } };\n }\n\n // Check not already a member\n const existing = await ctx.database.query<{ id: string }>(\n 'SELECT id FROM rbac_team_members WHERE team_id = ? AND user_id = ?',\n [teamId, userId.trim()],\n );\n if (existing.length > 0) {\n return { status: 409, body: { error: 'User is already a member of this team' } };\n }\n\n const id = crypto.randomUUID();\n const now = new Date().toISOString();\n await ctx.database.execute(\n 'INSERT INTO rbac_team_members (id, team_id, user_id, created_at) VALUES (?, ?, ?, ?)',\n [id, teamId, userId.trim(), now],\n );\n\n return {\n status: 201,\n body: { id, teamId, userId: userId.trim(), createdAt: now },\n };\n },\n },\n\n // Remove member from team (admin only)\n {\n method: 'DELETE' as const,\n path: '/rbac/teams/:teamId/members/:userId',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n const { teamId, userId } = ctx.params;\n await ctx.database.execute(\n 'DELETE FROM rbac_team_members WHERE team_id = ? AND user_id = ?',\n [teamId, userId],\n );\n return { status: 204, body: null };\n },\n },\n\n // Get teams for current user (for identity resolution)\n {\n method: 'GET' as const,\n path: '/rbac/my-teams',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n\n const rows = await ctx.database.query<{\n id: string;\n name: string;\n description: string | null;\n parent_id: string | null;\n created_by: string | null;\n created_at: string;\n updated_at: string | null;\n }>(\n 'SELECT t.id, t.name, t.description, t.parent_id, t.created_by, t.created_at, t.updated_at ' +\n 'FROM rbac_teams t INNER JOIN rbac_team_members tm ON t.id = tm.team_id ' +\n 'WHERE tm.user_id = ? ORDER BY t.name',\n [ctx.identity.id],\n );\n\n const teams = rows.map((r) => normalizeTeamRow(r));\n\n return { status: 200, body: { teams } };\n },\n },\n ]\n : []),\n ],\n\n // ─── Lifecycle Hooks ──────────────────────────────────────\n\n hooks: {\n // Enrich identity with teamIds after auth plugin resolves the session.\n // This runs after the auth plugin's onRequest hook since RBAC is registered\n // after auth. We query rbac_team_members to get the user's team IDs.\n onRequest: enableTeams\n ? async (\n _request: Request,\n context: { path: string; method: string; identity: InvectIdentity | null },\n ) => {\n if (!context.identity || !capturedDbApi) {\n return;\n }\n // Skip if teamIds already populated (custom mapUser)\n if (context.identity.teamIds && context.identity.teamIds.length > 0) {\n return;\n }\n\n try {\n const rows = await capturedDbApi.query<{ team_id: string }>(\n 'SELECT team_id FROM rbac_team_members WHERE user_id = ?',\n [context.identity.id],\n );\n if (rows.length > 0) {\n context.identity = {\n ...context.identity,\n teamIds: rows.map((r) => r.team_id),\n };\n }\n } catch {\n // Silently ignore — table may not exist yet during initial setup\n }\n }\n : undefined,\n\n // Enforce flow-level ACLs on authorization checks\n onAuthorize: async (context) => {\n if (!useFlowAccessTable) {\n return; // Defer to default RBAC\n }\n\n const { identity, resource, action } = context;\n\n // Only enforce for flow-related resources with specific IDs\n if (!identity || !resource?.id) {\n return;\n }\n\n if (!FLOW_RESOURCE_TYPES.has(resource.type)) {\n return;\n }\n\n if (identity.permissions?.includes('admin:*') || identity.role === 'admin') {\n return { allowed: true };\n }\n\n const database = context.database ?? capturedDbApi;\n if (!database) {\n return;\n }\n\n const effectivePermission = await getEffectiveFlowPermission(\n database,\n resource.id,\n identity,\n );\n if (!effectivePermission) {\n return { allowed: false };\n }\n\n const requiredPermission = mapActionToRequiredPermission(action);\n return {\n allowed:\n FLOW_PERMISSION_LEVELS[effectivePermission] >=\n FLOW_PERMISSION_LEVELS[requiredPermission],\n };\n },\n\n // Auto-grant owner access when a flow is created\n afterFlowRun: async (_context) => {\n // This hook is for flow *runs*, not flow creation.\n // Flow creation auto-grant is handled by the core when\n // auth.useFlowAccessTable is true.\n },\n },\n\n // ─── Error Codes ──────────────────────────────────────────\n\n $ERROR_CODES: {\n 'rbac:no_access': {\n message: 'You do not have access to this flow.',\n status: 403,\n },\n 'rbac:insufficient_permission': {\n message: 'Your access level is insufficient for this operation.',\n status: 403,\n },\n 'rbac:auth_required': {\n message: 'Authentication is required. Please sign in.',\n status: 401,\n },\n 'rbac:plugin_missing': {\n message: 'The RBAC plugin requires the @invect/user-auth plugin.',\n status: 500,\n },\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/backend/plugin.ts"],"sourcesContent":["/**\n * @invect/rbac — Backend Plugin\n *\n * RBAC plugin that provides:\n * - Flow access management endpoints (grant/revoke/list)\n * - Auth info endpoints (/auth/me, /auth/roles)\n * - Authorization hooks (enforces flow-level ACLs)\n * - UI manifest for the frontend plugin to render\n *\n * Requires the @invect/user-auth (better-auth) plugin to be loaded first\n * for session resolution. This plugin handles the *authorization* layer\n * on top of that authentication.\n */\n\nimport type {\n InvectPlugin,\n InvectPluginContext,\n InvectPermission,\n InvectPluginSchema,\n InvectIdentity,\n PluginDatabaseApi,\n PluginEndpointContext,\n} from '@invect/core';\nimport type {\n EffectiveAccessRecord,\n FlowAccessPermission,\n FlowAccessRecord,\n MovePreviewAccessChange,\n PluginUIManifest,\n ScopeAccessRecord,\n ScopeTreeNode,\n Team,\n} from '../shared/types';\n\n// ─────────────────────────────────────────────────────────────\n// Team ID Resolution Helper\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Resolve team IDs for a user from the rbac_team_members table.\n * Use this in a custom `mapUser` for the auth plugin to populate\n * `identity.teamIds` automatically.\n *\n * @example\n * ```ts\n * import { resolveTeamIds } from '@invect/rbac/backend';\n *\n * userAuth({\n * auth,\n * mapUser: async (user, session) => ({\n * id: user.id,\n * name: user.name ?? undefined,\n * role: user.role === 'admin' ? 'admin' : 'user',\n * teamIds: await resolveTeamIds(db, user.id),\n * }),\n * });\n * ```\n */\nexport async function resolveTeamIds(db: PluginDatabaseApi, userId: string): Promise<string[]> {\n const rows = await db.query<{ team_id: string }>(\n 'SELECT team_id FROM rbac_team_members WHERE user_id = ?',\n [userId],\n );\n return rows.map((r) => r.team_id);\n}\n\n// ─────────────────────────────────────────────────────────────\n// Types\n// ─────────────────────────────────────────────────────────────\n\nexport interface RbacPluginOptions {\n /**\n * Enable the flow access table for database-backed per-flow permissions.\n * When enabled, the plugin will check flow-level ACLs in addition to\n * role-based permissions. Requires `auth.useFlowAccessTable: true` in\n * the Invect config.\n *\n * @default true\n */\n useFlowAccessTable?: boolean;\n\n /**\n * Permission required to access the RBAC flows page.\n *\n * @default 'flow:read'\n */\n adminPermission?: InvectPermission;\n\n /**\n * Enable teams feature for grouping users.\n *\n * @default true\n */\n enableTeams?: boolean;\n}\n\nconst FLOW_RESOURCE_TYPES = new Set(['flow', 'flow-version', 'flow-run', 'node-execution']);\n\nconst FLOW_PERMISSION_LEVELS: Record<FlowAccessPermission, number> = {\n viewer: 1,\n operator: 2,\n editor: 3,\n owner: 4,\n};\n\nfunction isFlowAccessPermission(value: unknown): value is FlowAccessPermission {\n return value === 'viewer' || value === 'operator' || value === 'editor' || value === 'owner';\n}\n\nfunction toFlowAccessPermission(value: unknown): FlowAccessPermission | null {\n return isFlowAccessPermission(value) ? value : null;\n}\n\nfunction getHigherPermission(\n left: FlowAccessPermission | null,\n right: FlowAccessPermission | null,\n): FlowAccessPermission | null {\n if (!left) {\n return right;\n }\n if (!right) {\n return left;\n }\n return FLOW_PERMISSION_LEVELS[right] > FLOW_PERMISSION_LEVELS[left] ? right : left;\n}\n\nfunction createInClause(count: number): string {\n return Array.from({ length: count }, () => '?').join(', ');\n}\n\nfunction mapActionToRequiredPermission(action: string): FlowAccessPermission {\n if (action.includes('delete') || action.includes('share') || action.includes('admin')) {\n return 'owner';\n }\n if (action.includes('update') || action.includes('write') || action.includes('create')) {\n return 'editor';\n }\n if (action.includes('run') || action.includes('execute')) {\n return 'operator';\n }\n return 'viewer';\n}\n\nfunction normalizeFlowAccessRecord(row: Record<string, unknown>): FlowAccessRecord {\n return {\n id: String(row.id),\n flowId: String(row.flowId ?? row.flow_id),\n userId: (row.userId ?? row.user_id) ? String(row.userId ?? row.user_id) : null,\n teamId: (row.teamId ?? row.team_id) ? String(row.teamId ?? row.team_id) : null,\n permission: String(row.permission) as FlowAccessPermission,\n grantedBy: (row.grantedBy ?? row.granted_by) ? String(row.grantedBy ?? row.granted_by) : null,\n grantedAt: String(row.grantedAt ?? row.granted_at),\n expiresAt: (row.expiresAt ?? row.expires_at) ? String(row.expiresAt ?? row.expires_at) : null,\n };\n}\n\nfunction normalizeScopeAccessRecord(row: Record<string, unknown>): ScopeAccessRecord {\n return {\n id: String(row.id),\n scopeId: String(row.scopeId ?? row.scope_id),\n userId: (row.userId ?? row.user_id) ? String(row.userId ?? row.user_id) : null,\n teamId: (row.teamId ?? row.team_id) ? String(row.teamId ?? row.team_id) : null,\n permission: String(row.permission) as FlowAccessPermission,\n grantedBy: (row.grantedBy ?? row.granted_by) ? String(row.grantedBy ?? row.granted_by) : null,\n grantedAt: String(row.grantedAt ?? row.granted_at),\n };\n}\n\nfunction normalizeTeamRow(row: {\n id: string;\n name: string;\n description: string | null;\n parent_id?: string | null;\n created_by: string | null;\n created_at: string;\n updated_at: string | null;\n}): Team {\n return {\n id: row.id,\n name: row.name,\n description: row.description,\n parentId: row.parent_id ?? null,\n createdBy: row.created_by,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n };\n}\n\nasync function getFlowScopeId(db: PluginDatabaseApi, flowId: string): Promise<string | null> {\n const rows = await db.query<{ scope_id: string | null }>(\n 'SELECT scope_id FROM flows WHERE id = ?',\n [flowId],\n );\n return rows[0]?.scope_id ?? null;\n}\n\nasync function getAncestorScopeIds(db: PluginDatabaseApi, scopeId: string): Promise<string[]> {\n const rows = await db.query<{ id: string }>(\n `WITH RECURSIVE ancestors AS (\n SELECT id, parent_id FROM rbac_teams WHERE id = ?\n UNION ALL\n SELECT parent.id, parent.parent_id\n FROM rbac_teams parent\n INNER JOIN ancestors current ON parent.id = current.parent_id\n )\n SELECT id FROM ancestors`,\n [scopeId],\n );\n return rows.map((row) => row.id);\n}\n\nasync function getDescendantScopeIds(db: PluginDatabaseApi, scopeId: string): Promise<string[]> {\n const rows = await db.query<{ id: string }>(\n `WITH RECURSIVE descendants AS (\n SELECT id FROM rbac_teams WHERE id = ?\n UNION ALL\n SELECT child.id\n FROM rbac_teams child\n INNER JOIN descendants current ON child.parent_id = current.id\n )\n SELECT id FROM descendants`,\n [scopeId],\n );\n return rows.map((row) => row.id);\n}\n\nasync function listScopeAccessForScopeIds(\n db: PluginDatabaseApi,\n scopeIds: string[],\n userId: string,\n teamIds: string[],\n): Promise<ScopeAccessRecord[]> {\n if (scopeIds.length === 0) {\n return [];\n }\n\n const params: unknown[] = [...scopeIds, userId];\n let sql =\n `SELECT id, scope_id, user_id, team_id, permission, granted_by, granted_at ` +\n `FROM rbac_scope_access WHERE (scope_id IN (${createInClause(scopeIds.length)}) ` +\n `AND user_id = ?)`;\n\n if (teamIds.length > 0) {\n sql += ` OR (scope_id IN (${createInClause(scopeIds.length)}) AND team_id IN (${createInClause(teamIds.length)}))`;\n params.push(...scopeIds, ...teamIds);\n }\n\n const rows = await db.query<Record<string, unknown>>(sql, params);\n return rows.map(normalizeScopeAccessRecord);\n}\n\nasync function listDirectFlowAccessForIdentity(\n db: PluginDatabaseApi,\n flowId: string,\n userId: string,\n teamIds: string[],\n): Promise<FlowAccessRecord[]> {\n const params: unknown[] = [flowId, userId];\n let sql =\n 'SELECT id, flow_id, user_id, team_id, permission, granted_by, granted_at, expires_at ' +\n 'FROM flow_access WHERE (flow_id = ? AND user_id = ?)';\n\n if (teamIds.length > 0) {\n sql += ` OR (flow_id = ? AND team_id IN (${createInClause(teamIds.length)}))`;\n params.push(flowId, ...teamIds);\n }\n\n const rows = await db.query<Record<string, unknown>>(sql, params);\n const now = Date.now();\n return rows\n .map(normalizeFlowAccessRecord)\n .filter((record) => !record.expiresAt || new Date(record.expiresAt).getTime() > now);\n}\n\nasync function getEffectiveFlowAccessRecords(\n db: PluginDatabaseApi,\n flowId: string,\n userId: string,\n teamIds: string[],\n): Promise<EffectiveAccessRecord[]> {\n const directRecords = (\n await listDirectFlowAccessForIdentity(db, flowId, userId, teamIds)\n ).map<EffectiveAccessRecord>((record) => ({ ...record, source: 'direct' }));\n\n const scopeId = await getFlowScopeId(db, flowId);\n if (!scopeId) {\n return directRecords;\n }\n\n const ancestorIds = await getAncestorScopeIds(db, scopeId);\n const inheritedRows = await listScopeAccessForScopeIds(db, ancestorIds, userId, teamIds);\n if (inheritedRows.length === 0) {\n return directRecords;\n }\n\n const scopeRows = await db.query<{ id: string; name: string }>(\n `SELECT id, name FROM rbac_teams WHERE id IN (${createInClause(ancestorIds.length)})`,\n ancestorIds,\n );\n const scopeNames = new Map(scopeRows.map((row) => [row.id, row.name]));\n\n return [\n ...directRecords,\n ...inheritedRows.map<EffectiveAccessRecord>((record) => ({\n id: record.id,\n flowId,\n userId: record.userId,\n teamId: record.teamId,\n permission: record.permission,\n grantedBy: record.grantedBy,\n grantedAt: record.grantedAt,\n expiresAt: null,\n source: 'inherited',\n scopeId: record.scopeId,\n scopeName: scopeNames.get(record.scopeId) ?? null,\n })),\n ];\n}\n\nasync function getEffectiveFlowPermission(\n db: PluginDatabaseApi,\n flowId: string,\n identity: InvectIdentity,\n): Promise<FlowAccessPermission | null> {\n const records = await getEffectiveFlowAccessRecords(\n db,\n flowId,\n identity.id,\n identity.teamIds ?? [],\n );\n\n let highest: FlowAccessPermission | null = null;\n for (const record of records) {\n highest = getHigherPermission(highest, record.permission);\n }\n return highest;\n}\n\nasync function getCurrentUserAccessibleFlows(\n db: PluginDatabaseApi,\n identity: InvectIdentity,\n): Promise<{ flowIds: string[]; permissions: Record<string, FlowAccessPermission | null> }> {\n const rows = await db.query<{ id: string }>('SELECT id FROM flows');\n const permissions: Record<string, FlowAccessPermission | null> = {};\n\n await Promise.all(\n rows.map(async (row) => {\n permissions[row.id] = await getEffectiveFlowPermission(db, row.id, identity);\n }),\n );\n\n const flowIds = rows.map((row) => row.id).filter((flowId) => permissions[flowId]);\n return { flowIds, permissions };\n}\n\nasync function getScopePath(db: PluginDatabaseApi, scopeId: string | null): Promise<string[]> {\n if (!scopeId) {\n return [];\n }\n\n const rows = await db.query<{ id: string; name: string }>(\n `WITH RECURSIVE ancestors AS (\n SELECT id, name, parent_id, 0 AS depth FROM rbac_teams WHERE id = ?\n UNION ALL\n SELECT parent.id, parent.name, parent.parent_id, current.depth + 1\n FROM rbac_teams parent\n INNER JOIN ancestors current ON parent.id = current.parent_id\n )\n SELECT id, name FROM ancestors ORDER BY depth DESC`,\n [scopeId],\n );\n return rows.map((row) => row.name);\n}\n\nasync function listAllScopeAccessForScopeIds(\n db: PluginDatabaseApi,\n scopeIds: string[],\n): Promise<ScopeAccessRecord[]> {\n if (scopeIds.length === 0) {\n return [];\n }\n const rows = await db.query<Record<string, unknown>>(\n `SELECT id, scope_id, user_id, team_id, permission, granted_by, granted_at\n FROM rbac_scope_access\n WHERE scope_id IN (${createInClause(scopeIds.length)})`,\n scopeIds,\n );\n return rows.map(normalizeScopeAccessRecord);\n}\n\nasync function listAllDirectFlowAccess(\n db: PluginDatabaseApi,\n flowId: string,\n): Promise<FlowAccessRecord[]> {\n const rows = await db.query<Record<string, unknown>>(\n 'SELECT id, flow_id, user_id, team_id, permission, granted_by, granted_at, expires_at FROM flow_access WHERE flow_id = ?',\n [flowId],\n );\n const now = Date.now();\n return rows\n .map(normalizeFlowAccessRecord)\n .filter((record) => !record.expiresAt || new Date(record.expiresAt).getTime() > now);\n}\n\nasync function listAllEffectiveFlowAccessForPreview(\n db: PluginDatabaseApi,\n flowId: string,\n overrideScopeId?: string | null,\n): Promise<Array<FlowAccessRecord | ScopeAccessRecord>> {\n const direct = await listAllDirectFlowAccess(db, flowId);\n const scopeId =\n overrideScopeId === undefined ? await getFlowScopeId(db, flowId) : overrideScopeId;\n if (!scopeId) {\n return direct;\n }\n\n const ancestorIds = await getAncestorScopeIds(db, scopeId);\n const inherited = await listAllScopeAccessForScopeIds(db, ancestorIds);\n return [...direct, ...inherited];\n}\n\nasync function listAllEffectiveFlowAccessRecords(\n db: PluginDatabaseApi,\n flowId: string,\n): Promise<EffectiveAccessRecord[]> {\n const directRecords = (await listAllDirectFlowAccess(db, flowId)).map<EffectiveAccessRecord>(\n (record) => ({\n ...record,\n source: 'direct',\n }),\n );\n\n const scopeId = await getFlowScopeId(db, flowId);\n if (!scopeId) {\n return directRecords;\n }\n\n const ancestorIds = await getAncestorScopeIds(db, scopeId);\n const inheritedRows = await listAllScopeAccessForScopeIds(db, ancestorIds);\n if (inheritedRows.length === 0) {\n return directRecords;\n }\n\n const scopeRows = await db.query<{ id: string; name: string }>(\n `SELECT id, name FROM rbac_teams WHERE id IN (${createInClause(ancestorIds.length)})`,\n ancestorIds,\n );\n const scopeNames = new Map(scopeRows.map((row) => [row.id, row.name]));\n\n // For team-based inherited grants, expand into per-member user records so the\n // UI shows individual users rather than team entities.\n const teamIds = [\n ...new Set(inheritedRows.map((r) => r.teamId).filter((id): id is string => !!id)),\n ];\n const teamMembersByTeamId = new Map<string, string[]>();\n if (teamIds.length > 0) {\n const memberRows = await db.query<{ team_id: string; user_id: string }>(\n `SELECT team_id, user_id FROM rbac_team_members WHERE team_id IN (${createInClause(teamIds.length)})`,\n teamIds,\n );\n for (const row of memberRows) {\n const members = teamMembersByTeamId.get(row.team_id) ?? [];\n members.push(row.user_id);\n teamMembersByTeamId.set(row.team_id, members);\n }\n }\n\n const expandedInherited: EffectiveAccessRecord[] = [];\n for (const record of inheritedRows) {\n const scopeName = scopeNames.get(record.scopeId) ?? null;\n if (record.teamId) {\n // Expand team grant → one record per team member\n const members = teamMembersByTeamId.get(record.teamId) ?? [];\n for (const userId of members) {\n expandedInherited.push({\n id: `${record.id}:${userId}`,\n flowId,\n userId,\n teamId: null,\n permission: record.permission,\n grantedBy: record.grantedBy,\n grantedAt: record.grantedAt,\n expiresAt: null,\n source: 'inherited',\n scopeId: record.scopeId,\n scopeName,\n });\n }\n } else {\n expandedInherited.push({\n id: record.id,\n flowId,\n userId: record.userId,\n teamId: null,\n permission: record.permission,\n grantedBy: record.grantedBy,\n grantedAt: record.grantedAt,\n expiresAt: null,\n source: 'inherited',\n scopeId: record.scopeId,\n scopeName,\n });\n }\n }\n\n return [...directRecords, ...expandedInherited];\n}\n\nfunction buildAccessKey(record: { userId?: string | null; teamId?: string | null }): string {\n if (record.userId) {\n return `user:${record.userId}`;\n }\n if (record.teamId) {\n return `team:${record.teamId}`;\n }\n return 'unknown';\n}\n\nasync function resolveAccessChangeNames(\n db: PluginDatabaseApi,\n entries: Array<{\n userId?: string | null;\n teamId?: string | null;\n permission: FlowAccessPermission;\n source: string;\n }>,\n): Promise<MovePreviewAccessChange[]> {\n const userIds = Array.from(\n new Set(entries.map((entry) => entry.userId).filter(Boolean)),\n ) as string[];\n const teamIds = Array.from(\n new Set(entries.map((entry) => entry.teamId).filter(Boolean)),\n ) as string[];\n\n const [userRows, teamRows] = await Promise.all([\n userIds.length > 0\n ? db.query<{ id: string; name: string | null; email: string | null }>(\n `SELECT id, name, email FROM user WHERE id IN (${createInClause(userIds.length)})`,\n userIds,\n )\n : Promise.resolve([]),\n teamIds.length > 0\n ? db.query<{ id: string; name: string }>(\n `SELECT id, name FROM rbac_teams WHERE id IN (${createInClause(teamIds.length)})`,\n teamIds,\n )\n : Promise.resolve([]),\n ]);\n\n const userMap = new Map(userRows.map((row) => [row.id, row.name || row.email || row.id]));\n const teamMap = new Map(teamRows.map((row) => [row.id, row.name]));\n\n return entries.map((entry) => ({\n userId: entry.userId ?? undefined,\n teamId: entry.teamId ?? undefined,\n name: entry.userId\n ? (userMap.get(entry.userId) ?? entry.userId)\n : entry.teamId\n ? (teamMap.get(entry.teamId) ?? entry.teamId)\n : 'Unknown',\n permission: entry.permission,\n source: entry.source,\n }));\n}\n\n// ─────────────────────────────────────────────────────────────\n// Plugin factory\n// ─────────────────────────────────────────────────────────────\n\nexport function rbacPlugin(options: RbacPluginOptions = {}): InvectPlugin {\n const { useFlowAccessTable = true, adminPermission = 'flow:read', enableTeams = true } = options;\n\n // ── Plugin-owned tables ─────────────────────────────────────\n const teamsSchema: InvectPluginSchema = enableTeams\n ? {\n flows: {\n fields: {\n scope_id: {\n type: 'string',\n required: false,\n references: { table: 'rbac_teams', field: 'id', onDelete: 'set null' },\n index: true,\n },\n },\n },\n rbac_teams: {\n fields: {\n id: { type: 'string', primaryKey: true },\n name: { type: 'string', required: true },\n description: { type: 'text', required: false },\n parent_id: {\n type: 'string',\n required: false,\n references: { table: 'rbac_teams', field: 'id', onDelete: 'set null' },\n index: true,\n },\n created_by: {\n type: 'string',\n required: false,\n references: { table: 'user', field: 'id' },\n },\n created_at: { type: 'date', required: true, defaultValue: 'now()' },\n updated_at: { type: 'date', required: false },\n },\n },\n rbac_team_members: {\n fields: {\n id: { type: 'string', primaryKey: true },\n team_id: {\n type: 'string',\n required: true,\n references: { table: 'rbac_teams', field: 'id', onDelete: 'cascade' },\n index: true,\n },\n user_id: {\n type: 'string',\n required: true,\n references: { table: 'user', field: 'id', onDelete: 'cascade' },\n index: true,\n },\n created_at: { type: 'date', required: true, defaultValue: 'now()' },\n },\n },\n rbac_scope_access: {\n fields: {\n id: { type: 'string', primaryKey: true },\n scope_id: {\n type: 'string',\n required: true,\n references: { table: 'rbac_teams', field: 'id', onDelete: 'cascade' },\n index: true,\n },\n user_id: { type: 'string', required: false, index: true },\n team_id: { type: 'string', required: false, index: true },\n permission: {\n type: 'string',\n required: true,\n defaultValue: 'viewer',\n typeAnnotation: 'FlowAccessPermission',\n },\n granted_by: { type: 'string', required: false },\n granted_at: { type: 'date', required: true, defaultValue: 'now()' },\n },\n },\n }\n : {};\n\n // Mutable DB reference — captured from the first endpoint handler.\n // Used by the onRequest hook to resolve teamIds for the identity.\n let capturedDbApi: PluginDatabaseApi | null = null;\n\n // UI manifest — declares what the frontend should render.\n // Component IDs are resolved by the frontend plugin's component registry.\n const ui: PluginUIManifest = {\n sidebar: [\n {\n label: 'Access Control',\n icon: 'Shield',\n path: '/access',\n permission: adminPermission,\n },\n ...(enableTeams ? [] : []),\n ],\n pages: [\n {\n path: '/access',\n componentId: 'rbac.AccessControlPage',\n title: 'Access Control',\n },\n ...(enableTeams ? [] : []),\n ],\n panelTabs: [\n {\n context: 'flowEditor',\n label: 'Access',\n componentId: 'rbac.FlowAccessPanel',\n permission: 'flow:read',\n },\n ],\n headerActions: [\n {\n context: 'flowHeader',\n componentId: 'rbac.ShareButton',\n permission: 'flow:update',\n },\n ],\n };\n\n return {\n id: 'rbac',\n name: 'Role-Based Access Control',\n\n // Plugin-owned database tables\n schema: teamsSchema,\n\n // RBAC uses the core flow_access table (already checked by core),\n // but also depends on the auth plugin's tables for user identity.\n // Declaring them here ensures a clear error if the developer has the\n // RBAC plugin enabled but forgot the better-auth tables.\n requiredTables: [\n 'user',\n 'session',\n ...(enableTeams ? ['rbac_teams', 'rbac_team_members', 'rbac_scope_access'] : []),\n ],\n setupInstructions:\n 'The RBAC plugin requires better-auth tables (user, session). ' +\n 'Make sure @invect/user-auth is configured, then run ' +\n '`npx invect-cli generate` followed by `npx drizzle-kit push`.',\n\n // ─── Initialization ───────────────────────────────────────\n\n init: async (ctx: InvectPluginContext) => {\n // Verify that the auth plugin is loaded\n if (!ctx.hasPlugin('better-auth')) {\n ctx.logger.warn(\n 'RBAC plugin requires the @invect/user-auth plugin. ' +\n 'RBAC will work with reduced functionality (no session resolution). ' +\n 'Make sure userAuth() is registered before rbacPlugin().',\n );\n }\n\n ctx.logger.info('RBAC plugin initialized', {\n useFlowAccessTable,\n });\n },\n\n // ─── Plugin Endpoints ─────────────────────────────────────\n\n endpoints: [\n // ── Auth Info ──\n\n {\n method: 'GET',\n path: '/rbac/me',\n isPublic: false,\n handler: async (ctx) => {\n // Capture DB API on first endpoint call for the onRequest hook\n if (!capturedDbApi && enableTeams) {\n capturedDbApi = ctx.database;\n }\n\n const identity = ctx.identity;\n const permissions = ctx.core.getPermissions(identity);\n const resolvedRole = identity ? ctx.core.getResolvedRole(identity) : null;\n\n return {\n status: 200,\n body: {\n identity: identity\n ? {\n id: identity.id,\n name: identity.name,\n role: identity.role,\n resolvedRole,\n }\n : null,\n permissions,\n isAuthenticated: !!identity,\n },\n };\n },\n },\n\n {\n method: 'GET',\n path: '/rbac/roles',\n isPublic: false,\n permission: 'flow:read',\n handler: async (ctx) => {\n const roles = ctx.core.getAvailableRoles();\n return { status: 200, body: { roles } };\n },\n },\n\n // ── UI Manifest ──\n\n {\n method: 'GET',\n path: '/rbac/ui-manifest',\n isPublic: true,\n handler: async (_ctx) => {\n return {\n status: 200,\n body: {\n id: 'rbac',\n ...ui,\n },\n };\n },\n },\n\n // ── Flow Access Management ──\n\n {\n method: 'GET',\n path: '/rbac/flows/:flowId/access',\n permission: 'flow:read',\n handler: async (ctx) => {\n const flowId = ctx.params.flowId;\n if (!flowId) {\n return { status: 400, body: { error: 'Missing flowId parameter' } };\n }\n\n if (!ctx.core.isFlowAccessTableEnabled()) {\n return {\n status: 501,\n body: {\n error: 'Not Implemented',\n message:\n 'Flow access table not enabled. Set auth.useFlowAccessTable: true in config.',\n },\n };\n }\n\n if (!ctx.identity) {\n return {\n status: 401,\n body: { error: 'Unauthorized', message: 'Authentication required' },\n };\n }\n\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n const callerPermission = isAdmin\n ? 'owner'\n : await getEffectiveFlowPermission(ctx.database, flowId, ctx.identity);\n\n if (!callerPermission) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'No access to this flow' },\n };\n }\n\n const access = await ctx.core.listFlowAccess(flowId);\n return { status: 200, body: { access } };\n },\n },\n\n {\n method: 'POST',\n path: '/rbac/flows/:flowId/access',\n permission: 'flow:read',\n handler: async (ctx) => {\n const flowId = ctx.params.flowId;\n if (!flowId) {\n return { status: 400, body: { error: 'Missing flowId parameter' } };\n }\n\n if (!ctx.core.isFlowAccessTableEnabled()) {\n return {\n status: 501,\n body: {\n error: 'Not Implemented',\n message:\n 'Flow access table not enabled. Set auth.useFlowAccessTable: true in config.',\n },\n };\n }\n\n const { userId, teamId, permission, expiresAt } = ctx.body as {\n userId?: string;\n teamId?: string;\n permission?: string;\n expiresAt?: string;\n };\n\n if (!userId && !teamId) {\n return {\n status: 400,\n body: { error: 'Either userId or teamId must be provided' },\n };\n }\n\n if (!ctx.identity) {\n return {\n status: 401,\n body: { error: 'Unauthorized', message: 'Authentication required' },\n };\n }\n\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n const callerPermission = isAdmin\n ? 'owner'\n : await getEffectiveFlowPermission(ctx.database, flowId, ctx.identity);\n\n if (callerPermission !== 'owner') {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Owner access is required to manage sharing' },\n };\n }\n\n if (!permission || !['owner', 'editor', 'operator', 'viewer'].includes(permission)) {\n return {\n status: 400,\n body: { error: 'permission must be one of: owner, editor, operator, viewer' },\n };\n }\n\n const access = await ctx.core.grantFlowAccess({\n flowId,\n userId,\n teamId,\n permission: permission as 'owner' | 'editor' | 'operator' | 'viewer',\n grantedBy: ctx.identity?.id,\n expiresAt,\n });\n return { status: 201, body: access };\n },\n },\n\n {\n method: 'DELETE',\n path: '/rbac/flows/:flowId/access/:accessId',\n permission: 'flow:read',\n handler: async (ctx) => {\n const { flowId, accessId } = ctx.params;\n if (!flowId || !accessId) {\n return { status: 400, body: { error: 'Missing flowId or accessId parameter' } };\n }\n\n if (!ctx.core.isFlowAccessTableEnabled()) {\n return {\n status: 501,\n body: {\n error: 'Not Implemented',\n message:\n 'Flow access table not enabled. Set auth.useFlowAccessTable: true in config.',\n },\n };\n }\n\n if (!ctx.identity) {\n return {\n status: 401,\n body: { error: 'Unauthorized', message: 'Authentication required' },\n };\n }\n\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n const callerPermission = isAdmin\n ? 'owner'\n : await getEffectiveFlowPermission(ctx.database, flowId, ctx.identity);\n\n if (callerPermission !== 'owner') {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Owner access is required to manage sharing' },\n };\n }\n\n await ctx.core.revokeFlowAccess(accessId);\n return { status: 204, body: null };\n },\n },\n\n {\n method: 'GET',\n path: '/rbac/flows/accessible',\n isPublic: false,\n handler: async (ctx) => {\n if (!ctx.core.isFlowAccessTableEnabled()) {\n return {\n status: 501,\n body: {\n error: 'Not Implemented',\n message:\n 'Flow access table not enabled. Set auth.useFlowAccessTable: true in config.',\n },\n };\n }\n\n const identity = ctx.identity;\n if (!identity) {\n return {\n status: 401,\n body: { error: 'Unauthorized', message: 'Authentication required' },\n };\n }\n\n // Admins see all flows with implicit owner permission\n const isAdmin = ctx.core.getPermissions(identity).includes('admin:*');\n if (isAdmin) {\n return {\n status: 200,\n body: { flowIds: [], permissions: {}, isAdmin: true },\n };\n }\n\n const { flowIds, permissions } = await getCurrentUserAccessibleFlows(\n ctx.database,\n identity,\n );\n\n return {\n status: 200,\n body: {\n flowIds,\n permissions,\n isAdmin: false,\n },\n };\n },\n },\n\n {\n method: 'GET',\n path: '/rbac/flows/:flowId/effective-access',\n permission: 'flow:read',\n handler: async (ctx) => {\n const flowId = ctx.params.flowId;\n if (!flowId) {\n return { status: 400, body: { error: 'Missing flowId parameter' } };\n }\n if (!ctx.identity) {\n return {\n status: 401,\n body: { error: 'Unauthorized', message: 'Authentication required' },\n };\n }\n\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n const callerPermission = isAdmin\n ? 'owner'\n : await getEffectiveFlowPermission(ctx.database, flowId, ctx.identity);\n\n if (!callerPermission) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'No access to this flow' },\n };\n }\n\n const records = await listAllEffectiveFlowAccessRecords(ctx.database, flowId);\n\n return {\n status: 200,\n body: {\n flowId,\n scopeId: await getFlowScopeId(ctx.database, flowId),\n records,\n },\n };\n },\n },\n\n {\n method: 'PUT',\n path: '/rbac/flows/:flowId/scope',\n permission: 'flow:update',\n handler: async (ctx) => {\n const flowId = ctx.params.flowId;\n const { scopeId } = ctx.body as { scopeId?: string | null };\n if (!flowId) {\n return { status: 400, body: { error: 'Missing flowId parameter' } };\n }\n if (!ctx.identity) {\n return {\n status: 401,\n body: { error: 'Unauthorized', message: 'Authentication required' },\n };\n }\n\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n const callerPermission = isAdmin\n ? 'owner'\n : await getEffectiveFlowPermission(ctx.database, flowId, ctx.identity);\n if (callerPermission !== 'owner') {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Owner access is required to move flows' },\n };\n }\n\n if (scopeId) {\n const scopeRows = await ctx.database.query<{ id: string }>(\n 'SELECT id FROM rbac_teams WHERE id = ?',\n [scopeId],\n );\n if (scopeRows.length === 0) {\n return { status: 404, body: { error: 'Scope not found' } };\n }\n }\n\n await ctx.database.execute('UPDATE flows SET scope_id = ? WHERE id = ?', [\n scopeId ?? null,\n flowId,\n ]);\n return { status: 200, body: { success: true, flowId, scopeId: scopeId ?? null } };\n },\n },\n\n // ── Teams Management ──\n\n ...(enableTeams\n ? [\n {\n method: 'GET' as const,\n path: '/rbac/scopes/tree',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n\n const [teamRows, flowRows, memberRows, accessRows, teamRoleRows] =\n await Promise.all([\n ctx.database.query<{\n id: string;\n name: string;\n description: string | null;\n parent_id: string | null;\n created_by: string | null;\n created_at: string;\n updated_at: string | null;\n }>(\n 'SELECT id, name, description, parent_id, created_by, created_at, updated_at FROM rbac_teams ORDER BY name',\n ),\n ctx.database.query<{ id: string; name: string; scope_id: string | null }>(\n 'SELECT id, name, scope_id FROM flows ORDER BY name',\n ),\n ctx.database.query<{ team_id: string; member_count: number }>(\n 'SELECT team_id, COUNT(*) AS member_count FROM rbac_team_members GROUP BY team_id',\n ),\n ctx.database.query<{ scope_id: string; access_count: number }>(\n 'SELECT scope_id, COUNT(*) AS access_count FROM rbac_scope_access GROUP BY scope_id',\n ),\n ctx.database.query<{ scope_id: string; permission: FlowAccessPermission }>(\n 'SELECT scope_id, permission FROM rbac_scope_access WHERE team_id = scope_id AND team_id IS NOT NULL',\n ),\n ]);\n\n const memberCounts = new Map(\n memberRows.map((row) => [row.team_id, Number(row.member_count)]),\n );\n const accessCounts = new Map(\n accessRows.map((row) => [row.scope_id, Number(row.access_count)]),\n );\n const teamPermissions = new Map(\n teamRoleRows.map((row) => [row.scope_id, row.permission]),\n );\n\n const nodeMap = new Map<string, ScopeTreeNode>();\n for (const row of teamRows) {\n nodeMap.set(row.id, {\n ...normalizeTeamRow(row),\n children: [],\n flows: [],\n directAccessCount: accessCounts.get(row.id) ?? 0,\n memberCount: memberCounts.get(row.id) ?? 0,\n teamPermission: teamPermissions.get(row.id) ?? null,\n });\n }\n\n const roots: ScopeTreeNode[] = [];\n for (const node of nodeMap.values()) {\n if (node.parentId && nodeMap.has(node.parentId)) {\n nodeMap.get(node.parentId)?.children.push(node);\n } else {\n roots.push(node);\n }\n }\n\n const unscopedFlows: Array<{ id: string; name: string; scopeId: string | null }> =\n [];\n for (const flow of flowRows) {\n const mapped = { id: flow.id, name: flow.name, scopeId: flow.scope_id };\n if (flow.scope_id && nodeMap.has(flow.scope_id)) {\n nodeMap.get(flow.scope_id)?.flows.push(mapped);\n } else {\n unscopedFlows.push(mapped);\n }\n }\n\n return { status: 200, body: { scopes: roots, unscopedFlows } };\n },\n },\n\n {\n method: 'GET' as const,\n path: '/rbac/scopes/:scopeId/access',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n\n const rows = await ctx.database.query<Record<string, unknown>>(\n 'SELECT id, scope_id, user_id, team_id, permission, granted_by, granted_at FROM rbac_scope_access WHERE scope_id = ?',\n [ctx.params.scopeId],\n );\n return { status: 200, body: { access: rows.map(normalizeScopeAccessRecord) } };\n },\n },\n\n {\n method: 'POST' as const,\n path: '/rbac/scopes/:scopeId/access',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n const { scopeId } = ctx.params;\n const { userId, teamId, permission } = ctx.body as {\n userId?: string;\n teamId?: string;\n permission?: string;\n };\n\n if (!scopeId) {\n return { status: 400, body: { error: 'Missing scopeId parameter' } };\n }\n if (!userId && !teamId) {\n return {\n status: 400,\n body: { error: 'Either userId or teamId must be provided' },\n };\n }\n if (userId && teamId) {\n return {\n status: 400,\n body: { error: 'Provide either userId or teamId, not both' },\n };\n }\n if (teamId && teamId !== scopeId) {\n return {\n status: 400,\n body: { error: 'Teams can only hold a role on their own scope' },\n };\n }\n if (!isFlowAccessPermission(permission)) {\n return {\n status: 400,\n body: { error: 'permission must be one of: owner, editor, operator, viewer' },\n };\n }\n\n const existing = await ctx.database.query<{ id: string }>(\n 'SELECT id FROM rbac_scope_access WHERE scope_id = ? AND user_id IS ? AND team_id IS ?',\n [scopeId, userId ?? null, teamId ?? null],\n );\n\n const now = new Date().toISOString();\n if (existing[0]) {\n await ctx.database.execute(\n 'UPDATE rbac_scope_access SET permission = ?, granted_by = ?, granted_at = ? WHERE id = ?',\n [permission, ctx.identity.id, now, existing[0].id],\n );\n return {\n status: 200,\n body: {\n id: existing[0].id,\n scopeId,\n userId: userId ?? null,\n teamId: teamId ?? null,\n permission,\n grantedBy: ctx.identity.id,\n grantedAt: now,\n },\n };\n }\n\n const id = crypto.randomUUID();\n await ctx.database.execute(\n 'INSERT INTO rbac_scope_access (id, scope_id, user_id, team_id, permission, granted_by, granted_at) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [id, scopeId, userId ?? null, teamId ?? null, permission, ctx.identity.id, now],\n );\n\n return {\n status: 201,\n body: {\n id,\n scopeId,\n userId: userId ?? null,\n teamId: teamId ?? null,\n permission,\n grantedBy: ctx.identity.id,\n grantedAt: now,\n },\n };\n },\n },\n\n {\n method: 'DELETE' as const,\n path: '/rbac/scopes/:scopeId/access/:accessId',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n await ctx.database.execute('DELETE FROM rbac_scope_access WHERE id = ?', [\n ctx.params.accessId,\n ]);\n return { status: 204, body: null };\n },\n },\n\n {\n method: 'POST' as const,\n path: '/rbac/preview-move',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n const { type, id, targetScopeId } = ctx.body as {\n type?: 'flow' | 'scope';\n id?: string;\n targetScopeId?: string | null;\n };\n\n if (!type || !id) {\n return { status: 400, body: { error: 'type and id are required' } };\n }\n\n let affectedFlowIds: string[] = [];\n let itemName = id;\n if (type === 'flow') {\n const rows = await ctx.database.query<{ id: string; name: string }>(\n 'SELECT id, name FROM flows WHERE id = ?',\n [id],\n );\n if (!rows[0]) {\n return { status: 404, body: { error: 'Flow not found' } };\n }\n itemName = rows[0].name;\n affectedFlowIds = [id];\n } else {\n const scopeRows = await ctx.database.query<{ id: string; name: string }>(\n 'SELECT id, name FROM rbac_teams WHERE id = ?',\n [id],\n );\n if (!scopeRows[0]) {\n return { status: 404, body: { error: 'Scope not found' } };\n }\n itemName = scopeRows[0].name;\n const descendantScopeIds = await getDescendantScopeIds(ctx.database, id);\n if (targetScopeId && descendantScopeIds.includes(targetScopeId)) {\n return {\n status: 400,\n body: { error: 'Cannot move a scope into itself or its descendant' },\n };\n }\n const flowRows = await ctx.database.query<{ id: string }>(\n `SELECT id FROM flows WHERE scope_id IN (${createInClause(descendantScopeIds.length)})`,\n descendantScopeIds,\n );\n affectedFlowIds = flowRows.map((row) => row.id);\n }\n\n const targetPath = await getScopePath(ctx.database, targetScopeId ?? null);\n const targetAncestorIds = targetScopeId\n ? await getAncestorScopeIds(ctx.database, targetScopeId)\n : [];\n const targetScopeAccess = await listAllScopeAccessForScopeIds(\n ctx.database,\n targetAncestorIds,\n );\n\n const gainedEntries = new Map<\n string,\n {\n userId?: string | null;\n teamId?: string | null;\n permission: FlowAccessPermission;\n source: string;\n }\n >();\n let unchanged = 0;\n\n for (const flowId of affectedFlowIds) {\n const currentRecords = await listAllEffectiveFlowAccessForPreview(\n ctx.database,\n flowId,\n );\n const currentPermissions = new Map<string, FlowAccessPermission>();\n for (const record of currentRecords) {\n const permission = toFlowAccessPermission(record.permission);\n if (!permission) {\n continue;\n }\n const key = buildAccessKey(record);\n currentPermissions.set(\n key,\n getHigherPermission(currentPermissions.get(key) ?? null, permission) ??\n permission,\n );\n }\n\n for (const record of targetScopeAccess) {\n const key = buildAccessKey(record);\n const existingPermission = currentPermissions.get(key) ?? null;\n if (\n existingPermission &&\n FLOW_PERMISSION_LEVELS[existingPermission] >=\n FLOW_PERMISSION_LEVELS[record.permission]\n ) {\n unchanged += 1;\n continue;\n }\n gainedEntries.set(key, {\n userId: record.userId,\n teamId: record.teamId,\n permission: record.permission,\n source: `from ${targetPath.at(-1) ?? 'root'}`,\n });\n }\n }\n\n const gained = await resolveAccessChangeNames(\n ctx.database,\n Array.from(gainedEntries.values()),\n );\n\n return {\n status: 200,\n body: {\n item: { id, name: itemName, type },\n target: {\n id: targetScopeId ?? null,\n name: targetPath.at(-1) ?? 'Unscoped',\n path: targetPath,\n },\n affectedFlows: affectedFlowIds.length,\n accessChanges: {\n gained,\n unchanged,\n },\n },\n };\n },\n },\n\n // List all teams\n {\n method: 'GET' as const,\n path: '/rbac/teams',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n\n const rows = await ctx.database.query<{\n id: string;\n name: string;\n description: string | null;\n parent_id: string | null;\n created_by: string | null;\n created_at: string;\n updated_at: string | null;\n }>(\n 'SELECT id, name, description, parent_id, created_by, created_at, updated_at FROM rbac_teams ORDER BY name',\n );\n\n const teams = rows.map((r) => normalizeTeamRow(r));\n\n return { status: 200, body: { teams } };\n },\n },\n\n // Create a team (admin only)\n {\n method: 'POST' as const,\n path: '/rbac/teams',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n const { name, description, parentId } = ctx.body as {\n name?: string;\n description?: string;\n parentId?: string | null;\n };\n if (!name?.trim()) {\n return { status: 400, body: { error: 'Team name is required' } };\n }\n\n if (parentId) {\n const parentRows = await ctx.database.query<{ id: string }>(\n 'SELECT id FROM rbac_teams WHERE id = ?',\n [parentId],\n );\n if (parentRows.length === 0) {\n return { status: 404, body: { error: 'Parent scope not found' } };\n }\n }\n\n const id = crypto.randomUUID();\n const now = new Date().toISOString();\n await ctx.database.execute(\n 'INSERT INTO rbac_teams (id, name, description, parent_id, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)',\n [\n id,\n name.trim(),\n description?.trim() || null,\n parentId ?? null,\n ctx.identity.id,\n now,\n now,\n ],\n );\n\n return {\n status: 201,\n body: {\n id,\n name: name.trim(),\n description: description?.trim() || null,\n parentId: parentId ?? null,\n createdBy: ctx.identity.id,\n createdAt: now,\n updatedAt: now,\n },\n };\n },\n },\n\n // Update a team (admin only)\n {\n method: 'PUT' as const,\n path: '/rbac/teams/:teamId',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n const { teamId } = ctx.params;\n const { name, description, parentId } = ctx.body as {\n name?: string;\n description?: string;\n parentId?: string | null;\n };\n\n const existing = await ctx.database.query<{ id: string }>(\n 'SELECT id FROM rbac_teams WHERE id = ?',\n [teamId],\n );\n if (existing.length === 0) {\n return { status: 404, body: { error: 'Team not found' } };\n }\n\n if (parentId === teamId) {\n return { status: 400, body: { error: 'A scope cannot be its own parent' } };\n }\n if (parentId) {\n const parentRows = await ctx.database.query<{ id: string }>(\n 'SELECT id FROM rbac_teams WHERE id = ?',\n [parentId],\n );\n if (parentRows.length === 0) {\n return { status: 404, body: { error: 'Parent scope not found' } };\n }\n const descendantScopeIds = await getDescendantScopeIds(ctx.database, teamId);\n if (descendantScopeIds.includes(parentId)) {\n return {\n status: 400,\n body: { error: 'Cannot move a scope into itself or its descendant' },\n };\n }\n }\n\n const updates: string[] = [];\n const values: unknown[] = [];\n if (name !== undefined) {\n if (!name.trim()) {\n return { status: 400, body: { error: 'Team name cannot be empty' } };\n }\n updates.push('name = ?');\n values.push(name.trim());\n }\n if (description !== undefined) {\n updates.push('description = ?');\n values.push(description?.trim() || null);\n }\n if (parentId !== undefined) {\n updates.push('parent_id = ?');\n values.push(parentId ?? null);\n }\n if (updates.length === 0) {\n return { status: 400, body: { error: 'No fields to update' } };\n }\n\n updates.push('updated_at = ?');\n values.push(new Date().toISOString());\n values.push(teamId);\n\n await ctx.database.execute(\n `UPDATE rbac_teams SET ${updates.join(', ')} WHERE id = ?`,\n values,\n );\n\n return { status: 200, body: { success: true } };\n },\n },\n\n // Delete a team (admin only)\n {\n method: 'DELETE' as const,\n path: '/rbac/teams/:teamId',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n const { teamId } = ctx.params;\n const teams = await ctx.database.query<{ id: string; parent_id: string | null }>(\n 'SELECT id, parent_id FROM rbac_teams WHERE id = ?',\n [teamId],\n );\n if (teams.length === 0) {\n return { status: 404, body: { error: 'Team not found' } };\n }\n\n const parentId = teams[0].parent_id ?? null;\n await ctx.database.execute('UPDATE flows SET scope_id = ? WHERE scope_id = ?', [\n parentId,\n teamId,\n ]);\n\n // CASCADE on team_members handles cleanup\n await ctx.database.execute('DELETE FROM rbac_teams WHERE id = ?', [teamId]);\n return { status: 204, body: null };\n },\n },\n\n // Get team with members\n {\n method: 'GET' as const,\n path: '/rbac/teams/:teamId',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n\n const { teamId } = ctx.params;\n const teams = await ctx.database.query<{\n id: string;\n name: string;\n description: string | null;\n parent_id: string | null;\n created_by: string | null;\n created_at: string;\n updated_at: string | null;\n }>(\n 'SELECT id, name, description, parent_id, created_by, created_at, updated_at FROM rbac_teams WHERE id = ?',\n [teamId],\n );\n\n if (teams.length === 0) {\n return { status: 404, body: { error: 'Team not found' } };\n }\n\n const members = await ctx.database.query<{\n id: string;\n team_id: string;\n user_id: string;\n created_at: string;\n }>(\n 'SELECT id, team_id, user_id, created_at FROM rbac_team_members WHERE team_id = ?',\n [teamId],\n );\n\n const team = teams[0];\n return {\n status: 200,\n body: {\n ...normalizeTeamRow(team),\n members: members.map((m) => ({\n id: m.id,\n teamId: m.team_id,\n userId: m.user_id,\n createdAt: m.created_at,\n })),\n },\n };\n },\n },\n\n // Add member to team (admin only)\n {\n method: 'POST' as const,\n path: '/rbac/teams/:teamId/members',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n const { teamId } = ctx.params;\n const { userId } = ctx.body as { userId?: string };\n if (!userId?.trim()) {\n return { status: 400, body: { error: 'userId is required' } };\n }\n\n // Check team exists\n const teams = await ctx.database.query<{ id: string }>(\n 'SELECT id FROM rbac_teams WHERE id = ?',\n [teamId],\n );\n if (teams.length === 0) {\n return { status: 404, body: { error: 'Team not found' } };\n }\n\n // Check not already a member\n const existing = await ctx.database.query<{ id: string }>(\n 'SELECT id FROM rbac_team_members WHERE team_id = ? AND user_id = ?',\n [teamId, userId.trim()],\n );\n if (existing.length > 0) {\n return { status: 409, body: { error: 'User is already a member of this team' } };\n }\n\n const id = crypto.randomUUID();\n const now = new Date().toISOString();\n await ctx.database.execute(\n 'INSERT INTO rbac_team_members (id, team_id, user_id, created_at) VALUES (?, ?, ?, ?)',\n [id, teamId, userId.trim(), now],\n );\n\n return {\n status: 201,\n body: { id, teamId, userId: userId.trim(), createdAt: now },\n };\n },\n },\n\n // Remove member from team (admin only)\n {\n method: 'DELETE' as const,\n path: '/rbac/teams/:teamId/members/:userId',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n const isAdmin = ctx.core.getPermissions(ctx.identity).includes('admin:*');\n if (!isAdmin) {\n return {\n status: 403,\n body: { error: 'Forbidden', message: 'Admin access required' },\n };\n }\n\n const { teamId, userId } = ctx.params;\n await ctx.database.execute(\n 'DELETE FROM rbac_team_members WHERE team_id = ? AND user_id = ?',\n [teamId, userId],\n );\n return { status: 204, body: null };\n },\n },\n\n // Get teams for current user (for identity resolution)\n {\n method: 'GET' as const,\n path: '/rbac/my-teams',\n isPublic: false,\n handler: async (ctx: PluginEndpointContext) => {\n if (!ctx.identity) {\n return { status: 401, body: { error: 'Unauthorized' } };\n }\n\n const rows = await ctx.database.query<{\n id: string;\n name: string;\n description: string | null;\n parent_id: string | null;\n created_by: string | null;\n created_at: string;\n updated_at: string | null;\n }>(\n 'SELECT t.id, t.name, t.description, t.parent_id, t.created_by, t.created_at, t.updated_at ' +\n 'FROM rbac_teams t INNER JOIN rbac_team_members tm ON t.id = tm.team_id ' +\n 'WHERE tm.user_id = ? ORDER BY t.name',\n [ctx.identity.id],\n );\n\n const teams = rows.map((r) => normalizeTeamRow(r));\n\n return { status: 200, body: { teams } };\n },\n },\n ]\n : []),\n ],\n\n // ─── Lifecycle Hooks ──────────────────────────────────────\n\n hooks: {\n // Enrich identity with teamIds after auth plugin resolves the session.\n // This runs after the auth plugin's onRequest hook since RBAC is registered\n // after auth. We query rbac_team_members to get the user's team IDs.\n onRequest: enableTeams\n ? async (\n _request: Request,\n context: { path: string; method: string; identity: InvectIdentity | null },\n ) => {\n if (!context.identity || !capturedDbApi) {\n return;\n }\n // Skip if teamIds already populated (custom mapUser)\n if (context.identity.teamIds && context.identity.teamIds.length > 0) {\n return;\n }\n\n try {\n const rows = await capturedDbApi.query<{ team_id: string }>(\n 'SELECT team_id FROM rbac_team_members WHERE user_id = ?',\n [context.identity.id],\n );\n if (rows.length > 0) {\n context.identity = {\n ...context.identity,\n teamIds: rows.map((r) => r.team_id),\n };\n }\n } catch {\n // Silently ignore — table may not exist yet during initial setup\n }\n }\n : undefined,\n\n // Enforce flow-level ACLs on authorization checks\n onAuthorize: async (context) => {\n if (!useFlowAccessTable) {\n return; // Defer to default RBAC\n }\n\n const { identity, resource, action } = context;\n\n // Only enforce for flow-related resources with specific IDs\n if (!identity || !resource?.id) {\n return;\n }\n\n if (!FLOW_RESOURCE_TYPES.has(resource.type)) {\n return;\n }\n\n if (identity.permissions?.includes('admin:*') || identity.role === 'admin') {\n return { allowed: true };\n }\n\n const database = context.database ?? capturedDbApi;\n if (!database) {\n return;\n }\n\n const effectivePermission = await getEffectiveFlowPermission(\n database,\n resource.id,\n identity,\n );\n if (!effectivePermission) {\n return { allowed: false };\n }\n\n const requiredPermission = mapActionToRequiredPermission(action);\n return {\n allowed:\n FLOW_PERMISSION_LEVELS[effectivePermission] >=\n FLOW_PERMISSION_LEVELS[requiredPermission],\n };\n },\n\n // Auto-grant owner access when a flow is created\n afterFlowRun: async (_context) => {\n // This hook is for flow *runs*, not flow creation.\n // Flow creation auto-grant is handled by the core when\n // auth.useFlowAccessTable is true.\n },\n },\n\n // ─── Error Codes ──────────────────────────────────────────\n\n $ERROR_CODES: {\n 'rbac:no_access': {\n message: 'You do not have access to this flow.',\n status: 403,\n },\n 'rbac:insufficient_permission': {\n message: 'Your access level is insufficient for this operation.',\n status: 403,\n },\n 'rbac:auth_required': {\n message: 'Authentication is required. Please sign in.',\n status: 401,\n },\n 'rbac:plugin_missing': {\n message: 'The RBAC plugin requires the @invect/user-auth plugin.',\n status: 500,\n },\n },\n };\n}\n"],"mappings":"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"}
|
package/dist/backend/plugin.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ import type { InvectPlugin, InvectPermission, PluginDatabaseApi } from '@invect/
|
|
|
21
21
|
* ```ts
|
|
22
22
|
* import { resolveTeamIds } from '@invect/rbac/backend';
|
|
23
23
|
*
|
|
24
|
-
*
|
|
24
|
+
* userAuth({
|
|
25
25
|
* auth,
|
|
26
26
|
* mapUser: async (user, session) => ({
|
|
27
27
|
* id: user.id,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@invect/rbac",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "RBAC plugin for Invect — adds role-based access control UI, flow sharing, and permission management. Requires @invect/user-auth.",
|
|
5
5
|
"author": "robase",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,11 +29,11 @@
|
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@invect/core": "0.1.
|
|
32
|
+
"@invect/core": "0.1.2"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@invect/user-auth": ">=0.1.
|
|
36
|
-
"@invect/frontend": ">=0.1.
|
|
35
|
+
"@invect/user-auth": ">=0.1.1",
|
|
36
|
+
"@invect/frontend": ">=0.1.1",
|
|
37
37
|
"react": ">=18.0.0",
|
|
38
38
|
"react-dom": ">=18.0.0",
|
|
39
39
|
"@tanstack/react-query": ">=5.0.0",
|
|
@@ -77,8 +77,8 @@
|
|
|
77
77
|
"typescript": "^5.3.3",
|
|
78
78
|
"vitest": "^1.2.2",
|
|
79
79
|
"zustand": "^4.5.7",
|
|
80
|
-
"@invect/user-auth": "0.1.
|
|
81
|
-
"@invect/frontend": "0.1.
|
|
80
|
+
"@invect/user-auth": "0.1.1",
|
|
81
|
+
"@invect/frontend": "0.1.1"
|
|
82
82
|
},
|
|
83
83
|
"repository": {
|
|
84
84
|
"type": "git",
|