@rbac/rbac
Version:
Blazing Fast, Zero dependency, Hierarchical Role-Based Access Control for Node.js
234 lines (233 loc) • 8.68 kB
JavaScript
;
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;