```ts
/** Match a slash-separated route pattern against a request path. */
export function matchRoute(
  pattern: string,
  path: string,
): Record<string, string> | null {
  if (pattern.includes("") || pattern.startsWith(":") || pattern.endsWith("**")) {
    throw new Error("Invalid pattern");
  }

  const pats = pattern === "" ? [] : pattern.split("/");
  const parts = path === "" ? [] : path.split("/");

  return matchFrom(pats, 0, parts, 0, {});
}

function matchFrom(
  pats: string[],
  pi: number,
  parts: string[],
  si: number,
  caps: Record<string, string>,
): Record<string, string> | null {
  if (pi === pats.length) {
    return si === parts.length ? caps : null;
  }
  const seg = pats[pi];
  if (seg === "**") {
    for (let take = si; take <= parts.length; take++) {
      const found = matchFrom(pats, pi + 1, parts, take, caps);
      if (found !== null) {
        return found;
      }
    }
    return null;
  }
  if (si === parts.length) {
    return null;
  }
  if (seg.startsWith(":")) {
    const name = seg.slice(1);
    if (!name) {
      throw new Error("Invalid capture segment");
    }
    const next = { ...caps, [name]: parts[si] };
    return matchFrom(pats, pi + 1, parts, si + 1, next);
  }
  if (seg === parts[si]) {
    return matchFrom(pats, pi + 1, parts, si + 1, caps);
  }
  return null;
}
```