/** * 文件库领域基础:角色秩、命名规则(D14)、错误类型。 * * 语义锚定:仓库根《文件库-接口契约.md》(2.2 权限等级 / D8 可见性 / D14 命名) * 与 .omo/文件库-开工计划.md。本模块是独立文件库,不复用 hub 的 * Folder/Project/PermissionGrant 体系。 */ export const FILE_LIB_ROLES = ["VIEW", "EDIT", "MANAGE"] as const; export type FileLibRole = (typeof FILE_LIB_ROLES)[number]; /** 契约 2.2:MANAGE > EDIT > VIEW,严格全序。 */ export const ROLE_RANK: Record = { VIEW: 1, EDIT: 2, MANAGE: 3 }; export function roleAtLeast(role: FileLibRole, min: FileLibRole): boolean { return ROLE_RANK[role] >= ROLE_RANK[min]; } /** 业务错误。statusCode 由路由层映射为 HTTP 响应(D8 语义在此层只表达为 code)。 */ export class FileLibError extends Error { constructor( readonly statusCode: number, readonly code: string, message: string, /** 结构化附加信息(如 409 时的 currentVersion),路由层并入错误响应。 */ readonly details?: Record, ) { super(message); this.name = "FileLibError"; } } export const NODE_NAME_MAX_LENGTH = 128; /** D14:禁 `/`;反斜杠同样禁止(它会变成存储路径的分隔符,且易用于伪装)。控制字符禁。 */ const FORBIDDEN_NAME_CHARS = /[/\\\p{C}]/u; /** * D14:NFC 归一化 + trim,然后校验长度与字符集。 * 违规抛 FileLibError(400, "invalid_name"),路由层原样透传。 */ export function normalizeNodeName(raw: string): string { const name = raw.normalize("NFC").trim(); if (name.length === 0) { throw new FileLibError(400, "invalid_name", "name must not be empty"); } if (name.length > NODE_NAME_MAX_LENGTH) { throw new FileLibError(400, "invalid_name", `name exceeds ${NODE_NAME_MAX_LENGTH} characters`); } if (FORBIDDEN_NAME_CHARS.test(name)) { throw new FileLibError(400, "invalid_name", "name must not contain '/', '\\' or control characters"); } return name; } /** D14:活跃兄弟节点大小写不敏感唯一的比较键(DB 层另有部分唯一索引兜底)。 */ export function nameKey(normalizedName: string): string { return normalizedName.toLowerCase(); }