// Groups admin screen: list / create / delete Keto groups and manage membership. // A group is a Keto subject set `Group:#members`; a member is a user or a nested group (see // parseSubject). Writes go only to Keto (README "stateless"). Keto has no "create object" — a group // exists exactly while it has ≥1 member, so create writes its first-member tuple and delete removes // every member tuple. Pure builders turn tuples + the request URL into view models; below them are thin // per-route handlers (keyed on ctx.params) over a shared `withGroups` gate — admin-only, CSRF-guarded, // each returning a RouteResult. import { type KetoClient, type KratosAdmin, paginate, parseListQuery, type RelationQuery, type RelationTuple, type RequestContext, type RouteHandler, type RouteResult, type SubjectSet, type User } from "#plugin-api"; import { ADMIN_GROUPS_BASE, buildConfirmModel, guardedForm, notFound, requireAdmin, unavailable } from "./admin-shared.ts"; import type { FieldConfig } from "./admin-users.ts"; const GROUP_NS = "Group"; const MEMBERS = "members"; const DEFAULT_PAGE_SIZE = 25; const PAGE_SIZES = [25, 50, 100]; // One Keto page of candidate users is fetched for the member pickers (mirrors admin-users). const LIST_FETCH_SIZE = 250; const GROUP_NAME = /^[a-z0-9][a-z0-9_-]*$/; // URL-safe; doubles as the path segment const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; // a Kratos identity id export interface GroupView { memberCount: number; name: string; } // A member's view model: a user (label = email) or a nested group (label = group name). `subject` // is the form value that round-trips it — `user:` or `group:` (see parseSubject). export interface MemberView { kind: "group" | "user"; label: string; subject: string; } // One option in a member