Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/nurl.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {decode, encode} from './punycode'
import {mask, MaskOptions, match as matchUrlPattern} from './utils'
import {
extractPathKey,
getDynamicPaths,
Expand All @@ -9,7 +8,10 @@ import {
refineQueryWithPathname,
convertQueryToArray,
Query,
} from './utils/internal'
match as matchUrlPattern,
mask,
MaskOptions,
} from './utils'

interface URLOptions
extends Partial<
Expand Down
65 changes: 0 additions & 65 deletions src/utils/external.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * from './external'
export * from './url'
86 changes: 84 additions & 2 deletions src/utils/internal.ts → src/utils/url.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import NURL from '../nurl'

const DYNAMIC_PATH_COLON_REGEXP = /^:/
const DYNAMIC_PATH_BRACKETS_REGEXP = /^\[.*\]$/

Expand Down Expand Up @@ -87,11 +89,91 @@ export function convertQueryToArray(query: Query): string[][] {
* @param {string} pathname
* @returns {string} path priority representation
*
* @example /user/:id/profile -> 212
* @example /user/admin/:tab -> 221
* @example getPathPriority('/user/:id/profile') -> '212'
* @example getPathPriority('/user/admin/:tab') -> '221'
*/
export function getPathPriority(pathname: string): string {
const segments = pathname.split('/').filter(Boolean)

return segments.map((segment) => (isDynamicPath(segment) ? '1' : '2')).join('')
}

/**
* Match a URL against a pattern and extract dynamic parameters.
* @param {string} url - The URL to match.
* @param {string} pattern - The pattern to match against.
* @returns {Record<string, string> | null} - An object containing the extracted parameters or null if no match.
*/
export const match = (url: string, pattern: string): Record<string, string> | null => {
if (!NURL.canParse(url) || !NURL.canParse(pattern)) {
return null
}

const urlSegments = url.split(/[?#]/)[0]?.split('/').filter(Boolean) || []
const patternSegments = pattern.split(/[?#]/)[0]?.split('/').filter(Boolean) || []

if (urlSegments.length !== patternSegments.length) {
return null
}

const params: Record<string, string> = {}

for (let i = 0; i < patternSegments.length; i++) {
const patternSegment = patternSegments[i]
const urlSegment = urlSegments[i]

if (isDynamicPath(patternSegment)) {
const pathKey = extractPathKey(patternSegment)
params[pathKey] = urlSegment
} else if (patternSegment !== urlSegment) {
return null
}
}

return params
}

export interface MaskOptions {
/** Patterns to match against the URL pathname */
patterns: string[]
/** Sensitive parameters to mask */
sensitiveParams: string[]
/** Character used for masking (default: '*') */
maskChar?: string
/** Length of the mask (default: 4) */
maskLength?: number
/** Whether to preserve the length of the sensitive value when masking (default: false) */
preserveLength?: boolean
}

/**
* Masks sensitive parameters in a URL based on provided options.
* @param {string} url - The URL to mask.
* @param {MaskOptions} options - The masking options.
* @returns {string} - The masked URL.
*/
export const mask = (
url: string,
{patterns, sensitiveParams, maskChar = '*', maskLength = 4, preserveLength = false}: MaskOptions,
) => {
const sortedPatterns = [...patterns].sort((a, b) => (getPathPriority(b) > getPathPriority(a) ? 1 : -1))
for (const pattern of sortedPatterns) {
const urlObj = NURL.create(url)
const matchedParams = match(urlObj.pathname, pattern)
if (!matchedParams) {
continue
}
sensitiveParams.forEach((sensitiveParam) => {
if (sensitiveParam in matchedParams) {
const originalValue = matchedParams[sensitiveParam]
const lengthToMask = preserveLength ? originalValue.length : maskLength
matchedParams[sensitiveParam] = maskChar.repeat(lengthToMask)
}
})

urlObj.pathname = refinePathnameWithQuery(pattern, matchedParams)
return urlObj.toString()
}

return url
}
Loading