Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,109 @@
import {
RATE_LIMIT_PLUGIN_CODES,
RateLimitPluginError,
} from "../error-codes/error-codes.js";
/**
* A compiled path pattern ready for matching against inbound request
* paths. Created once per route rule at plugin init and reused per
* request.
*/
export interface CompiledPathPattern {
readonly source: string;
readonly regex: RegExp;
readonly paramNames: readonly string[];
}
/**
* Result of matching a compiled pattern against a path.
*/
export interface PathMatchResult {
readonly matched: boolean;
readonly params: Readonly<Record<string, string>>;
}
const PARAM_RE = /:[A-Za-z_][A-Za-z0-9_]*/g;
function escapeRegex(literal: string): string {
return literal.replace(/[.+?^${}()|[\]\\*]/g, "\\$&");
}
/**
* Compile a Hono-style path pattern into a `CompiledPathPattern`.
*
* Supported forms:
* - exact literal: `"/api/users"` matches only `/api/users`
* - trailing wildcard: `"/api/users/*"` matches anything under
* `/api/users/`
* - inline wildcard: `"/api/*"` matches one path segment
* - named param: `"/api/users/:id"` captures `id`
*
* Throws `RateLimitPluginError(INVALID_PATH_PATTERN)` when the
* compiled regex cannot be built — defensive guard so any future
* pattern-syntax mistake surfaces via the typed code rather than a
* bare `SyntaxError`.
*/
export function compilePathPattern(source: string): CompiledPathPattern {
const paramNames: string[] = [];
let cursor = 0;
let regexBody = "";
PARAM_RE.lastIndex = 0;
for (;;) {
const match = PARAM_RE.exec(source);
if (match === null) break;
const literal = source.slice(cursor, match.index);
regexBody += escapeRegex(literal).replace(/\\\*/g, "[^/]+");
const name = match[0].slice(1);
paramNames.push(name);
regexBody += "([^/]+)";
cursor = match.index + match[0].length;
}
const tail = source.slice(cursor);
if (tail.endsWith("/*")) {
regexBody += escapeRegex(tail.slice(0, -2)).replace(/\\\*/g, "[^/]+");
regexBody += "(?:/.*)?";
} else {
regexBody += escapeRegex(tail).replace(/\\\*/g, "[^/]+");
}
let regex: RegExp;
try {
regex = new RegExp(`^${regexBody}$`);
} catch (err) {
throw new RateLimitPluginError(
RATE_LIMIT_PLUGIN_CODES.INVALID_PATH_PATTERN,
`Failed to compile path pattern '${source}'`,
{ cause: err, context: { source, statusCode: 500 } }
);
}
return { source, regex, paramNames };
}
/**
* Test a compiled pattern against a concrete path. Returns a
* `PathMatchResult` carrying any captured params keyed by the names
* declared in the source pattern.
*/
export function matchPath(
pattern: CompiledPathPattern,
path: string
): PathMatchResult {
const match = pattern.regex.exec(path);
if (match === null) return { matched: false, params: Object.freeze({}) };
const params: Record<string, string> = {};
for (let i = 0; i < pattern.paramNames.length; i++) {
const name = pattern.paramNames[i] as string;
// `([^/]+)` is non-optional so a successful regex match guarantees
// every capture group resolved to a non-empty string.
params[name] = match[i + 1] as string;
}
return { matched: true, params: Object.freeze(params) };
}
/**
* Convenience helper: compile and match in a single call. Suitable for
* one-off tests; in production the plugin caches `CompiledPathPattern`
* per route rule.
*/
export function isPathMatch(source: string, path: string): boolean {
return matchPath(compilePathPattern(source), path).matched;
}