// ── Lock patterns: `:param` segments and a trailing `*` ──────────────────── // // The demo's lock list names routes the way the routers declare them — // `/admin/users/:id` — so a reviewer can hold the list against the route // manifest line by line. A trailing `/*` means "this path and everything under // it", for the areas DEMO_MODE.md §3.3 locks as a whole ("every write under // /admin/modules"). // // The matcher reproduces the two express 4 routing defaults that decide whether // a request reaches a handler at all: matching is case-insensitive, and a // trailing slash is optional. A lock that was stricter than the router about // either would let `PUT /Admin/Site-Mode/` through to a handler that answers it. const SEGMENT = /^(?::[A-Za-z][A-Za-z0-9]*|[A-Za-z0-9._-]+)$/ const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') /** * Compile a lock pattern. * * @param {string} pattern e.g. `/admin/users/:id` or `/admin/modules/*` * @returns {{pattern: string, names: string[], re: RegExp}} * @throws on a pattern that is not absolute, has an empty segment, or puts `*` * anywhere but last — a lock that silently compiled to something else would * be worse than no lock, because it would read as one. */ function compile(pattern) { if (typeof pattern !== 'string' || !pattern.startsWith('/')) { throw new Error(`lock path "${pattern}" must start with /`) } const segs = pattern.slice(1).split('/') const names = [] let source = '' segs.forEach((seg, i) => { if (seg === '*') { if (i !== segs.length - 1) throw new Error(`lock path "${pattern}": * must be the last segment`) source += '(?:/.*)?' return } if (!SEGMENT.test(seg)) throw new Error(`lock path "${pattern}": bad segment "${seg}"`) if (seg.startsWith(':')) { names.push(seg.slice(1)) source += '/([^/]+)' } else { source += `/${escape(seg)}` } }) return { pattern, names, re: new RegExp(`^${source}/?$`, 'i') } } /** * Match a request path against a compiled pattern. * * @returns {Record|null} the decoded params, or null */ function match(compiled, path) { const m = compiled.re.exec(path) if (!m) return null const params = {} compiled.names.forEach((name, i) => { try { params[name] = decodeURIComponent(m[i + 1]) } catch { params[name] = m[i + 1] } }) return params } module.exports = { compile, match }