import type { AppContext } from "./app-context.js"; import { roles } from "@atbb/db"; import { and, eq } from "drizzle-orm"; interface DefaultRole { name: string; description: string; permissions: string[]; priority: number; critical: boolean; } const DEFAULT_ROLES: DefaultRole[] = [ { name: "Owner", description: "Forum owner with full control", permissions: ["*"], priority: 0, critical: true, }, { name: "Admin", description: "Can manage forum structure and users", permissions: [ "space.atbb.permission.manageCategories", "space.atbb.permission.manageRoles", "space.atbb.permission.manageMembers", "space.atbb.permission.moderatePosts", "space.atbb.permission.banUsers", "space.atbb.permission.pinTopics", "space.atbb.permission.lockTopics", "space.atbb.permission.createTopics", "space.atbb.permission.createPosts", ], priority: 10, critical: true, }, { name: "Moderator", description: "Can moderate content and users", permissions: [ "space.atbb.permission.moderatePosts", "space.atbb.permission.banUsers", "space.atbb.permission.pinTopics", "space.atbb.permission.lockTopics", "space.atbb.permission.createTopics", "space.atbb.permission.createPosts", ], priority: 20, critical: true, }, { name: "Member", description: "Regular forum member", permissions: [ "space.atbb.permission.createTopics", "space.atbb.permission.createPosts", ], priority: 30, critical: true, }, ]; /** * Seed default roles to Forum DID's PDS. * * Idempotent: Checks for existing roles by name before creating. * Safe to run on every startup. * * @throws Error if ForumAgent is unavailable or if any critical role fails to seed */ export async function seedDefaultRoles(ctx: AppContext): Promise<{ created: number; skipped: number }> { // Check ForumAgent availability if (!ctx.forumAgent) { throw new Error("ForumAgent not available - permission system cannot function without roles. Check FORUM_HANDLE and FORUM_PASSWORD environment variables."); } const agent = ctx.forumAgent.getAgent(); if (!agent) { throw new Error("ForumAgent not authenticated - permission system cannot function without roles. Check FORUM_HANDLE and FORUM_PASSWORD are valid."); } let created = 0; let skipped = 0; for (const defaultRole of DEFAULT_ROLES) { try { // Check if role already exists by name const [existingRole] = await ctx.db .select() .from(roles) .where(and(eq(roles.did, ctx.config.forumDid), eq(roles.name, defaultRole.name))) .limit(1); if (existingRole) { ctx.logger.info(`Role "${defaultRole.name}" already exists, skipping`, { operation: "seedDefaultRoles", roleName: defaultRole.name, }); skipped++; continue; } // Create role record on Forum DID's PDS const response = await agent.com.atproto.repo.createRecord({ repo: ctx.config.forumDid, collection: "space.atbb.forum.role", record: { $type: "space.atbb.forum.role", name: defaultRole.name, description: defaultRole.description, permissions: defaultRole.permissions, priority: defaultRole.priority, createdAt: new Date().toISOString(), }, }); ctx.logger.info(`Created default role "${defaultRole.name}"`, { operation: "seedDefaultRoles", roleName: defaultRole.name, uri: response.data.uri, cid: response.data.cid, }); created++; } catch (error) { ctx.logger.error(`Failed to seed role "${defaultRole.name}"`, { operation: "seedDefaultRoles", roleName: defaultRole.name, error: error instanceof Error ? error.message : String(error), }); if (defaultRole.critical) { throw new Error( `Failed to seed critical role "${defaultRole.name}": ${error instanceof Error ? error.message : String(error)}` ); } } } return { created, skipped }; }