Utilities

Small browser helpers for QA and demo capture.

CircleOS PII mask

Deterministic masking for tagged names, emails, phone numbers, birth dates, addresses, and avatars. Paste into the DevTools console — the script installs window.__circlePiiMask, applies masking immediately, and watches for remounts.

Markers

<div data-pii="name">Jane Doe</div>
<div data-pii="email">jane@example.com</div>
<div data-pii="phone">+49 1512 1234567</div>
<div data-pii="dob">23.09.1988 (37)</div>
<div data-pii="address">Rosenthaler Str. 16
10119 Berlin</div>
<img data-pii="avatar" src="/avatar.jpg" alt="" />

Runtime API

window.__circlePiiMask.mask();
window.__circlePiiMask.unmask();
window.__circlePiiMask.toggle();
window.__circlePiiMask.destroy();

Script

(() => {
  if (window.__circlePiiMask?.destroy) {
    window.__circlePiiMask.destroy();
  }

  const CONFIG = {
    selector: "[data-pii]",
    avatarMode: "blur", // "blur" | "hide"
  };

  const NAME_SEEDS = [
    "Anna Schmidt",
    "Lukas Müller",
    "Sophie Fischer",
    "Paul Weber",
    "Leonie Wagner",
    "Jonas Becker",
    "Marie Hoffmann",
    "Felix Schäfer",
    "Clara Koch",
    "Noah Bauer",
    "Emma Richter",
    "Ben Klein",
    "Mila Wolf",
    "Leon Neumann",
    "Hannah Schwarz",
    "Finn Zimmermann",
    "Laura Braun",
    "Elias Krüger",
    "Johanna Hartmann",
    "Mats Lange",
    "Nele Werner",
    "David Schmitz",
    "Ida Krause",
    "Tom Meier",
    "Lina Lehmann",
    "Moritz Schulz",
    "Greta Maier",
    "Oskar Kraus",
    "Pia Keller",
    "Anton Herrmann",
    "Theresa König",
    "Julian Walter",
    "Mira Mayer",
    "Vincent Huber",
    "Paula Kaiser",
    "Emil Fuchs",
    "Amelie Peters",
    "Nico Lang",
    "Frieda Scholz",
    "Samuel Möller",
    "Lotta Weiß",
    "Maximilian Jung",
    "Carla Pohl",
    "Theo Simon",
    "Elisa Franke",
    "Jakob Albrecht",
    "Nina Vogt",
    "Henry Winter",
    "Jule Seidel",
    "Marlon Graf",
  ];

  const STREET_SEEDS = [
    "Lindenstrasse 12",
    "Bergweg 8",
    "Gartenallee 27",
    "Sonnenweg 14",
    "Feldstrasse 33",
    "Am Park 5",
    "Wiesenweg 19",
    "Birkenallee 42",
    "Bahnhofstrasse 11",
    "Mozartstrasse 24",
  ];

  const CITY_SEEDS = [
    ["10115", "Berlin"],
    ["20095", "Hamburg"],
    ["80331", "Muenchen"],
    ["50667", "Koeln"],
    ["60311", "Frankfurt am Main"],
    ["01067", "Dresden"],
    ["70173", "Stuttgart"],
    ["04109", "Leipzig"],
    ["28195", "Bremen"],
    ["30159", "Hannover"],
  ];

  const textStore = new WeakMap();
  const avatarStore = new WeakMap();

  let observer = null;
  let masked = false;
  let applying = false;
  let scheduled = false;

  const transliterate = (value) =>
    String(value)
      .normalize("NFKD")
      .replace(/[\u0300-\u036f]/g, "")
      .replace(/ä/g, "ae")
      .replace(/ö/g, "oe")
      .replace(/ü/g, "ue")
      .replace(/Ä/g, "Ae")
      .replace(/Ö/g, "Oe")
      .replace(/Ü/g, "Ue")
      .replace(/ß/g, "ss")
      .replace(/[^a-zA-Z0-9]+/g, "-")
      .replace(/^-+|-+$/g, "")
      .toLowerCase();

  const hashString = (value) => {
    let hash = 2166136261;
    for (let index = 0; index < value.length; index += 1) {
      hash ^= value.charCodeAt(index);
      hash = Math.imul(hash, 16777619);
    }
    return hash >>> 0;
  };

  const pickSeedName = (source) => {
    const index = hashString(source || "person") % NAME_SEEDS.length;
    return NAME_SEEDS[index];
  };

  const fakeEmail = (source) => {
    const [firstName, ...rest] = pickSeedName(source).split(" ");
    const lastName = rest.join(" ");
    const suffix = String((hashString(`email:${source}`) % 90) + 10);
    return `${transliterate(firstName)}.${transliterate(lastName)}${suffix}@beispiel.invalid`;
  };

  const fakePhone = (source) => {
    const prefixes = [
      "1512",
      "1523",
      "1550",
      "1577",
      "1590",
      "1602",
      "1704",
      "1718",
      "1726",
      "1763",
    ];
    const hash = hashString(`phone:${source}`);
    const prefix = prefixes[hash % prefixes.length];
    const tail = String(1000000 + (hash % 9000000));
    return `+49 ${prefix.slice(0, 3)} ${prefix.slice(3)} ${tail.slice(0, 3)} ${tail.slice(3)}`;
  };

  const fakeDob = (source) => {
    const day = String((hashString(`dob-day:${source}`) % 28) + 1).padStart(
      2,
      "0",
    );
    const month = String((hashString(`dob-month:${source}`) % 12) + 1).padStart(
      2,
      "0",
    );
    const year = String(1960 + (hashString(`dob-year:${source}`) % 40));
    const age = new Date().getFullYear() - Number(year);
    return `${day}.${month}.${year} (${age})`;
  };

  const fakeAddress = (source) => {
    const street =
      STREET_SEEDS[hashString(`street:${source}`) % STREET_SEEDS.length];
    const [postalCode, city] =
      CITY_SEEDS[hashString(`city:${source}`) % CITY_SEEDS.length];
    return `${street}\n${postalCode}, ${city}, DE`;
  };

  const getText = (element) => {
    if (
      element instanceof HTMLInputElement ||
      element instanceof HTMLTextAreaElement
    ) {
      return element.value;
    }
    return element.textContent || "";
  };

  const setText = (element, value) => {
    if (
      element instanceof HTMLInputElement ||
      element instanceof HTMLTextAreaElement
    ) {
      element.value = value;
      return;
    }
    element.textContent = value;
  };

  const maskTextValue = (kind, original) => {
    const clean = String(original || "").trim();
    if (!clean) return clean;

    if (kind === "name") return pickSeedName(clean);
    if (kind === "email") return fakeEmail(clean);
    if (kind === "phone") return fakePhone(clean);
    if (kind === "dob") return fakeDob(clean);
    if (kind === "address") return fakeAddress(clean);
    return clean;
  };

  const rememberText = (element) => {
    if (!textStore.has(element)) {
      textStore.set(element, getText(element));
    }
    return textStore.get(element);
  };

  const rememberAvatar = (element) => {
    if (!avatarStore.has(element)) {
      avatarStore.set(element, {
        filter: element.style.filter,
        opacity: element.style.opacity,
        transform: element.style.transform,
        transition: element.style.transition,
        pointerEvents: element.style.pointerEvents,
      });
    }
    return avatarStore.get(element);
  };

  const maskAvatar = (element) => {
    rememberAvatar(element);
    element.style.transition =
      "filter 120ms ease, opacity 120ms ease, transform 120ms ease";
    element.style.pointerEvents = "none";

    if (CONFIG.avatarMode === "hide") {
      element.style.opacity = "0.08";
      element.style.filter = "grayscale(1) brightness(1.15)";
      element.style.transform = "scale(1.02)";
      return;
    }

    element.style.opacity = "1";
    element.style.filter =
      "blur(12px) grayscale(1) saturate(0) brightness(1.05)";
    element.style.transform = "scale(1.08)";
  };

  const unmaskAvatar = (element) => {
    const original = avatarStore.get(element);
    if (!original) return;
    element.style.filter = original.filter;
    element.style.opacity = original.opacity;
    element.style.transform = original.transform;
    element.style.transition = original.transition;
    element.style.pointerEvents = original.pointerEvents;
  };

  const maskElement = (element) => {
    const kind = element.getAttribute("data-pii");
    if (!kind) return;

    if (kind === "avatar") {
      maskAvatar(element);
      element.setAttribute("data-pii-masked", "true");
      return;
    }

    const original = rememberText(element);
    setText(element, maskTextValue(kind, original));
    if (kind === "address") {
      element.style.whiteSpace = "pre-line";
    }
    element.setAttribute("data-pii-masked", "true");
  };

  const unmaskElement = (element) => {
    const kind = element.getAttribute("data-pii");
    if (!kind) return;

    if (kind === "avatar") {
      unmaskAvatar(element);
      element.removeAttribute("data-pii-masked");
      return;
    }

    if (!textStore.has(element)) return;
    setText(element, textStore.get(element));
    if (kind === "address") {
      element.style.whiteSpace = "";
    }
    element.removeAttribute("data-pii-masked");
  };

  const applyMask = (root = document) => {
    applying = true;
    root.querySelectorAll(CONFIG.selector).forEach(maskElement);
    applying = false;
  };

  const applyUnmask = (root = document) => {
    applying = true;
    root
      .querySelectorAll(`${CONFIG.selector}[data-pii-masked="true"]`)
      .forEach(unmaskElement);
    applying = false;
  };

  const scheduleRefresh = () => {
    if (!masked || scheduled) return;
    scheduled = true;
    requestAnimationFrame(() => {
      scheduled = false;
      applyMask(document);
    });
  };

  const startObserver = () => {
    if (observer) return;

    observer = new MutationObserver(() => {
      if (!applying) scheduleRefresh();
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true,
      characterData: true,
      attributes: true,
    });
  };

  const stopObserver = () => {
    if (!observer) return;
    observer.disconnect();
    observer = null;
  };

  const api = {
    config: CONFIG,
    mask() {
      masked = true;
      applyMask(document);
      startObserver();
      console.info("PII masking enabled");
    },
    unmask() {
      masked = false;
      stopObserver();
      applyUnmask(document);
      console.info("PII masking disabled");
    },
    toggle() {
      if (masked) this.unmask();
      else this.mask();
    },
    destroy() {
      this.unmask();
      delete window.__circlePiiMask;
    },
  };

  window.__circlePiiMask = api;
  api.mask();
})();