```ts
/**
 * Fills a template string by replacing {key} placeholders with values from the mapping.
 */
export function fillTemplate(template: string, values: Record<string, string>): string {
  if (typeof template !== "string") {
    throw new Error("fillTemplate expects a string template");
  }

  let result = "";
  let i = 0;

  while (i < template.length) {
    if (template[i] === "{") {
      const end = template.indexOf("}", i);
      if (end === -1) {
        throw new Error("unterminated placeholder");
      }

      const name = template.slice(i + 1, end);
      if (name === "") {
        throw new Error("empty placeholder name");
      }

      if (!/^[a-zA-Z0-9_]+$/.test(name)) {
        throw new Error("invalid placeholder name");
      }

      const value = values[name];
      if (value === undefined) {
        throw new Error(`unknown placeholder name: ${name}`);
      }

      result += value;
      i = end + 1;
    } else {
      result += template[i];
      i++;
    }
  }

  return result;
}
```