import type { Tagged } from 'type-fest'; /** RID 字段要求(至少 1 字符) */ export const RESOURCE_ID_PART_MIN_LENGTH = 1 as number; /** RID 字段要求(至多 240 字符) */ export const RESOURCE_ID_PART_MAX_LENGTH = 240 as number; /** * 允许的字符: * - 用户可用: * - 'A-Za-z0-9' * - '_-' * - 内部使用: * - '~':[owner] 用于分隔外部用户名和外部证书名(自定义认证证书) * - '@':[owner](暂定)跨系统组网时用于分隔用户名和外部系统名 * - '@'开头:表示“非资源类型/非用户/非资源”,在日志等非资源/用户系统中需要用户名、资源类型或 RID,但无法提供时使用 * - '!.' * - '()[]{}' */ const RESOURCE_ID_PART_VALID_CHARS = [ // '\\0-\\x1F', // 控制字符 // ' ', // 0x20 '!', // 0x21 // '"', // 0x22 // '#', // 0x23 // '$', // 0x24 // '%', // 0x25 // '&', // 0x26 // "'", // 0x27 '(', // 0x28 ')', // 0x29 // '*', // 0x2A // '+', // 0x2B // ',', // 0x2C String.raw`\-`, // 0x2D '.', // 0x2E // '/', // 0x2F '0-9', // 0x30-0x39 // ':', // 0x3A // ';', // 0x3B // '<', // 0x3C // '=', // 0x3D // '>', // 0x3E // '?', // 0x3F '@', // 0x40 'A-Z', // 0x41-0x5A String.raw`\[`, // 0x5B //'\\\\', // 0x5C String.raw`\]`, // 0x5D // '^', // 0x5E '_', // 0x5F // '`', // 0x60 'a-z', // 0x61-0x7A '{', // 0x7B // '|', // 0x7C '}', // 0x7D '~', // 0x7E //'\\x7F', // 控制字符 ]; const RESOURCE_ID_PART_REGEX_SOURCE = `([${RESOURCE_ID_PART_VALID_CHARS.join('')}]{${RESOURCE_ID_PART_MIN_LENGTH},${RESOURCE_ID_PART_MAX_LENGTH}})`; /** RID 字段要求(不包含特殊符号和空格) */ export const RESOURCE_ID_PART_REGEX = new RegExp(`^${RESOURCE_ID_PART_REGEX_SOURCE}$`); /** RID 字段要求 */ export const RESOURCE_ID_MAX_LENGTH = RESOURCE_ID_PART_MAX_LENGTH * 3 + 2; /** RID 正则 */ export const RESOURCE_ID_REGEX = new RegExp( `^${RESOURCE_ID_PART_REGEX_SOURCE}/${RESOURCE_ID_PART_REGEX_SOURCE}/${RESOURCE_ID_PART_REGEX_SOURCE}$`, ); /** 资源类型 ID */ export type ResourceTypeId = Tagged; /** 输出错误 */ function printErrorPart(part: unknown): string { if (part == null) return String(part); if (typeof part == 'string') { if (part.length > 20) { return JSON.stringify(part.slice(0, 20)).slice(0, 20) + '...'; } return JSON.stringify(part); } return Object.prototype.toString.call(part); } /** 检查 RID 的一段是否有效 */ function checkPart(name: string, part: unknown): asserts part is string { if (typeof part != 'string') throw new Error(`Invalid ${name} ${printErrorPart(part)}, should be string`); if (RESOURCE_ID_PART_REGEX.test(part)) return; if (part.length < RESOURCE_ID_PART_MIN_LENGTH) throw new Error( `Invalid ${name} ${printErrorPart( part, )}, should not be shorter than ${RESOURCE_ID_PART_MIN_LENGTH} characters`, ); if (part.length > RESOURCE_ID_PART_MAX_LENGTH) throw new Error( `Invalid ${name} ${printErrorPart( part, )}, should not be longer than ${RESOURCE_ID_PART_MAX_LENGTH} characters`, ); throw new Error(`Invalid ${name} ${printErrorPart(part)}, should not contains spaces or special characters`); } /** RID 信息 */ export interface ResourceIdInfo { /** 类型 */ type: ResourceTypeId; /** 所有者 */ owner: string; /** 资源 key */ key: string; } /** 生成 ResourceTypeId */ export function ResourceTypeId(type: T): ResourceTypeId { checkPart('type', type); return type as ResourceTypeId; } /** 资源 ID */ export type ResourceId = Tagged<`${ResourceTypeId}/${string}/${string}`, 'ResourceId'>; /** 生成 ResourceId */ function buildResourceId(type: ResourceTypeId, owner: string, key: string): ResourceId { checkPart('type', type); checkPart('owner', owner); checkPart('key', key); return `${type}/${owner}/${key}` as ResourceId; } /** 生成 ResourceId */ export function ResourceId(type: ResourceTypeId, owner: string, key: string): ResourceId; /** 生成 ResourceId */ export function ResourceId(val: ResourceIdInfo): ResourceId; /** 校验 ResourceId */ export function ResourceId(rid: string | null | undefined): ResourceId; /** 生成 ResourceId */ export function ResourceId(typeOrVal?: string | null | ResourceIdInfo, owner?: string, key?: string): ResourceId { if (typeOrVal == null) throw new Error(`Required an non-null value`); if (typeof typeOrVal == 'string' && owner != null && key != null) { return buildResourceId(typeOrVal as ResourceTypeId, owner, key); } if (typeof typeOrVal == 'object' && typeOrVal.type != null && typeOrVal.owner != null && typeOrVal.key != null) { return buildResourceId(typeOrVal.type, typeOrVal.owner, typeOrVal.key); } if (isResourceId(typeOrVal)) { return typeOrVal; } throw new Error(`${printErrorPart(typeOrVal)} is not an valid resource id`); } /** 生成给定类型 ResourceId */ export function ResourceIdOf( type: ResourceTypeId, ...args: [val: ResourceIdInfo] | [rid: string | null | undefined] | [owner: string, key: string] ): ResourceId { type = ResourceTypeId(type); const [ownerOrVal, key] = args; if (ownerOrVal == null) throw new Error(`Required an non-null value`); if (typeof ownerOrVal == 'string' && key != null) { return buildResourceId(type, ownerOrVal, key); } if (typeof ownerOrVal == 'object' && ownerOrVal.owner != null && ownerOrVal.key != null) { if (ownerOrVal.type !== type) throw new Error(`Invalid type ${printErrorPart(ownerOrVal.type)}, should be ${printErrorPart(type)}`); return buildResourceId(type, ownerOrVal.owner, ownerOrVal.key); } if (isResourceIdOf(type, ownerOrVal)) { return ownerOrVal; } throw new Error(`${printErrorPart(ownerOrVal)} is not an valid resource id of ${printErrorPart(type)}`); } /** 解析 ResourceId */ export function parseResourceId(val: string | null | undefined): ResourceIdInfo | undefined { if (typeof val != 'string' || !val) return undefined; const match = RESOURCE_ID_REGEX.exec(val); if (!match) return undefined; return { type: match[1]! as ResourceTypeId, owner: match[2]!, key: match[3]!, }; } /** 解析 ResourceId 为给定类型 */ export function parseResourceIdOf( type: ResourceTypeId, val: string | null | undefined, ): ResourceIdInfo | undefined { type = ResourceTypeId(type); const parsed = parseResourceId(val); if (parsed?.type !== type) return undefined; return parsed as ResourceIdInfo; } /** * 校验 ResourceId */ export function isResourceId(val: unknown): val is ResourceId { if (!val || typeof val != 'string') return false; return RESOURCE_ID_REGEX.test(val); } /** * 校验 ResourceId 为给定类型 */ export function isResourceIdOf(type: ResourceTypeId, val: unknown): val is ResourceId { type = ResourceTypeId(type); return isResourceId(val) && val.startsWith(type) && val[type.length] === '/'; }