diff --git a/packages/server/rpc/engine/grant.ts b/packages/server/rpc/engine/grant.ts new file mode 100644 index 0000000..4d771ce --- /dev/null +++ b/packages/server/rpc/engine/grant.ts @@ -0,0 +1,174 @@ +/** + * Engine RPC grant methods. + * + * Implements: + * - grant.create: Grant tree access to a user + * - grant.list: List grants (optionally filter by user) + * - grant.get: Get a specific grant + * - grant.revoke: Revoke tree access + * - grant.check: Check if user has access to a tree path for an action + */ +import type { TreeGrant } from "@memory-engine/engine"; +import { AppError } from "../errors"; +import { buildRegistry } from "../registry"; +import type { HandlerContext } from "../types"; +import { + type GrantCheckParams, + type GrantCreateParams, + type GrantGetParams, + type GrantListParams, + type GrantRevokeParams, + grantCheckSchema, + grantCreateSchema, + grantGetSchema, + grantListSchema, + grantRevokeSchema, +} from "./schemas"; +import { assertEngineContext, type EngineContext } from "./types"; + +// ============================================================================= +// Response Types +// ============================================================================= + +/** + * Grant response (serializable). + */ +interface GrantResponse { + id: string; + userId: string; + treePath: string; + actions: string[]; + grantedBy: string | null; + withGrantOption: boolean; + createdAt: string; +} + +/** + * Convert a TreeGrant to a serializable response. + */ +function toGrantResponse(grant: TreeGrant): GrantResponse { + return { + id: grant.id, + userId: grant.userId, + treePath: grant.treePath, + actions: grant.actions, + grantedBy: grant.grantedBy, + withGrantOption: grant.withGrantOption, + createdAt: grant.createdAt.toISOString(), + }; +} + +// ============================================================================= +// Method Handlers +// ============================================================================= + +/** + * grant.create - Grant tree access to a user. + */ +async function grantCreate( + params: GrantCreateParams, + context: HandlerContext, +): Promise<{ created: boolean }> { + assertEngineContext(context); + const { db, userId } = context as EngineContext; + + await db.grantTreeAccess({ + userId: params.userId, + treePath: params.treePath, + actions: params.actions, + grantedBy: userId, + withGrantOption: params.withGrantOption, + }); + + return { created: true }; +} + +/** + * grant.list - List grants. + */ +async function grantList( + params: GrantListParams, + context: HandlerContext, +): Promise<{ grants: GrantResponse[] }> { + assertEngineContext(context); + const { db } = context as EngineContext; + + const grants = await db.listTreeGrants(params.userId); + return { grants: grants.map(toGrantResponse) }; +} + +/** + * grant.get - Get a specific grant. + */ +async function grantGet( + params: GrantGetParams, + context: HandlerContext, +): Promise { + assertEngineContext(context); + const { db } = context as EngineContext; + + const grant = await db.getTreeGrant(params.userId, params.treePath); + if (!grant) { + throw new AppError( + "NOT_FOUND", + `Grant not found for user ${params.userId} at path ${params.treePath}`, + ); + } + + return toGrantResponse(grant); +} + +/** + * grant.revoke - Revoke tree access. + */ +async function grantRevoke( + params: GrantRevokeParams, + context: HandlerContext, +): Promise<{ revoked: boolean }> { + assertEngineContext(context); + const { db } = context as EngineContext; + + const revoked = await db.revokeTreeAccess(params.userId, params.treePath); + if (!revoked) { + throw new AppError( + "NOT_FOUND", + `Grant not found for user ${params.userId} at path ${params.treePath}`, + ); + } + + return { revoked }; +} + +/** + * grant.check - Check if user has access to a tree path for an action. + */ +async function grantCheck( + params: GrantCheckParams, + context: HandlerContext, +): Promise<{ allowed: boolean }> { + assertEngineContext(context); + const { db } = context as EngineContext; + + const allowed = await db.checkTreeAccess( + params.userId, + params.treePath, + params.action, + ); + + return { allowed }; +} + +// ============================================================================= +// Registry +// ============================================================================= + +/** + * Build the grant methods registry. + */ +export const grantMethods = buildRegistry() + .register("grant.create", grantCreateSchema, grantCreate) + .register("grant.list", grantListSchema, grantList) + .register("grant.get", grantGetSchema, grantGet) + .register("grant.revoke", grantRevokeSchema, grantRevoke) + .register("grant.check", grantCheckSchema, grantCheck) + .build(); diff --git a/packages/server/rpc/engine/index.ts b/packages/server/rpc/engine/index.ts index 1881ba1..0654928 100644 --- a/packages/server/rpc/engine/index.ts +++ b/packages/server/rpc/engine/index.ts @@ -1,22 +1,30 @@ import { buildRegistry } from "../registry"; +import { grantMethods } from "./grant"; import { memoryMethods } from "./memory"; +import { roleMethods } from "./role"; +import { userMethods } from "./user"; /** * Engine RPC method registry. * - * Chunk 3 (current) - Memory methods: + * Memory methods (chunk 3): * - memory.create, memory.batchCreate, memory.get, memory.update, memory.delete * - memory.search, memory.tree, memory.move, memory.deleteTree * - * Chunk 4 - User, grant, role methods: - * - user.create, user.get, user.list, user.update, user.delete - * - grant.create, grant.list, grant.revoke - * - role.create, role.addMember, role.removeMember, role.listMembers + * User, grant, role methods (chunk 4): + * - user.create, user.get, user.getByName, user.list, user.rename, user.delete + * - grant.create, grant.list, grant.get, grant.revoke, grant.check + * - role.create, role.addMember, role.removeMember, role.listMembers, role.listForUser * - * Chunk 5 - API key methods: + * API key methods (chunk 5): * - apiKey.create, apiKey.list, apiKey.revoke */ -export const engineMethods = buildRegistry().merge(memoryMethods).build(); +export const engineMethods = buildRegistry() + .merge(memoryMethods) + .merge(userMethods) + .merge(grantMethods) + .merge(roleMethods) + .build(); // Re-export types for consumers export type { EngineContext } from "./types"; diff --git a/packages/server/rpc/engine/role.ts b/packages/server/rpc/engine/role.ts new file mode 100644 index 0000000..cccbda7 --- /dev/null +++ b/packages/server/rpc/engine/role.ts @@ -0,0 +1,208 @@ +/** + * Engine RPC role methods. + * + * Implements: + * - role.create: Create a role (user with canLogin=false) + * - role.addMember: Add a member to a role + * - role.removeMember: Remove a member from a role + * - role.listMembers: List members of a role + * - role.listForUser: List roles a user belongs to + */ +import type { RoleInfo, RoleMember, User } from "@memory-engine/engine"; +import { AppError } from "../errors"; +import { buildRegistry } from "../registry"; +import type { HandlerContext } from "../types"; +import { + type RoleAddMemberParams, + type RoleCreateParams, + type RoleListForUserParams, + type RoleListMembersParams, + type RoleRemoveMemberParams, + roleAddMemberSchema, + roleCreateSchema, + roleListForUserSchema, + roleListMembersSchema, + roleRemoveMemberSchema, +} from "./schemas"; +import { assertEngineContext, type EngineContext } from "./types"; + +// ============================================================================= +// Response Types +// ============================================================================= + +/** + * Role response (a user with canLogin=false). + */ +interface RoleResponse { + id: string; + name: string; + ownedBy: string | null; + createdAt: string; + updatedAt: string | null; +} + +/** + * Convert a User (role) to a serializable response. + */ +function toRoleResponse(user: User): RoleResponse { + return { + id: user.id, + name: user.name, + ownedBy: user.ownedBy, + createdAt: user.createdAt.toISOString(), + updatedAt: user.updatedAt?.toISOString() ?? null, + }; +} + +/** + * Role member response. + */ +interface RoleMemberResponse { + roleId: string; + memberId: string; + withAdminOption: boolean; + createdAt: string; +} + +/** + * Convert a RoleMember to a serializable response. + */ +function toRoleMemberResponse(member: RoleMember): RoleMemberResponse { + return { + roleId: member.roleId, + memberId: member.memberId, + withAdminOption: member.withAdminOption, + createdAt: member.createdAt.toISOString(), + }; +} + +/** + * Role info response (for listForUser). + */ +interface RoleInfoResponse { + id: string; + name: string; + withAdminOption: boolean; +} + +/** + * Convert a RoleInfo to a serializable response. + */ +function toRoleInfoResponse(info: RoleInfo): RoleInfoResponse { + return { + id: info.id, + name: info.name, + withAdminOption: info.withAdminOption, + }; +} + +// ============================================================================= +// Method Handlers +// ============================================================================= + +/** + * role.create - Create a role (user with canLogin=false). + */ +async function roleCreate( + params: RoleCreateParams, + context: HandlerContext, +): Promise { + assertEngineContext(context); + const { db } = context as EngineContext; + + const role = await db.createRole(params.name, params.ownedBy); + return toRoleResponse(role); +} + +/** + * role.addMember - Add a member to a role. + */ +async function roleAddMember( + params: RoleAddMemberParams, + context: HandlerContext, +): Promise<{ added: boolean }> { + assertEngineContext(context); + const { db } = context as EngineContext; + + try { + await db.addRoleMember( + params.roleId, + params.memberId, + params.withAdminOption, + ); + return { added: true }; + } catch (error) { + // Check for cycle error + if ( + error instanceof Error && + error.message.includes("would create a cycle") + ) { + throw new AppError("VALIDATION_ERROR", error.message); + } + throw error; + } +} + +/** + * role.removeMember - Remove a member from a role. + */ +async function roleRemoveMember( + params: RoleRemoveMemberParams, + context: HandlerContext, +): Promise<{ removed: boolean }> { + assertEngineContext(context); + const { db } = context as EngineContext; + + const removed = await db.removeRoleMember(params.roleId, params.memberId); + if (!removed) { + throw new AppError( + "NOT_FOUND", + `Membership not found for role ${params.roleId} and member ${params.memberId}`, + ); + } + + return { removed }; +} + +/** + * role.listMembers - List members of a role. + */ +async function roleListMembers( + params: RoleListMembersParams, + context: HandlerContext, +): Promise<{ members: RoleMemberResponse[] }> { + assertEngineContext(context); + const { db } = context as EngineContext; + + const members = await db.listRoleMembers(params.roleId); + return { members: members.map(toRoleMemberResponse) }; +} + +/** + * role.listForUser - List roles a user belongs to. + */ +async function roleListForUser( + params: RoleListForUserParams, + context: HandlerContext, +): Promise<{ roles: RoleInfoResponse[] }> { + assertEngineContext(context); + const { db } = context as EngineContext; + + const roles = await db.listRolesForUser(params.userId); + return { roles: roles.map(toRoleInfoResponse) }; +} + +// ============================================================================= +// Registry +// ============================================================================= + +/** + * Build the role methods registry. + */ +export const roleMethods = buildRegistry() + .register("role.create", roleCreateSchema, roleCreate) + .register("role.addMember", roleAddMemberSchema, roleAddMember) + .register("role.removeMember", roleRemoveMemberSchema, roleRemoveMember) + .register("role.listMembers", roleListMembersSchema, roleListMembers) + .register("role.listForUser", roleListForUserSchema, roleListForUser) + .build(); diff --git a/packages/server/rpc/engine/schemas.test.ts b/packages/server/rpc/engine/schemas.test.ts index 7c8899f..e026e92 100644 --- a/packages/server/rpc/engine/schemas.test.ts +++ b/packages/server/rpc/engine/schemas.test.ts @@ -3,6 +3,10 @@ */ import { describe, expect, test } from "bun:test"; import { + grantCheckSchema, + grantCreateSchema, + grantListSchema, + grantRevokeSchema, memoryBatchCreateSchema, memoryCreateSchema, memoryDeleteSchema, @@ -12,7 +16,16 @@ import { memorySearchSchema, memoryTreeSchema, memoryUpdateSchema, + roleAddMemberSchema, + roleCreateSchema, + roleListForUserSchema, + roleListMembersSchema, + roleRemoveMemberSchema, treePathSchema, + userCreateSchema, + userGetSchema, + userListSchema, + userRenameSchema, uuidv7Schema, } from "./schemas"; @@ -388,3 +401,238 @@ describe("memoryDeleteTreeSchema", () => { expect(result.success).toBe(false); }); }); + +// ============================================================================= +// User Schema Tests +// ============================================================================= + +describe("userCreateSchema", () => { + test("accepts minimal params", () => { + const result = userCreateSchema.safeParse({ + name: "alice", + }); + expect(result.success).toBe(true); + }); + + test("accepts full params", () => { + const result = userCreateSchema.safeParse({ + id: "019d694f-79f6-7595-8faf-b70b01c11f98", + name: "alice", + ownedBy: "019d694f-79f6-7595-8faf-b70b01c11f99", + canLogin: true, + superuser: false, + createrole: true, + }); + expect(result.success).toBe(true); + }); + + test("rejects empty name", () => { + const result = userCreateSchema.safeParse({ + name: "", + }); + expect(result.success).toBe(false); + }); +}); + +describe("userGetSchema", () => { + test("accepts valid UUID", () => { + const result = userGetSchema.safeParse({ + id: "019d694f-79f6-7595-8faf-b70b01c11f98", + }); + expect(result.success).toBe(true); + }); +}); + +describe("userListSchema", () => { + test("accepts empty params", () => { + const result = userListSchema.safeParse({}); + expect(result.success).toBe(true); + }); + + test("accepts canLogin filter", () => { + const result = userListSchema.safeParse({ + canLogin: false, + }); + expect(result.success).toBe(true); + }); +}); + +describe("userRenameSchema", () => { + test("accepts valid params", () => { + const result = userRenameSchema.safeParse({ + id: "019d694f-79f6-7595-8faf-b70b01c11f98", + name: "new-name", + }); + expect(result.success).toBe(true); + }); + + test("rejects empty name", () => { + const result = userRenameSchema.safeParse({ + id: "019d694f-79f6-7595-8faf-b70b01c11f98", + name: "", + }); + expect(result.success).toBe(false); + }); +}); + +// ============================================================================= +// Grant Schema Tests +// ============================================================================= + +describe("grantCreateSchema", () => { + test("accepts valid params", () => { + const result = grantCreateSchema.safeParse({ + userId: "019d694f-79f6-7595-8faf-b70b01c11f98", + treePath: "work.projects", + actions: ["read", "write"], + }); + expect(result.success).toBe(true); + }); + + test("accepts with grant option", () => { + const result = grantCreateSchema.safeParse({ + userId: "019d694f-79f6-7595-8faf-b70b01c11f98", + treePath: "work", + actions: ["admin"], + withGrantOption: true, + }); + expect(result.success).toBe(true); + }); + + test("rejects empty actions", () => { + const result = grantCreateSchema.safeParse({ + userId: "019d694f-79f6-7595-8faf-b70b01c11f98", + treePath: "work", + actions: [], + }); + expect(result.success).toBe(false); + }); + + test("rejects invalid action", () => { + const result = grantCreateSchema.safeParse({ + userId: "019d694f-79f6-7595-8faf-b70b01c11f98", + treePath: "work", + actions: ["read", "invalid"], + }); + expect(result.success).toBe(false); + }); +}); + +describe("grantListSchema", () => { + test("accepts empty params", () => { + const result = grantListSchema.safeParse({}); + expect(result.success).toBe(true); + }); + + test("accepts userId filter", () => { + const result = grantListSchema.safeParse({ + userId: "019d694f-79f6-7595-8faf-b70b01c11f98", + }); + expect(result.success).toBe(true); + }); +}); + +describe("grantRevokeSchema", () => { + test("accepts valid params", () => { + const result = grantRevokeSchema.safeParse({ + userId: "019d694f-79f6-7595-8faf-b70b01c11f98", + treePath: "work.projects", + }); + expect(result.success).toBe(true); + }); +}); + +describe("grantCheckSchema", () => { + test("accepts valid params", () => { + const result = grantCheckSchema.safeParse({ + userId: "019d694f-79f6-7595-8faf-b70b01c11f98", + treePath: "work.projects.api", + action: "read", + }); + expect(result.success).toBe(true); + }); + + test("rejects invalid action", () => { + const result = grantCheckSchema.safeParse({ + userId: "019d694f-79f6-7595-8faf-b70b01c11f98", + treePath: "work", + action: "execute", + }); + expect(result.success).toBe(false); + }); +}); + +// ============================================================================= +// Role Schema Tests +// ============================================================================= + +describe("roleCreateSchema", () => { + test("accepts minimal params", () => { + const result = roleCreateSchema.safeParse({ + name: "editors", + }); + expect(result.success).toBe(true); + }); + + test("accepts with ownedBy", () => { + const result = roleCreateSchema.safeParse({ + name: "editors", + ownedBy: "019d694f-79f6-7595-8faf-b70b01c11f98", + }); + expect(result.success).toBe(true); + }); + + test("rejects empty name", () => { + const result = roleCreateSchema.safeParse({ + name: "", + }); + expect(result.success).toBe(false); + }); +}); + +describe("roleAddMemberSchema", () => { + test("accepts valid params", () => { + const result = roleAddMemberSchema.safeParse({ + roleId: "019d694f-79f6-7595-8faf-b70b01c11f98", + memberId: "019d694f-79f6-7595-8faf-b70b01c11f99", + }); + expect(result.success).toBe(true); + }); + + test("accepts with admin option", () => { + const result = roleAddMemberSchema.safeParse({ + roleId: "019d694f-79f6-7595-8faf-b70b01c11f98", + memberId: "019d694f-79f6-7595-8faf-b70b01c11f99", + withAdminOption: true, + }); + expect(result.success).toBe(true); + }); +}); + +describe("roleRemoveMemberSchema", () => { + test("accepts valid params", () => { + const result = roleRemoveMemberSchema.safeParse({ + roleId: "019d694f-79f6-7595-8faf-b70b01c11f98", + memberId: "019d694f-79f6-7595-8faf-b70b01c11f99", + }); + expect(result.success).toBe(true); + }); +}); + +describe("roleListMembersSchema", () => { + test("accepts valid params", () => { + const result = roleListMembersSchema.safeParse({ + roleId: "019d694f-79f6-7595-8faf-b70b01c11f98", + }); + expect(result.success).toBe(true); + }); +}); + +describe("roleListForUserSchema", () => { + test("accepts valid params", () => { + const result = roleListForUserSchema.safeParse({ + userId: "019d694f-79f6-7595-8faf-b70b01c11f98", + }); + expect(result.success).toBe(true); + }); +}); diff --git a/packages/server/rpc/engine/schemas.ts b/packages/server/rpc/engine/schemas.ts index d4f09bd..2c182c2 100644 --- a/packages/server/rpc/engine/schemas.ts +++ b/packages/server/rpc/engine/schemas.ts @@ -196,3 +196,182 @@ export const memoryDeleteTreeSchema = z.object({ }); export type MemoryDeleteTreeParams = z.infer; + +// ============================================================================= +// User Method Schemas +// ============================================================================= + +/** + * user.create params. + */ +export const userCreateSchema = z.object({ + id: uuidv7Schema.optional().nullable(), + name: z.string().min(1, "name is required"), + ownedBy: uuidv7Schema.optional().nullable(), + canLogin: z.boolean().optional(), + superuser: z.boolean().optional(), + createrole: z.boolean().optional(), +}); + +export type UserCreateParams = z.infer; + +/** + * user.get params. + */ +export const userGetSchema = z.object({ + id: uuidv7Schema, +}); + +export type UserGetParams = z.infer; + +/** + * user.getByName params. + */ +export const userGetByNameSchema = z.object({ + name: z.string().min(1), +}); + +export type UserGetByNameParams = z.infer; + +/** + * user.list params. + */ +export const userListSchema = z.object({ + canLogin: z.boolean().optional(), +}); + +export type UserListParams = z.infer; + +/** + * user.rename params. + */ +export const userRenameSchema = z.object({ + id: uuidv7Schema, + name: z.string().min(1, "name is required"), +}); + +export type UserRenameParams = z.infer; + +/** + * user.delete params. + */ +export const userDeleteSchema = z.object({ + id: uuidv7Schema, +}); + +export type UserDeleteParams = z.infer; + +// ============================================================================= +// Grant Method Schemas +// ============================================================================= + +/** + * Valid actions for tree grants. + */ +export const grantActionSchema = z.enum(["read", "write", "delete", "admin"]); + +/** + * grant.create params. + */ +export const grantCreateSchema = z.object({ + userId: uuidv7Schema, + treePath: treePathSchema, + actions: z.array(grantActionSchema).min(1, "at least one action required"), + withGrantOption: z.boolean().optional(), +}); + +export type GrantCreateParams = z.infer; + +/** + * grant.list params. + */ +export const grantListSchema = z.object({ + userId: uuidv7Schema.optional(), +}); + +export type GrantListParams = z.infer; + +/** + * grant.get params. + */ +export const grantGetSchema = z.object({ + userId: uuidv7Schema, + treePath: treePathSchema, +}); + +export type GrantGetParams = z.infer; + +/** + * grant.revoke params. + */ +export const grantRevokeSchema = z.object({ + userId: uuidv7Schema, + treePath: treePathSchema, +}); + +export type GrantRevokeParams = z.infer; + +/** + * grant.check params. + */ +export const grantCheckSchema = z.object({ + userId: uuidv7Schema, + treePath: treePathSchema, + action: grantActionSchema, +}); + +export type GrantCheckParams = z.infer; + +// ============================================================================= +// Role Method Schemas +// ============================================================================= + +/** + * role.create params. + * Creates a user with canLogin=false (a role for grouping grants). + */ +export const roleCreateSchema = z.object({ + name: z.string().min(1, "name is required"), + ownedBy: uuidv7Schema.optional().nullable(), +}); + +export type RoleCreateParams = z.infer; + +/** + * role.addMember params. + */ +export const roleAddMemberSchema = z.object({ + roleId: uuidv7Schema, + memberId: uuidv7Schema, + withAdminOption: z.boolean().optional(), +}); + +export type RoleAddMemberParams = z.infer; + +/** + * role.removeMember params. + */ +export const roleRemoveMemberSchema = z.object({ + roleId: uuidv7Schema, + memberId: uuidv7Schema, +}); + +export type RoleRemoveMemberParams = z.infer; + +/** + * role.listMembers params. + */ +export const roleListMembersSchema = z.object({ + roleId: uuidv7Schema, +}); + +export type RoleListMembersParams = z.infer; + +/** + * role.listForUser params. + */ +export const roleListForUserSchema = z.object({ + userId: uuidv7Schema, +}); + +export type RoleListForUserParams = z.infer; diff --git a/packages/server/rpc/engine/user.ts b/packages/server/rpc/engine/user.ts new file mode 100644 index 0000000..885eeef --- /dev/null +++ b/packages/server/rpc/engine/user.ts @@ -0,0 +1,192 @@ +/** + * Engine RPC user methods. + * + * Implements: + * - user.create: Create a new user + * - user.get: Get user by ID + * - user.getByName: Get user by name + * - user.list: List users (optionally filter by canLogin) + * - user.rename: Rename a user + * - user.delete: Delete a user + */ +import type { User } from "@memory-engine/engine"; +import { AppError } from "../errors"; +import { buildRegistry } from "../registry"; +import type { HandlerContext } from "../types"; +import { + type UserCreateParams, + type UserDeleteParams, + type UserGetByNameParams, + type UserGetParams, + type UserListParams, + type UserRenameParams, + userCreateSchema, + userDeleteSchema, + userGetByNameSchema, + userGetSchema, + userListSchema, + userRenameSchema, +} from "./schemas"; +import { assertEngineContext, type EngineContext } from "./types"; + +// ============================================================================= +// Response Types +// ============================================================================= + +/** + * User response (serializable). + */ +interface UserResponse { + id: string; + name: string; + ownedBy: string | null; + canLogin: boolean; + superuser: boolean; + createrole: boolean; + createdAt: string; + updatedAt: string | null; +} + +/** + * Convert a User to a serializable response. + */ +function toUserResponse(user: User): UserResponse { + return { + id: user.id, + name: user.name, + ownedBy: user.ownedBy, + canLogin: user.canLogin, + superuser: user.superuser, + createrole: user.createrole, + createdAt: user.createdAt.toISOString(), + updatedAt: user.updatedAt?.toISOString() ?? null, + }; +} + +// ============================================================================= +// Method Handlers +// ============================================================================= + +/** + * user.create - Create a new user. + */ +async function userCreate( + params: UserCreateParams, + context: HandlerContext, +): Promise { + assertEngineContext(context); + const { db } = context as EngineContext; + + const user = await db.createUser({ + id: params.id ?? undefined, + name: params.name, + ownedBy: params.ownedBy ?? undefined, + canLogin: params.canLogin, + superuser: params.superuser, + createrole: params.createrole, + }); + + return toUserResponse(user); +} + +/** + * user.get - Get user by ID. + */ +async function userGet( + params: UserGetParams, + context: HandlerContext, +): Promise { + assertEngineContext(context); + const { db } = context as EngineContext; + + const user = await db.getUser(params.id); + if (!user) { + throw new AppError("NOT_FOUND", `User not found: ${params.id}`); + } + + return toUserResponse(user); +} + +/** + * user.getByName - Get user by name. + */ +async function userGetByName( + params: UserGetByNameParams, + context: HandlerContext, +): Promise { + assertEngineContext(context); + const { db } = context as EngineContext; + + const user = await db.getUserByName(params.name); + if (!user) { + throw new AppError("NOT_FOUND", `User not found: ${params.name}`); + } + + return toUserResponse(user); +} + +/** + * user.list - List users. + */ +async function userList( + params: UserListParams, + context: HandlerContext, +): Promise<{ users: UserResponse[] }> { + assertEngineContext(context); + const { db } = context as EngineContext; + + const users = await db.listUsers(params.canLogin); + return { users: users.map(toUserResponse) }; +} + +/** + * user.rename - Rename a user. + */ +async function userRename( + params: UserRenameParams, + context: HandlerContext, +): Promise<{ renamed: boolean }> { + assertEngineContext(context); + const { db } = context as EngineContext; + + const renamed = await db.renameUser(params.id, params.name); + if (!renamed) { + throw new AppError("NOT_FOUND", `User not found: ${params.id}`); + } + + return { renamed }; +} + +/** + * user.delete - Delete a user. + */ +async function userDelete( + params: UserDeleteParams, + context: HandlerContext, +): Promise<{ deleted: boolean }> { + assertEngineContext(context); + const { db } = context as EngineContext; + + const deleted = await db.deleteUser(params.id); + if (!deleted) { + throw new AppError("NOT_FOUND", `User not found: ${params.id}`); + } + + return { deleted }; +} + +// ============================================================================= +// Registry +// ============================================================================= + +/** + * Build the user methods registry. + */ +export const userMethods = buildRegistry() + .register("user.create", userCreateSchema, userCreate) + .register("user.get", userGetSchema, userGet) + .register("user.getByName", userGetByNameSchema, userGetByName) + .register("user.list", userListSchema, userList) + .register("user.rename", userRenameSchema, userRename) + .register("user.delete", userDeleteSchema, userDelete) + .build();