UNPKG

@rbac/rbac

Version:

Blazing Fast, Zero dependency, Hierarchical Role-Based Access Control for Node.js

234 lines (233 loc) 8.68 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.buildPermissionData = exports.hasMatchingOperation = exports.globToRegex = exports.regexFromOperation = exports.normalizeWhen = exports.defaultLogger = exports.underline = exports.colorize = exports.supportsColor = exports.isGlob = void 0; const isRegex = (value) => value instanceof RegExp; const isGlob = (value) => typeof value === 'string' && value.includes('*'); exports.isGlob = isGlob; // Color support detection cache let colorSupportCache = null; /** * Determines if the current environment supports ANSI color codes. * * Detection logic (in priority order): * 1. If FORCE_COLOR is set to truthy value → return true * 2. If NO_COLOR is set to any value → return false * 3. If process.stdout.isTTY is false or undefined → return false * 4. If in known CI environment with color support → return true * 5. If process.stdout.isTTY is true → return true * 6. Default → return false * * The result is cached to avoid repeated environment checks. */ const supportsColor = () => { // Return cached result if available if (colorSupportCache !== null) { return colorSupportCache; } try { // Check FORCE_COLOR (highest priority) const forceColor = process.env.FORCE_COLOR; if (forceColor !== undefined && forceColor !== '' && forceColor !== '0' && forceColor !== 'false') { colorSupportCache = true; return true; } // Check NO_COLOR (second priority) const noColor = process.env.NO_COLOR; if (noColor !== undefined) { colorSupportCache = false; return false; } // Check if stdout is available and is a TTY if (!process.stdout || process.stdout.isTTY !== true) { colorSupportCache = false; return false; } // Check for known CI environments with color support const ci = process.env.CI; if (ci !== undefined) { const githubActions = process.env.GITHUB_ACTIONS; const gitlabCi = process.env.GITLAB_CI; const circleCi = process.env.CIRCLECI; if (githubActions || gitlabCi || circleCi) { colorSupportCache = true; return true; } } // If stdout is a TTY, support colors if (process.stdout.isTTY === true) { colorSupportCache = true; return true; } // Default to no color support colorSupportCache = false; return false; } catch (error) { // On any error, default to plain text output colorSupportCache = false; return false; } }; exports.supportsColor = supportsColor; /** * Conditionally applies ANSI color codes to text based on color support. * * @param text - The text to potentially colorize * @param colorCode - The ANSI color code (e.g., "1;32" for bright green) * @param enabled - Whether color support is enabled * @returns Formatted text with ANSI codes when enabled, plain text otherwise */ const colorize = (text, colorCode, enabled) => { if (enabled) { return `\x1b[${colorCode}m${text}\x1b[0m`; } return text; }; exports.colorize = colorize; const globPatterns = { '*': '([^/]+)', '**': '(.+/)?([^/]+)', '**/': '(.+/)?' }; const replaceGlobToRegex = (glob) => glob .replace(/\./g, '\\.') .replace(/\*\*$/g, '(.+)') .replace(/(?:\*\*\/|\*\*|\*)/g, str => globPatterns[str]); const joinGlobs = (globs) => '(' + globs.map(replaceGlobToRegex).join('|') + ')'; const underline = () => '-'.repeat(Math.max((process.stdout.columns || 80) - 1, 1)); exports.underline = underline; const defaultLogger = (role, operation, result, colorsEnabled) => { // Detect color support once at the start, or use provided value const useColors = colorsEnabled !== null && colorsEnabled !== void 0 ? colorsEnabled : (0, exports.supportsColor)(); // Apply colors conditionally based on detection const resultColor = result ? '1;32' : '1;31'; // green for true, red for false const fResult = (0, exports.colorize)(String(result), resultColor, useColors); const fRole = (0, exports.colorize)(String(role), '1;33', useColors); // yellow const fOperation = (0, exports.colorize)(String(operation), '1;33', useColors); // yellow const rbacname = (0, exports.colorize)('RBAC', '1;37', useColors); // white // Build the main message with blue base color const mainMessage = ` ${rbacname} ROLE: [${fRole}] OPERATION: [${fOperation}] PERMISSION: [${fResult}]`; const coloredMainMessage = useColors ? `\x1b[1;34m${mainMessage}\x1b[0m` : mainMessage; // Build underline with yellow color const underlineStr = (0, exports.underline)(); const coloredUnderline = useColors ? `\x1b[33m${underlineStr}\x1b[0m` : underlineStr; console.log('%s ', coloredUnderline); console.log('%s ', coloredMainMessage); console.log('%s ', coloredUnderline); }; exports.defaultLogger = defaultLogger; const normalizeWhen = (when) => { if (when === true) return true; if (when === false) return async () => false; if (typeof when === 'function') { if (when.length >= 2) { return async (params) => new Promise(resolve => { when(params, (err, result) => { if (err) return resolve(false); resolve(Boolean(result)); }); }); } return async (params) => { try { return Boolean(await when(params)); } catch (_a) { return false; } }; } if (when instanceof Promise) { return async () => { try { return Boolean(await when); } catch (_a) { return false; } }; } return async () => Boolean(when); }; exports.normalizeWhen = normalizeWhen; const regexCache = new Map(); const globCache = new Map(); const regexFromOperation = (value) => { if (isRegex(value)) return value; const cached = regexCache.get(value); if (cached) return cached; try { const flags = value.replace(/.*\/([gimsuy]*)$/, '$1'); const pattern = value.replace(new RegExp('^/(.*?)/' + flags + '$'), '$1'); const regex = new RegExp(pattern, flags); regexCache.set(value, regex); return regex; } catch (e) { return null; } }; exports.regexFromOperation = regexFromOperation; const globToRegex = (glob) => { if (Array.isArray(glob)) return new RegExp('^' + joinGlobs(glob) + '$'); const cached = globCache.get(glob); if (cached) return cached; const regex = new RegExp('^' + replaceGlobToRegex(glob) + '$'); globCache.set(glob, regex); return regex; }; exports.globToRegex = globToRegex; const hasMatchingOperation = (regex, names) => { for (const name of names) { regex.lastIndex = 0; if (regex.test(name)) return true; } return false; }; exports.hasMatchingOperation = hasMatchingOperation; const buildPermissionData = (permissions) => { const direct = new Set(); const conditional = new Map(); const patterns = []; for (const p of permissions) { if (typeof p === 'string') { const regex = (0, exports.regexFromOperation)(p); if ((0, exports.isGlob)(p)) { patterns.push({ name: p, regex: (0, exports.globToRegex)(p), when: true }); } else if (regex) { patterns.push({ name: p, regex, when: true }); } else { direct.add(p); } } else { const when = (0, exports.normalizeWhen)(p.when); const regex = (0, exports.regexFromOperation)(p.name); if ((0, exports.isGlob)(p.name)) { patterns.push({ name: p.name, regex: (0, exports.globToRegex)(p.name), when }); } else if (regex) { patterns.push({ name: p.name, regex, when }); } else if (when === true) { direct.add(p.name); } else { conditional.set(p.name, when); } } } const all = Array.from(direct).concat(Array.from(conditional.keys()), patterns.map(p => p.name)); return { direct, conditional, patterns, all }; }; exports.buildPermissionData = buildPermissionData;