@satoshibits/cache-keys
Version:
Type-safe cache key generation with zero dependencies
170 lines • 5.83 kB
JavaScript
;
/**
* Prefix for encoded components to make decoding deterministic
*/
var ENCODING_PREFIX = '_e:';
/**
* Default base64url encoder
*/
function base64urlEncode(str) {
// Use btoa for browser compatibility, with Buffer as fallback for Node
var base64 = typeof globalThis.Buffer !== 'undefined'
? globalThis.Buffer.from(str).toString('base64')
: globalThis.btoa(str);
return base64
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
/**
* Default base64url decoder
*/
export function base64urlDecode(str) {
// Validate base64url format
if (!/^[A-Za-z0-9_-]*$/.test(str)) {
throw new Error('Invalid base64url string');
}
// Add padding if needed
var padding = '='.repeat((4 - (str.length % 4)) % 4);
var base64 = str.replace(/-/g, '+').replace(/_/g, '/') + padding;
// Use Buffer for Node, atob for browser
return typeof globalThis.Buffer !== 'undefined'
? globalThis.Buffer.from(base64, 'base64').toString()
: globalThis.atob(base64);
}
/**
* Sanitize a cache key component to prevent injection attacks
* @param component The component to sanitize
* @param options Sanitization options
* @returns Sanitized component
*/
export function sanitizeKeyComponent(component, options) {
if (options === void 0) { options = {}; }
var _a = options.allowColons, allowColons = _a === void 0 ? false : _a, _b = options.encoder, encoder = _b === void 0 ? base64urlEncode : _b;
// For components that might contain special characters
if (!allowColons && component.includes(':')) {
return ENCODING_PREFIX + encoder(component);
}
// Check for pattern-matching wildcards or other dangerous characters
var dangerousChars = /[*?[\]\\@]/g;
if (dangerousChars.test(component)) {
return ENCODING_PREFIX + encoder(component);
}
return component;
}
/**
* Attempt to decode a potentially encoded component
* @param component The component that might be encoded
* @returns Decoded component or original if not encoded
*/
export function decodeKeyComponent(component) {
// Only decode if it has our encoding prefix
if (component.startsWith(ENCODING_PREFIX)) {
var encodedPart = component.substring(ENCODING_PREFIX.length);
// Handle empty encoded part
if (!encodedPart) {
return '';
}
try {
return base64urlDecode(encodedPart);
}
catch (_a) {
// If decoding fails, return the component as-is to be safe
return component;
}
}
return component;
}
/**
* Validate that a key component is safe to use
* @param component The component to validate
* @returns true if the component is safe
*/
export function isValidKeyComponent(component) {
// Empty components are not allowed
if (!component || component.length === 0) {
return false;
}
// Check for null bytes or other control characters
// eslint-disable-next-line no-control-regex
if (/[\x00-\x1F\x7F]/.test(component)) {
return false;
}
return true;
}
/**
* Join multiple components into a cache key
* @param components Components to join
* @param separator Separator between components
* @param sanitize Whether to sanitize components
* @returns Joined cache key
*/
export function joinKeyComponents(components, separator, sanitize) {
if (separator === void 0) { separator = ':'; }
if (sanitize === void 0) { sanitize = true; }
if (sanitize) {
components = components.map(function (c) { return sanitizeKeyComponent(c); });
}
// Validate all components
if (!components.every(isValidKeyComponent)) {
throw new Error('Invalid key component detected');
}
return components.join(separator);
}
/**
* Split a cache key into components
* @param key The key to split
* @param separator Separator between components
* @param decode Whether to decode components
* @returns Array of components
*/
export function splitKeyComponents(key, separator, decode) {
if (separator === void 0) { separator = ':'; }
if (decode === void 0) { decode = true; }
var components = [];
var current = '';
var i = 0;
while (i < key.length) {
// Check if we're at the start of an encoded component
if (key.slice(i).startsWith(ENCODING_PREFIX)) {
// If we have accumulated content, push it as a component
if (current) {
components.push(current);
current = '';
}
// Find the next separator after the encoding prefix
var prefixEnd = i + ENCODING_PREFIX.length;
var nextSeparator = key.indexOf(separator, prefixEnd);
// If no separator found, the rest of the string is the encoded component
if (nextSeparator === -1) {
components.push(key.slice(i));
break;
}
// Extract the encoded component including the prefix
components.push(key.slice(i, nextSeparator));
i = nextSeparator + separator.length;
}
else if (key[i] === separator) {
// Regular separator - push current component if any
if (current || components.length > 0) {
components.push(current);
current = '';
}
i++;
}
else {
// Regular character - add to current component
current += key[i];
i++;
}
}
// Don't forget the last component
if (current) {
components.push(current);
}
if (decode) {
return components.map(decodeKeyComponent);
}
return components;
}
//# sourceMappingURL=sanitizer.mjs.map