> ## Documentation Index
> Fetch the complete documentation index at: https://docs.endorlabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Deploy Package Firewall to developer machines with MDM

> Generate self-contained scripts that configure developer machines to route package installations through Endor Labs Package Firewall, and push them with your MDM tool.

export const Draft = ({children}) => {
  const SHOW_PARAM = 'show';
  const SHOW_VALUE = 'hidden';
  const BORDER_RGB = '220, 38, 38';
  const BORDER_COLOR = '#dc2626';
  const BG_LIGHT = 'rgba(254, 226, 226, 0.65)';
  const BG_DARK = 'rgba(127, 29, 29, 0.35)';
  const HEADER_TEXT_LIGHT = '#111111';
  const HEADER_TEXT_DARK = '#f5f5f5';
  const CARD_MARGIN = '1rem 0';
  const CARD_BORDER_RADIUS = '8px';
  const HEADER_PADDING = '0.75rem 1rem';
  const HEADER_FONT_SIZE = '1.25rem';
  const BODY_PADDING = '0 1rem 0.75rem';
  const ref = useRef(null);
  const [isDark, setIsDark] = useState(false);
  useEffect(() => {
    const checkTheme = () => {
      const root = document.documentElement;
      setIsDark(root.dataset.theme === 'dark' || root.classList.contains('dark'));
    };
    checkTheme();
    const observer = new MutationObserver(checkTheme);
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ['data-theme', 'class']
    });
    return () => observer.disconnect();
  }, []);
  useEffect(() => {
    if (!ref.current) return;
    const params = new URLSearchParams(globalThis.location.search);
    if (params.get(SHOW_PARAM) === SHOW_VALUE) {
      ref.current.style.display = 'block';
    }
  }, []);
  return <div ref={ref} className="not-prose" style={{
    display: 'none'
  }} role="note" aria-label="Draft internal content">
      <div style={{
    margin: CARD_MARGIN,
    border: `2px solid ${BORDER_COLOR}`,
    borderRadius: CARD_BORDER_RADIUS,
    backgroundColor: isDark ? BG_DARK : BG_LIGHT,
    boxShadow: `0 0 0 1px rgba(${BORDER_RGB}, 0.15) inset`
  }}>
        <div className="not-prose" style={{
    padding: HEADER_PADDING,
    fontWeight: 700,
    fontSize: HEADER_FONT_SIZE,
    color: isDark ? HEADER_TEXT_DARK : HEADER_TEXT_LIGHT,
    textAlign: 'center'
  }}>
          Do not use! Draft content. Development in progress.
        </div>
        <div style={{
    padding: BODY_PADDING
  }}>{children}</div>
      </div>
    </div>;
};

export const LicenseBadge = ({sku, skus, relation = 'any'}) => {
  const DATA_URL = '/snippets/license-sku-data.json';
  const CACHE_KEY = 'license-sku-data';
  const CACHE_TTL_MS = 60 * 60 * 1000;
  const REGISTRY_KEY = '__licenseSkuRegistry';
  const FALLBACK_LICENSES_URL = '/introduction/licenses';
  const ACCENT = '#26D07C';
  const CONJUNCTION = {
    any: 'or',
    all: 'and'
  };
  const FONT_STACK = '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif';
  const [isDark, setIsDark] = useState(false);
  const [data, setData] = useState(null);
  const [hasFetchError, setHasFetchError] = useState(false);
  const skuList = useMemo(() => {
    if (Array.isArray(skus) && skus.length > 0) {
      return skus.filter(code => typeof code === 'string' && code.trim()).map(c => c.trim());
    }
    if (typeof sku === 'string' && sku.trim()) {
      return [sku.trim()];
    }
    return [];
  }, [sku, skus]);
  useEffect(() => {
    const check = () => {
      const root = document.documentElement;
      setIsDark(root.dataset.theme === 'dark' || root.classList.contains('dark'));
    };
    check();
    const observer = new MutationObserver(check);
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ['data-theme', 'class']
    });
    return () => observer.disconnect();
  }, []);
  useEffect(() => {
    let cancelled = false;
    const readCache = () => {
      try {
        const raw = sessionStorage.getItem(CACHE_KEY);
        if (!raw) return null;
        const parsed = JSON.parse(raw);
        if (Date.now() - parsed.ts > CACHE_TTL_MS) {
          sessionStorage.removeItem(CACHE_KEY);
          return null;
        }
        return parsed.data;
      } catch (e) {
        return null;
      }
    };
    const writeCache = value => {
      try {
        sessionStorage.setItem(CACHE_KEY, JSON.stringify({
          ts: Date.now(),
          data: value
        }));
      } catch (e) {}
    };
    const fetchSkuData = async () => {
      const cached = readCache();
      if (cached) return cached;
      if (!globalThis[REGISTRY_KEY]) {
        globalThis[REGISTRY_KEY] = {};
      }
      const registry = globalThis[REGISTRY_KEY];
      if (registry[CACHE_KEY]) return registry[CACHE_KEY];
      const promise = (async () => {
        const resp = await fetch(DATA_URL);
        if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
        const json = await resp.json();
        writeCache(json);
        return json;
      })();
      registry[CACHE_KEY] = promise;
      promise.finally(() => {
        delete registry[CACHE_KEY];
      });
      return promise;
    };
    fetchSkuData().then(d => {
      if (!cancelled) setData(d);
    }).catch(() => {
      if (!cancelled) setHasFetchError(true);
    });
    return () => {
      cancelled = true;
    };
  }, []);
  const textColor = isDark ? '#e6edf3' : '#1f2937';
  const textMuted = isDark ? 'rgba(230,237,243,0.65)' : 'rgba(31,41,55,0.65)';
  const bannerBackground = isDark ? '#161b22' : '#f6f8fa';
  const borderColor = isDark ? 'rgba(38,208,124,0.35)' : 'rgba(38,208,124,0.45)';
  const linkColor = isDark ? '#4ade80' : '#047857';
  const errorBackground = isDark ? '#3b1111' : '#fef2f2';
  const errorBorder = isDark ? '#7f1d1d' : '#fecaca';
  const errorText = isDark ? '#fecaca' : '#7f1d1d';
  const licensesUrl = data?.licensesPageUrl || FALLBACK_LICENSES_URL;
  const resolveSkuName = code => {
    const entry = data?.skus?.[code];
    if (entry?.name) return entry.name;
    return null;
  };
  const formatSkuLabel = code => {
    const name = resolveSkuName(code);
    if (name) return name;
    return code;
  };
  const joinWithConjunction = (codes, conjunction) => {
    const labels = codes.map(formatSkuLabel);
    if (labels.length === 0) return '';
    if (labels.length === 1) return labels[0];
    if (labels.length === 2) return `${labels[0]} ${conjunction} ${labels[1]}`;
    const head = labels.slice(0, -1).join(', ');
    const tail = labels[labels.length - 1];
    return `${head}, ${conjunction} ${tail}`;
  };
  const buildSkuSentence = codes => {
    const conj = CONJUNCTION[relation] || CONJUNCTION.any;
    const names = joinWithConjunction(codes, conj);
    const noun = codes.length > 1 ? 'licenses' : 'license';
    return `${names} ${noun}`;
  };
  const renderLink = text => <a href={licensesUrl} className="lic-link" style={{
    color: linkColor,
    fontSize: '0.75rem',
    fontWeight: 500,
    textDecoration: 'none',
    whiteSpace: 'nowrap'
  }}>
      {text} →
    </a>;
  const renderBanner = codes => <div className="lic-banner not-prose" style={{
    margin: '1rem 0',
    padding: '0.5rem 0.85rem',
    background: bannerBackground,
    border: `1px solid ${borderColor}`,
    borderLeft: `3px solid ${ACCENT}`,
    borderRadius: '6px',
    color: textColor,
    fontSize: '0.75rem',
    display: 'flex',
    alignItems: 'center',
    gap: '0.5rem',
    flexWrap: 'wrap',
    fontFamily: FONT_STACK,
    lineHeight: 1.5
  }}>
      <span style={{
    flex: 1,
    minWidth: 0
  }}>
        <span style={{
    color: textMuted,
    marginRight: '0.3rem'
  }}>Requires</span>
        <span style={{
    fontWeight: 600
  }}>{buildSkuSentence(codes)}</span>
      </span>
      {renderLink('Licenses')}
    </div>;
  const renderLoading = () => <div className="not-prose" style={{
    margin: '1rem 0',
    padding: '0.6rem 0.9rem',
    background: bannerBackground,
    border: `1px dashed ${borderColor}`,
    borderRadius: '6px',
    color: textMuted,
    fontSize: '0.85rem',
    fontStyle: 'italic',
    fontFamily: FONT_STACK
  }} role="status" aria-live="polite">
      Loading license info…
    </div>;
  const renderInputError = message => <div className="not-prose" style={{
    margin: '1rem 0',
    padding: '0.6rem 0.9rem',
    background: errorBackground,
    border: `1px solid ${errorBorder}`,
    borderRadius: '6px',
    color: errorText,
    fontSize: '0.85rem',
    fontFamily: FONT_STACK
  }} role="alert">
      {message}
    </div>;
  if (typeof sku === 'string' && Array.isArray(skus)) {
    return renderInputError('LicenseBadge: pass either `sku` or `skus`, not both.');
  }
  if (skuList.length === 0) {
    return renderInputError('LicenseBadge: `sku` or `skus` is required.');
  }
  if (hasFetchError) return renderBanner(skuList);
  if (!data) return renderLoading();
  return renderBanner(skuList);
};

export const YamlTable = ({children, data: propData, content}) => {
  const KV_RE = /^([A-Za-z][A-Za-z0-9_()/#\s-]+?):\s*(.+)$/;
  const INLINE_MD_RE = /(\[([^\]]+)\]\(([^)]+)\))|(`([^`]+)`)|(\*\*([^*]+)\*\*)|(\*([^*]+)\*)/g;
  const YES_RE = /^-yes-$/i;
  const NO_RE = /^-no-$/i;
  const LIMITED_RE = /^-(limited|partial)-$/i;
  const NA_RE = /^-(na|none)-$/i;
  const NA2_RE = /^-na2-$/i;
  const SIMPLE_TAG_RE = /(<br\s*\/?>)|(<p\s*\/?>)|(-note-)|(-warning-)/gi;
  const tryParseKV = trimmed => {
    const m = KV_RE.exec(trimmed);
    return m ? {
      key: m[1],
      value: m[2].trim()
    } : null;
  };
  const registerKey = (key, seenKeys, orderedKeys) => {
    if (!seenKeys.has(key)) {
      orderedKeys.push(key);
      seenKeys.add(key);
    }
  };
  const flushEntry = (currentEntry, entries) => {
    if (Object.keys(currentEntry).length > 0) entries.push(currentEntry);
  };
  const parseDashPrefixed = (lines, entries, orderedKeys, seenKeys) => {
    let currentEntry = {};
    let inEntry = false;
    for (const line of lines) {
      const trimmed = line.trim();
      if (trimmed.startsWith('- ')) {
        if (inEntry) entries.push(currentEntry);
        currentEntry = {};
        inEntry = true;
        const kv = tryParseKV(trimmed.substring(2).trim());
        if (kv) {
          registerKey(kv.key, seenKeys, orderedKeys);
          currentEntry[kv.key] = kv.value;
        }
      } else if (inEntry && trimmed !== '') {
        const kv = tryParseKV(trimmed);
        if (kv) {
          registerKey(kv.key, seenKeys, orderedKeys);
          currentEntry[kv.key] = kv.value;
        }
      }
    }
    flushEntry(currentEntry, entries);
  };
  const parseBlankSeparated = (lines, entries, orderedKeys, seenKeys) => {
    let currentEntry = {};
    let inEntry = false;
    for (const line of lines) {
      const trimmed = line.trim();
      if (trimmed === '') {
        if (inEntry) {
          flushEntry(currentEntry, entries);
          currentEntry = {};
          inEntry = false;
        }
        continue;
      }
      const kv = tryParseKV(trimmed);
      if (!kv) continue;
      const isNewEntry = !line.startsWith(' ') && !line.startsWith('\t');
      if (isNewEntry && inEntry && Object.keys(currentEntry).length > 0) {
        entries.push(currentEntry);
        currentEntry = {};
      }
      registerKey(kv.key, seenKeys, orderedKeys);
      currentEntry[kv.key] = kv.value;
      inEntry = true;
    }
    flushEntry(currentEntry, entries);
  };
  const normalizeEntries = (entries, orderedKeys) => entries.map(entry => {
    const filled = {};
    for (const key of orderedKeys) filled[key] = entry[key] || '';
    return filled;
  });
  const parseYamlTableContent = contentStr => {
    if (!contentStr) return [];
    const entries = [];
    const orderedKeys = [];
    const seenKeys = new Set();
    const lines = contentStr.split('\n');
    if (lines.some(line => line.trim().startsWith('- '))) {
      parseDashPrefixed(lines, entries, orderedKeys, seenKeys);
    } else {
      parseBlankSeparated(lines, entries, orderedKeys, seenKeys);
    }
    return normalizeEntries(entries, orderedKeys);
  };
  const processText = text => {
    if (!text) return text;
    const parts = [];
    let keyIndex = 0;
    let lastIndex = 0;
    let match;
    while ((match = INLINE_MD_RE.exec(text)) !== null) {
      if (match.index > lastIndex) parts.push(text.slice(lastIndex, match.index));
      if (match[1]) {
        parts.push(<a key={keyIndex++} href={match[3]}>{match[2]}</a>);
      } else if (match[4]) {
        parts.push(<code key={keyIndex++}>{match[5]}</code>);
      } else if (match[6]) {
        parts.push(<strong key={keyIndex++}>{match[7]}</strong>);
      } else if (match[8]) {
        parts.push(<em key={keyIndex++}>{match[9]}</em>);
      }
      lastIndex = match.index + match[0].length;
    }
    if (lastIndex < text.length) parts.push(text.slice(lastIndex));
    if (parts.length === 0) return text;
    const keyRef = {
      current: keyIndex
    };
    return expandHtmlTags(parts, keyRef);
  };
  const processBadges = text => {
    if (!text || typeof text !== 'string') return text;
    if (YES_RE.test(text)) return <span className="yt-badge-yes" role="img" aria-label="Supported" title="Supported">✓</span>;
    if (NO_RE.test(text)) return <span className="yt-badge-no" role="img" aria-label="Not supported" title="Not supported">✗</span>;
    if (LIMITED_RE.test(text)) return <span className="yt-badge-limited" role="img" aria-label="Partially supported" title="Partially supported">◐</span>;
    if (NA_RE.test(text) || NA2_RE.test(text)) return <span className="yt-sr-only" title="Not applicable">Not applicable</span>;
    return processText(text);
  };
  const cellClassName = text => {
    if (!text || typeof text !== 'string') return undefined;
    if (NA_RE.test(text)) return 'yt-cell-na';
    if (NA2_RE.test(text)) return 'yt-cell-na2';
    return undefined;
  };
  const expandSimpleTags = (str, keyRef) => {
    const result = [];
    let last = 0;
    SIMPLE_TAG_RE.lastIndex = 0;
    let m;
    while ((m = SIMPLE_TAG_RE.exec(str)) !== null) {
      if (m.index > last) result.push(str.slice(last, m.index));
      if (m[1]) {
        result.push(<br key={keyRef.current++} />);
      } else if (m[2]) {
        result.push(<br key={keyRef.current++} />, <br key={keyRef.current++} />);
      } else if (m[3]) {
        result.push(<span key={keyRef.current++} className="yt-badge-note" style={{
          fontWeight: 600
        }}>Note: </span>);
      } else if (m[4]) {
        result.push(<span key={keyRef.current++} className="yt-badge-warning" style={{
          fontWeight: 600
        }}>Warning: </span>);
      }
      last = m.index + m[0].length;
    }
    if (last < str.length) result.push(str.slice(last));
    return result;
  };
  const expandHtmlTags = (chunks, keyRef) => {
    const out = [];
    for (const chunk of chunks) {
      if (typeof chunk === 'string') {
        out.push(...expandSimpleTags(chunk, keyRef));
      } else {
        out.push(chunk);
      }
    }
    return out;
  };
  const extractText = node => {
    if (node === null || node === undefined) return '';
    if (typeof node === 'string') return node;
    if (typeof node === 'number') return String(node);
    if (typeof node === 'boolean') return '';
    if (Array.isArray(node)) return node.map(extractText).join('');
    if (node && typeof node === 'object' && node.type) {
      const props = node.props || ({});
      if (typeof props.children === 'string') return props.children;
      if (props.children) return extractText(props.children);
      return '';
    }
    return String(node || '');
  };
  const [mounted, setMounted] = useState(false);
  useEffect(() => {
    setMounted(true);
  }, []);
  const data = useMemo(() => {
    if (propData) return propData;
    if (content && typeof content === 'string') return parseYamlTableContent(content);
    if (!children) return [];
    if (typeof children === 'string') return parseYamlTableContent(children);
    const childrenArray = Array.isArray(children) ? children : [children];
    return parseYamlTableContent(childrenArray.map(extractText).join('').trim());
  }, [children, propData, content]);
  const columns = useMemo(() => {
    if (!data || data.length === 0) return [];
    const firstRow = data[0];
    if (!firstRow || typeof firstRow !== 'object') return [];
    return Object.keys(firstRow);
  }, [data]);
  if (!mounted) return null;
  if (!data || data.length === 0) return null;
  const rowKey = row => columns.map(c => row[c] || '').join('|');
  return <table>
      <thead>
        <tr>
          {columns.map(col => <th key={col}>{col.replaceAll('_', ' ')}</th>)}
        </tr>
      </thead>
      <tbody>
        {data.map(row => <tr key={rowKey(row)}>
            {columns.map(col => <td key={col} className={cellClassName(row[col])}>{processBadges(row[col])}</td>)}
          </tr>)}
      </tbody>
    </table>;
};

export const PackageFirewallMdmGenerator = ({owner = 'endorlabs', repo = 'mdm-scripts', branch = 'main', basePath = 'package-firewall'}) => {
  const CACHE_TTL = 60 * 60 * 1000;
  const rawUrl = rel => 'https://raw.githubusercontent.com/' + owner + '/' + repo + '/' + branch + '/' + basePath + '/' + rel;
  const FILES = {
    bash: {
      generate: 'bash/generate.sh',
      common: 'bash/lib/common.sh',
      envsh: 'bash/templates/envsh.sh',
      js: 'bash/templates/js.sh',
      python: 'bash/templates/python.sh',
      go: 'bash/templates/go.sh',
      maven: 'bash/templates/maven.sh',
      remove: 'bash/templates/remove.sh',
      bENVSH: 'shared/blocks/envsh.txt',
      bNPMRC: 'shared/blocks/npmrc.txt',
      bYARNC: 'shared/blocks/yarnrc_classic.txt',
      bYARN: 'shared/blocks/yarnrc.txt',
      bPIP: 'shared/blocks/pipconf.txt',
      bUV: 'shared/blocks/uvtoml.txt',
      bGO: 'shared/blocks/goenv.txt',
      bMAVEN: 'shared/blocks/mavensettings.txt'
    },
    powershell: {
      generate: 'powershell/generate.ps1',
      common: 'powershell/lib/common.ps1',
      header: 'powershell/templates/script-header.ps1',
      envvars: 'powershell/templates/envvars.ps1',
      js: 'powershell/templates/js.ps1',
      python: 'powershell/templates/python.ps1',
      go: 'powershell/templates/go.ps1',
      maven: 'powershell/templates/maven.ps1',
      remove: 'powershell/templates/remove.ps1',
      bNPMRC: 'shared/blocks/npmrc.txt',
      bYARNC: 'shared/blocks/yarnrc_classic.txt',
      bYARN: 'shared/blocks/yarnrc.txt',
      bPIP: 'shared/blocks/pipconf.txt',
      bUV: 'shared/blocks/uvtoml.txt',
      bGO: 'shared/blocks/goenv.txt',
      bMAVEN: 'shared/blocks/mavensettings.txt'
    }
  };
  const PINS = {
    bash: {
      generate: 'e704b2a7017157f602fd5cdd0c9b1e7aadb046c6',
      common: '42681063b1ad58d8d12a96cf59212addb5aaadfc'
    },
    powershell: {
      generate: '9f2fc74e6887928ed46696e6f095338b9a991972'
    }
  };
  const SHELLS = {
    bash: {
      label: 'Bash (macOS / Linux)',
      ext: 'sh',
      runAs: 'root'
    },
    powershell: {
      label: 'PowerShell (Windows)',
      ext: 'ps1',
      runAs: 'SYSTEM'
    }
  };
  const ECOSYSTEMS = [{
    value: 'all',
    label: 'All ecosystems'
  }, {
    value: 'js',
    label: 'JavaScript (npm, pnpm, yarn, bun)'
  }, {
    value: 'python',
    label: 'Python (pip, uv, poetry)'
  }, {
    value: 'go',
    label: 'Go (go modules)'
  }, {
    value: 'maven',
    label: 'Maven (settings.xml)'
  }, {
    value: 'remove',
    label: 'Remove (offboarding)'
  }];
  const BLOCK_META = {
    bENVSH: {
      label: 'Credentials block (env.sh)',
      shells: ['bash'],
      ecos: ['js', 'python', 'go', 'all']
    },
    bNPMRC: {
      label: 'npm / pnpm / yarn classic / bun (.npmrc)',
      shells: ['bash', 'powershell'],
      ecos: ['js', 'all']
    },
    bYARNC: {
      label: 'yarn classic (.yarnrc)',
      shells: ['bash', 'powershell'],
      ecos: ['js', 'all']
    },
    bYARN: {
      label: 'yarn 2+ berry (.yarnrc.yml)',
      shells: ['bash', 'powershell'],
      ecos: ['js', 'all']
    },
    bPIP: {
      label: 'pip (pip.conf / pip.ini)',
      shells: ['bash', 'powershell'],
      ecos: ['python', 'all']
    },
    bUV: {
      label: 'uv (uv.toml)',
      shells: ['bash', 'powershell'],
      ecos: ['python', 'all']
    },
    bGO: {
      label: 'Go (go env, GOPROXY)',
      shells: ['bash', 'powershell'],
      ecos: ['go', 'all']
    },
    bMAVEN: {
      label: 'Maven (~/.m2/settings.xml)',
      shells: ['bash', 'powershell'],
      ecos: ['maven', 'all']
    }
  };
  const MDM = {
    bash: {
      kandji: {
        label: 'Kandji',
        steps: ['Library → Custom Scripts → Add Script.', 'Paste the script content or upload the file.', 'Set Run as: Root.', 'Set Execution Frequency: Run once per device (or every check-in for ongoing enforcement).', 'Assign to the relevant device blueprint.']
      },
      jamf: {
        label: 'Jamf Pro',
        steps: ['Settings → Scripts → New; paste the script content.', 'Policies → New Policy → Scripts; add your script.', 'Set Execution Frequency as appropriate.', 'Scope to the target devices.']
      },
      generic: {
        label: 'Generic MDM',
        steps: ['Upload the script file.', 'Ensure it runs as root: the script detects the logged-in console user and writes config to the correct home directory.']
      }
    },
    powershell: {
      intune: {
        label: 'Microsoft Intune',
        steps: ['Devices → Scripts and remediations → Platform scripts → Add.', 'Upload the .ps1 file.', 'Run this script using the logged on credentials: No (runs as SYSTEM).', 'Enforce script signature check: No.', 'Run script in 64-bit PowerShell: Yes.', 'Assign to the target device group.']
      },
      generic: {
        label: 'Generic MDM',
        steps: ['Upload the script file.', 'Ensure it runs as SYSTEM: the script detects the logged-in console user via explorer.exe and writes to the correct user profile.']
      }
    }
  };
  const DEFAULT_TOOL = {
    bash: 'kandji',
    powershell: 'intune'
  };
  const [isDark, setIsDark] = useState(false);
  const [shell, setShell] = useState('bash');
  const [ecosystem, setEcosystem] = useState('all');
  const [mdmTool, setMdmTool] = useState('kandji');
  const [ns, setNs] = useState('');
  const [keyId, setKeyId] = useState('');
  const [secret, setSecret] = useState('');
  const [showSecret, setShowSecret] = useState(false);
  const [showCustomize, setShowCustomize] = useState(false);
  const [blockEdits, setBlockEdits] = useState({});
  const [sources, setSources] = useState({});
  const [status, setStatus] = useState('loading');
  const [copied, setCopied] = useState(false);
  const [expanded, setExpanded] = useState(false);
  const [drift, setDrift] = useState(null);
  useEffect(() => {
    const check = () => {
      const r = document.documentElement;
      setIsDark(r.dataset.theme === 'dark' || r.classList.contains('dark'));
    };
    check();
    const obs = new MutationObserver(check);
    obs.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ['data-theme', 'class']
    });
    return () => obs.disconnect();
  }, []);
  useEffect(() => {
    if (sources[shell]) {
      setDrift(null);
      setStatus('ready');
      return undefined;
    }
    let cancelled = false;
    const gitBlobSha1 = async text => {
      const enc = new TextEncoder();
      const body = enc.encode(text);
      const head = enc.encode('blob ' + body.length + '\u0000');
      const buf = new Uint8Array(head.length + body.length);
      buf.set(head, 0);
      buf.set(body, head.length);
      const digest = await crypto.subtle.digest('SHA-1', buf);
      return Array.from(new Uint8Array(digest)).map(b => b.toString(16).padStart(2, '0')).join('');
    };
    const verify = async map => {
      try {
        const pins = PINS[shell];
        for (const k of Object.keys(pins)) {
          if (typeof map[k] !== 'string') continue;
          const actual = await gitBlobSha1(map[k]);
          if (cancelled) return;
          if (actual !== pins[k]) {
            console.debug('pf-mdm: source drift on ' + FILES[shell][k], {
              expected: pins[k],
              actual
            });
            setDrift({
              file: FILES[shell][k]
            });
            setStatus('drift');
            return;
          }
        }
        if (cancelled) return;
        setDrift(null);
        setSources(p => ({
          ...p,
          [shell]: map
        }));
        setStatus('ready');
      } catch (e) {
        if (!cancelled) {
          console.debug('pf-mdm: verify failed', e);
          setStatus('error');
        }
      }
    };
    const cacheKey = 'pf-mdm-' + owner + '/' + repo + '@' + branch + ':' + shell;
    try {
      const raw = sessionStorage.getItem(cacheKey);
      if (raw) {
        const c = JSON.parse(raw);
        if (Date.now() - c.ts < CACHE_TTL) {
          verify(c.data);
          return () => {
            cancelled = true;
          };
        }
      }
    } catch (e) {
      console.debug('pf-mdm: cache read failed', e);
    }
    setStatus('loading');
    Promise.all(Object.entries(FILES[shell]).map(([k, rel]) => fetch(rawUrl(rel)).then(r => {
      if (!r.ok) throw new Error(rel + ': HTTP ' + r.status);
      return r.text();
    }).then(text => [k, text]))).then(pairs => {
      if (cancelled) return;
      const map = Object.fromEntries(pairs);
      try {
        sessionStorage.setItem(cacheKey, JSON.stringify({
          ts: Date.now(),
          data: map
        }));
      } catch (e) {
        console.debug('pf-mdm: cache write failed', e);
      }
      verify(map);
    }).catch(() => {
      if (!cancelled) setStatus('error');
    });
    return () => {
      cancelled = true;
    };
  }, [shell, owner, repo, branch, basePath]);
  const b64 = s => {
    try {
      return btoa(String.fromCodePoint(...new TextEncoder().encode(s)));
    } catch (e) {
      console.debug('pf-mdm: b64 failed', e);
      return '';
    }
  };
  const derive = (inp, shell) => {
    const {ns, keyId, secret, fqdn} = inp;
    let f = fqdn.trim() || 'https://factory.endorlabs.com';
    if (shell === 'powershell') f = f.replace(/\/+$/, '');
    const kid = keyId.trim();
    const sec = secret.trim();
    const host = f.replace(/^https?:\/\//, '');
    const trusted = host.replace(/:.*$/, '');
    const nsPath = '/v1/namespaces/' + ns + '/firewall/';
    const pip = 'https://' + kid + ':' + sec + '@' + host + nsPath + 'pypi/simple/';
    const goProxy = 'https://' + kid + ':' + sec + '@' + host + nsPath + 'go/,direct';
    const d = {
      NAMESPACE: ns,
      API_KEY_ID: kid,
      API_SECRET: sec,
      FQDN: f,
      NPM_REGISTRY_URL: f + nsPath + 'npm/',
      NPM_REGISTRY_HOST: host + nsPath + 'npm/',
      NPM_AUTH_B64: b64(kid + ':' + sec),
      API_SECRET_B64: b64(sec),
      PYPI_URL: f + nsPath + 'pypi/simple/',
      PIP_INDEX_URL: pip,
      TRUSTED_HOST: trusted,
      GO_PROXY_URL: goProxy,
      MAVEN_REGISTRY_URL: f + nsPath + 'maven/'
    };
    if (shell === 'bash') d.ENDOR_PYPI_URL = pip;
    return d;
  };
  const substitute = (text, d) => {
    let out = text;
    for (const k of Object.keys(d)) out = out.split('{{' + k + '}}').join(d[k]);
    return out;
  };
  const echo = s => s + '\n';
  const buildBash = (src, eco, d, ns) => {
    const gen = src.generate;
    const heredoc = name => gen.match(new RegExp((String.raw)`cat << '${name}'\n([\s\S]*?)\n${name}\n`))[1] + '\n';
    const ARGBLOCK = heredoc('ARGBLOCK');
    const FOOTERBLOCK = heredoc('FOOTERBLOCK');
    const USERBLOCK = heredoc('USERBLOCK');
    const SEP_EQ = gen.match(/echo "(# ═+)"/)[1];
    const SEP_DASH = gen.match(/echo "(# ─+)"/)[1];
    const HDR_COMMON = gen.match(/echo "(# ── Common functions[^"]*)"/)[1];
    const HDR_BLOCKS = gen.match(/echo "(# ── Block content[^"]*)"/)[1];
    const descJs = gen.match(/endor-js\.sh" \\\n\s*"([^"]*)"/)[1];
    const descPy = gen.match(/endor-python\.sh" \\\n\s*"([^"]*)"/)[1];
    const descAll = gen.match(/endor-all\.sh" \\\n\s*"([^"]*)"/)[1];
    const descRm = gen.match(/"(Removes Endor Package Firewall[^"]*)"/)[1];
    const descGo = gen.match(/endor-go\.sh" \\\n\s*"([^"]*)"/)[1];
    const descMaven = gen.match(/endor-maven\.sh" \\\n\s*"([^"]*)"/)[1];
    const doneAll = gen.match(/echo "echo \\"(\[endor\][^"]*)\\""/)[1].replace('${ENDOR_NAMESPACE}', ns);
    const inlineCommon = () => src.common.split('\n').filter(l => !l.startsWith('# ')).filter(l => !(/^[ \t]*$/).test(l)).join('\n') + '\n';
    const blockKey = {
      ENVSH_BLOCK: 'bENVSH',
      NPMRC_BLOCK: 'bNPMRC',
      YARNRC_CLASSIC_BLOCK: 'bYARNC',
      YARNRC_BLOCK: 'bYARN',
      PIP_BLOCK: 'bPIP',
      UV_BLOCK: 'bUV',
      GO_BLOCK: 'bGO',
      MAVEN_BLOCK: 'bMAVEN'
    };
    const emitBlock = v => {
      const delim = 'ENDOR_' + v;
      return echo(v + '=$(cat <<\'' + delim + '\'') + substitute(src[blockKey[v]], d) + echo('') + echo(delim) + echo(')');
    };
    const emitAllBlocks = () => echo(HDR_BLOCKS) + emitBlock('ENVSH_BLOCK') + emitBlock('NPMRC_BLOCK') + emitBlock('YARNRC_CLASSIC_BLOCK') + emitBlock('YARNRC_BLOCK') + emitBlock('PIP_BLOCK') + emitBlock('UV_BLOCK') + emitBlock('GO_BLOCK') + emitBlock('MAVEN_BLOCK') + echo(SEP_DASH) + echo('');
    const header = (outName, desc) => echo('#!/usr/bin/env bash') + echo('# MDM-deployable: ' + desc) + echo('# Generated for namespace=' + ns + ' fqdn=' + d.FQDN + '.') + echo('# Do not edit — regenerate with generate.sh.') + echo('# Usage: ' + outName + ' [--dry-run]') + echo('') + echo('set -euo pipefail') + echo('') + echo(HDR_COMMON) + inlineCommon() + echo(SEP_DASH) + echo('') + ARGBLOCK + echo('') + USERBLOCK + echo('');
    const tpl = k => substitute(src[k], d);
    const envSetup = () => echo(SEP_EQ) + echo('# Env setup') + echo(SEP_EQ);
    if (eco === 'remove') return header('endor-remove.sh', descRm) + tpl('remove');
    if (eco === 'all') {
      return header('endor-all.sh', descAll) + emitAllBlocks() + envSetup() + tpl('envsh') + echo('') + echo(SEP_EQ) + echo('# JavaScript') + echo(SEP_EQ) + tpl('js') + echo('') + echo(SEP_EQ) + echo('# Python') + echo(SEP_EQ) + tpl('python') + echo('') + echo(SEP_EQ) + echo('# Go') + echo(SEP_EQ) + tpl('go') + echo('') + echo(SEP_EQ) + echo('# Maven') + echo(SEP_EQ) + tpl('maven') + echo('') + echo('echo ""') + echo('echo "' + doneAll + '"') + echo('') + FOOTERBLOCK;
    }
    const desc = ({
      js: descJs,
      python: descPy,
      go: descGo,
      maven: descMaven
    })[eco];
    return header('endor-' + eco + '.sh', desc) + emitAllBlocks() + envSetup() + tpl('envsh') + echo('') + tpl(eco) + echo('') + FOOTERBLOCK;
  };
  const buildPs = (src, eco, d, ns) => {
    const gen = src.generate;
    const lit = (re, fallback) => {
      const m = gen.match(re);
      return m ? m[1] : fallback;
    };
    const SEP_ENV = lit(/('# == Env vars setup =+')/, "'# == Env vars setup ='").slice(1, -1);
    const SEP_JS = lit(/('# == JavaScript =+')/, "'# == JavaScript ='").slice(1, -1);
    const SEP_PY = lit(/('# == Python =+')/, "'# == Python ='").slice(1, -1);
    const SEP_GO = lit(/('# == Go =+')/, "'# == Go ='").slice(1, -1);
    const SEP_MAVEN = lit(/('# == Maven =+')/, "'# == Maven ='").slice(1, -1);
    const descJs = lit(/'(Configures JavaScript[^']*)'/, 'Configures JavaScript package managers (npm, pnpm, yarn, bun) for Endor Package Firewall.');
    const descPy = lit(/'(Configures Python[^']*)'/, 'Configures Python package managers (pip, uv, poetry) for Endor Package Firewall.');
    const descGo = lit(/'(Configures Go modules[^']*)'/, 'Configures Go modules (GOPROXY) for Endor Package Firewall.');
    const descMaven = lit(/'(Configures Maven[^']*)'/, 'Configures Maven for Endor Package Firewall.');
    const descAll = lit(/'(Configures all package managers[^']*)'/, 'Configures all package managers for Endor Package Firewall.');
    const descRm = lit(/'(Removes Endor Package Firewall[^']*)'/, 'Removes Endor Package Firewall configuration from all managed files and registry env vars.');
    const doneAll = lit(/Write-Host '(\[endor\] \[done\][^']*)'/, "[endor] [done] All package managers configured for $ENDOR_NAMESPACE.").replace('$ENDOR_NAMESPACE', ns);
    const header = (name, desc) => {
      let r = substitute(src.header, d);
      r = r.split('{{DESCRIPTION}}').join(desc).split('{{SCRIPTNAME}}').join(name).split('{{COMMON_CONTENT}}').join(src.common);
      return r;
    };
    const blockKey = {
      NPMRC_BLOCK: 'bNPMRC',
      YARNRC_CLASSIC_BLOCK: 'bYARNC',
      YARNRC_BLOCK: 'bYARN',
      PIP_BLOCK: 'bPIP',
      UV_BLOCK: 'bUV',
      GO_BLOCK: 'bGO',
      MAVEN_BLOCK: 'bMAVEN'
    };
    const blockAssign = v => '$' + v + " = @'\n" + substitute(src[blockKey[v]], d).replace(/\s+$/, '') + "\n'@\n";
    const allBlocks = () => ['# -- Block content (from shared/blocks/) --', blockAssign('NPMRC_BLOCK'), blockAssign('YARNRC_CLASSIC_BLOCK'), blockAssign('YARNRC_BLOCK'), blockAssign('PIP_BLOCK'), blockAssign('UV_BLOCK'), blockAssign('GO_BLOCK'), blockAssign('MAVEN_BLOCK'), '# --', ''].join('\n');
    const tpl = k => substitute(src[k], d);
    const join = parts => parts.join('\n') + '\n';
    if (eco === 'remove') return join([header('endor-remove.ps1', descRm), tpl('remove')]);
    if (eco === 'all') {
      return join([header('endor-all.ps1', descAll), allBlocks(), SEP_ENV, tpl('envvars'), '', SEP_JS, tpl('js'), '', SEP_PY, tpl('python'), '', SEP_GO, tpl('go'), '', SEP_MAVEN, tpl('maven'), '', "Write-Host ''", "Write-Host '" + doneAll + "'"]);
    }
    const desc = ({
      js: descJs,
      python: descPy,
      go: descGo,
      maven: descMaven
    })[eco];
    return join([header('endor-' + eco + '.ps1', desc), allBlocks(), SEP_ENV, tpl('envvars'), '', tpl(eco)]);
  };
  const needsCreds = ecosystem !== 'remove';
  const cleanNs = ns.replaceAll(/[^a-zA-Z0-9._-]/g, '');
  const validNs = cleanNs.length > 0 && cleanNs === ns;
  const validInputs = validNs && (!needsCreds || keyId.trim() && secret.trim());
  const result = useMemo(() => {
    const src = sources[shell];
    if (!src || !validInputs) return null;
    try {
      const d = derive({
        ns,
        keyId,
        secret,
        fqdn: ''
      }, shell);
      const effective = {
        ...src,
        ...blockEdits
      };
      return shell === 'bash' ? buildBash(effective, ecosystem, d, ns) : buildPs(effective, ecosystem, d, ns);
    } catch (e) {
      console.debug('pf-mdm: assemble failed', e);
      return null;
    }
  }, [sources, shell, ecosystem, ns, keyId, secret, validInputs, blockEdits]);
  const ext = SHELLS[shell].ext;
  const fileName = 'endor-' + ecosystem + '.' + ext;
  const handleCopy = () => {
    if (!result || !navigator.clipboard) return;
    navigator.clipboard.writeText(result).then(() => {
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    }).catch(e => {
      console.debug('pf-mdm: clipboard unavailable', e);
    });
  };
  const handleDownload = () => {
    if (!result) return;
    const blob = new Blob([result], {
      type: 'text/plain'
    });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = fileName;
    document.body.appendChild(a);
    a.click();
    a.remove();
    URL.revokeObjectURL(url);
  };
  const switchShell = s => {
    setShell(s);
    setMdmTool(DEFAULT_TOOL[s]);
  };
  const bg = isDark ? '#0d1117' : '#ffffff';
  const bgLight = isDark ? '#161b22' : '#f6f8fa';
  const text = isDark ? '#e6edf3' : '#1f2937';
  const muted = isDark ? 'rgba(230,237,243,0.7)' : 'rgba(31,41,55,0.7)';
  const GRAD = 'linear-gradient(to bottom, rgba(50,225,140,0.92), rgba(30,190,110,0.92))';
  const activeFg = isDark ? '#000' : '#fff';
  const secondaryBg = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.4)';
  const S = {
    card: {
      background: bg,
      border: '2px solid rgba(38,208,124,' + (isDark ? '0.4' : '0.5') + ')',
      borderRadius: '16px',
      padding: '1.25rem',
      color: text,
      boxShadow: '0 2px 8px rgba(38,208,124,0.08)'
    },
    group: {
      background: bgLight,
      borderRadius: '12px',
      padding: '0.75rem',
      border: '1px solid rgba(38,208,124,0.3)'
    },
    label: {
      display: 'block',
      fontWeight: 600,
      marginBottom: '0.25rem',
      color: text,
      fontSize: '0.8rem'
    },
    help: {
      fontSize: '0.7rem',
      color: muted,
      lineHeight: 1.3,
      marginTop: '0.25rem'
    },
    input: {
      padding: '0.5rem 0.75rem',
      border: '1px solid rgba(38,208,124,0.45)',
      borderRadius: '8px',
      fontSize: '0.85rem',
      background: bg,
      color: text,
      width: '100%',
      boxSizing: 'border-box',
      outline: 'none'
    },
    select: {
      padding: '0.5rem 0.75rem',
      border: '1px solid rgba(38,208,124,0.45)',
      borderRadius: '8px',
      fontSize: '0.85rem',
      background: bg,
      color: text,
      width: '100%',
      cursor: 'pointer',
      outline: 'none'
    },
    toggleBtn: (active, left) => ({
      flex: 1,
      padding: '0.5rem 0.75rem',
      border: 'none',
      borderLeft: left ? '1px solid rgba(38,208,124,0.45)' : 'none',
      cursor: 'pointer',
      fontSize: '0.85rem',
      fontWeight: 600,
      background: active ? GRAD : bgLight,
      color: active ? activeFg : text
    }),
    btn: primary => ({
      padding: '0.45rem 0.9rem',
      background: primary ? GRAD : secondaryBg,
      color: primary ? activeFg : text,
      border: '1.5px solid rgba(38,208,124,0.5)',
      borderRadius: '10px',
      cursor: 'pointer',
      fontSize: '0.8rem',
      fontWeight: 500
    }),
    pre: {
      background: bg,
      color: text,
      padding: '0.75rem',
      borderRadius: '6px',
      fontFamily: "'Monaco','Menlo','Ubuntu Mono',monospace",
      fontSize: '0.78rem',
      lineHeight: 1.45,
      overflowX: 'auto',
      margin: 0,
      whiteSpace: 'pre',
      border: '1px solid rgba(38,208,124,0.15)',
      maxHeight: expanded ? 'none' : '320px',
      overflowY: expanded ? 'auto' : 'hidden'
    },
    notice: {
      marginTop: '0.5rem',
      padding: '0.5rem 0.75rem',
      background: isDark ? '#3b2f0b' : '#fff8e6',
      border: '1px solid ' + (isDark ? '#7a5c00' : '#f5d98a'),
      borderRadius: '8px',
      color: isDark ? '#f5d98a' : '#7a5c00',
      fontSize: '0.78rem',
      lineHeight: 1.45
    },
    err: {
      padding: '0.75rem',
      background: isDark ? '#3b1111' : '#fef2f2',
      border: '1px solid ' + (isDark ? '#7f1d1d' : '#fecaca'),
      borderRadius: '8px',
      color: isDark ? '#fecaca' : '#7f1d1d',
      fontSize: '0.82rem',
      lineHeight: 1.5
    },
    code: {
      background: bgLight,
      padding: '0.1rem 0.35rem',
      borderRadius: '4px',
      fontSize: '0.8em',
      fontFamily: "'Monaco','Menlo',monospace"
    },
    textarea: {
      width: '100%',
      boxSizing: 'border-box',
      minHeight: '120px',
      padding: '0.5rem 0.6rem',
      border: '1px solid rgba(38,208,124,0.45)',
      borderRadius: '8px',
      background: bg,
      color: text,
      fontFamily: "'Monaco','Menlo','Ubuntu Mono',monospace",
      fontSize: '0.72rem',
      lineHeight: 1.4,
      resize: 'vertical',
      outline: 'none'
    }
  };
  const cloneDir = basePath + '/' + shell;
  const manualCmd = shell === 'bash' ? 'git clone https://github.com/' + owner + '/' + repo + '\ncd ' + repo + '/' + cloneDir + '\nENDOR_NAMESPACE=' + (cleanNs || '<namespace>') + ' ENDOR_API_KEY_ID=<key-id> ENDOR_API_SECRET=<secret> ./generate.sh' : 'git clone https://github.com/' + owner + '/' + repo + '\ncd ' + repo + '/' + cloneDir + '\n$env:ENDOR_NAMESPACE=\'' + (cleanNs || '<namespace>') + '\'; $env:ENDOR_API_KEY_ID=\'<key-id>\'; $env:ENDOR_API_SECRET=\'<secret>\'\n./generate.ps1';
  const toolList = Object.entries(MDM[shell]);
  const tool = MDM[shell][mdmTool] || toolList[0][1];
  let missingHint = 'Enter your namespace to generate the script.';
  if (ns && !validNs) {
    missingHint = 'Namespace can contain only letters, digits, dots, hyphens, and underscores.';
  } else if (validNs) {
    missingHint = 'Enter your API key ID and secret to generate the script.';
  } else if (needsCreds) {
    missingHint = 'Enter your namespace, API key ID, and secret to generate the script.';
  }
  const srcMap = sources[shell];
  const editableBlocks = needsCreds && srcMap ? Object.keys(BLOCK_META).filter(k => BLOCK_META[k].shells.includes(shell) && BLOCK_META[k].ecos.includes(ecosystem) && srcMap[k] !== undefined) : [];
  const editedCount = editableBlocks.filter(k => blockEdits[k] !== undefined && blockEdits[k] !== srcMap[k]).length;
  return <div className="not-prose" style={{
    margin: '1.5rem 0',
    fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif'
  }}>
      <div style={S.card}>
        <div style={{
    marginBottom: '0.75rem'
  }}>
          <h3 style={{
    margin: '0 0 0.25rem 0',
    fontWeight: 600,
    fontSize: '1.1rem',
    color: text
  }}>Package Firewall MDM Script Generator</h3>
          <p style={{
    margin: 0,
    fontSize: '0.85rem',
    color: muted,
    lineHeight: 1.4
  }}>Generate a self-contained script to push via MDM. Everything runs in your browser. Your credentials are never sent anywhere.</p>
        </div>

        {status === 'drift' && <div style={S.err}>
            <strong>This generator is temporarily unavailable.</strong> The Package Firewall scripts in <code style={S.code}>{owner}/{repo}</code> changed since this page was last validated{drift?.file ? ' (' + drift.file + ')' : ''}, so generation is disabled here to avoid producing an incorrect script. Generate it from the source repo instead:
            <pre style={{
    ...S.pre,
    marginTop: '0.5rem'
  }}>{manualCmd}</pre>
            <div style={{
    marginTop: '0.5rem'
  }}>Source and instructions: <a href={'https://github.com/' + owner + '/' + repo + '/tree/' + branch + '/' + basePath} target="_blank" rel="noreferrer" style={{
    color: 'inherit',
    fontWeight: 600
  }}>github.com/{owner}/{repo}</a></div>
          </div>}

        {status !== 'drift' && <div style={{
    display: 'grid',
    gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))',
    gap: '0.75rem',
    marginBottom: '0.75rem'
  }}>
          <div style={S.group}>
            <span style={S.label} id="pf-platform">Platform</span>
            <fieldset aria-labelledby="pf-platform" style={{
    margin: 0,
    padding: 0,
    display: 'flex',
    borderRadius: '8px',
    overflow: 'hidden',
    border: '1px solid rgba(38,208,124,0.45)'
  }}>
              <button type="button" aria-pressed={shell === 'bash'} onClick={() => switchShell('bash')} style={S.toggleBtn(shell === 'bash', false)}>macOS / Linux</button>
              <button type="button" aria-pressed={shell === 'powershell'} onClick={() => switchShell('powershell')} style={S.toggleBtn(shell === 'powershell', true)}>Windows</button>
            </fieldset>
            <div style={S.help}>{SHELLS[shell].label}. Scripts run as <strong>{SHELLS[shell].runAs}</strong>.</div>
          </div>

          <div style={S.group}>
            <span style={S.label} id="pf-eco">Ecosystem</span>
            <select aria-labelledby="pf-eco" value={ecosystem} onChange={e => setEcosystem(e.target.value)} style={S.select}>
              {ECOSYSTEMS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
            </select>
            <div style={S.help}>Which package managers to configure (or remove Endor config).</div>
          </div>

          <div style={S.group}>
            <span style={S.label} id="pf-tool">MDM tool</span>
            <select aria-labelledby="pf-tool" value={mdmTool} onChange={e => setMdmTool(e.target.value)} style={S.select}>
              {toolList.map(([k, v]) => <option key={k} value={k}>{v.label}</option>)}
            </select>
            <div style={S.help}>Determines the upload steps shown below.</div>
          </div>

          <div style={S.group}>
            <span style={S.label}>Namespace <span style={{
    color: '#d32f2f'
  }}>*</span></span>
            <input type="text" maxLength="100" aria-label="Namespace" aria-required="true" value={ns} onChange={e => setNs(e.target.value)} placeholder="your-namespace" style={S.input} />
            <div style={S.help}>Your Endor Labs namespace. Letters, digits, dots, hyphens, underscores.</div>
          </div>

          {needsCreds && <div style={S.group}>
              <span style={S.label}>API key<span style={{
    color: '#d32f2f'
  }}>*</span></span>
              <input type="text" autoComplete="off" aria-label="API key ID" aria-required="true" value={keyId} onChange={e => setKeyId(e.target.value)} placeholder="key id" style={S.input} />
              <div style={S.help}>Endor Labs API key ID.</div>
            </div>}

          {needsCreds && <div style={S.group}>
              <span style={S.label}>API secret <span style={{
    color: '#d32f2f'
  }}>*</span></span>
              <div style={{
    display: 'flex',
    gap: '0.35rem'
  }}>
                <input type={showSecret ? 'text' : 'password'} autoComplete="new-password" aria-label="API secret" aria-required="true" value={secret} onChange={e => setSecret(e.target.value)} placeholder="secret" style={S.input} />
                <button type="button" onClick={() => setShowSecret(v => !v)} style={{
    ...S.btn(false),
    flexShrink: 0
  }}>{showSecret ? 'Hide' : 'Show'}</button>
              </div>
              <div style={S.help}>Processed only in your browser and not sent to Endor Labs. The secret is baked into the script in plaintext. Treat the output as a secret.</div>
            </div>}
        </div>}

        {status === 'ready' && editableBlocks.length > 0 && <div style={{
    marginBottom: '0.75rem'
  }}>
            <button type="button" onClick={() => setShowCustomize(v => !v)} style={S.btn(false)}>{showCustomize ? '▾' : '▸'} Customize config blocks{editedCount ? ' (' + editedCount + ' edited)' : ''}</button>
            {showCustomize && <div style={{
    ...S.group,
    marginTop: '0.6rem'
  }}>
                <div style={S.help}>Edit what each Endor-managed block writes to its config file. Do not modify unless you know what you are doing. <code style={S.code}>{'${ENDOR_...}'}</code> refs resolve at runtime on the device.</div>
                {editableBlocks.map(k => <div key={k} style={{
    marginTop: '0.6rem'
  }}>
                    <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '0.5rem',
    marginBottom: '0.25rem'
  }}>
                      <span style={{
    ...S.label,
    marginBottom: 0
  }}>{BLOCK_META[k].label}</span>
                      {blockEdits[k] !== undefined && blockEdits[k] !== srcMap[k] && <button type="button" onClick={() => setBlockEdits(p => {
    const n = {
      ...p
    };
    delete n[k];
    return n;
  })} style={{
    ...S.btn(false),
    marginLeft: 'auto',
    padding: '0.2rem 0.5rem',
    fontSize: '0.72rem'
  }}>Reset</button>}
                    </div>
                    <textarea aria-label={'Customize ' + BLOCK_META[k].label} spellCheck={false} value={blockEdits[k] ?? srcMap[k]} onChange={e => setBlockEdits(p => ({
    ...p,
    [k]: e.target.value
  }))} style={S.textarea} />
                  </div>)}
              </div>}
          </div>}

        {status === 'loading' && <div style={{
    ...S.group,
    color: muted,
    fontStyle: 'italic'
  }}>Loading source from GitHub…</div>}

        {status === 'error' && <div style={S.err}>
            <strong>Couldn't read the generator from GitHub.</strong> Generate the script locally instead:
            <pre style={{
    ...S.pre,
    marginTop: '0.5rem'
  }}>{manualCmd}</pre>
          </div>}

        {status === 'ready' && !validInputs && <div style={{
    ...S.group,
    color: muted,
    fontSize: '0.82rem'
  }}>
            {missingHint}
          </div>}

        {status === 'ready' && validInputs && result && <div>
            <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '0.5rem',
    marginBottom: '0.4rem'
  }}>
              <span style={{
    fontWeight: 600,
    fontSize: '0.85rem',
    color: text
  }}>{fileName}</span>
              <button type="button" onClick={handleCopy} style={{
    ...S.btn(true),
    marginLeft: 'auto'
  }}>{copied ? '✓ Copied' : 'Copy'}</button>
              <button type="button" onClick={handleDownload} style={S.btn(false)}>Download</button>
            </div>
            <pre style={S.pre}>{result}</pre>
            <button type="button" onClick={() => setExpanded(v => !v)} style={{
    ...S.btn(false),
    marginTop: '0.4rem',
    width: '100%'
  }}>{expanded ? 'Show less' : 'Show full script'}</button>
            {needsCreds && <div style={S.notice}>⚠ This script contains your API key and secret in plaintext (required to write credentials at deploy time). Restrict access to the MDM policy and the downloaded file and do not commit it.</div>}
          </div>}

        {status === 'ready' && validInputs && !result && <div style={S.err}>
            <strong>Couldn't assemble the script from the current source.</strong> The generator in <code style={S.code}>{owner}/{repo}</code> may have changed in a way this page does not yet handle. Generate it locally instead:
            <pre style={{
    ...S.pre,
    marginTop: '0.5rem'
  }}>{manualCmd}</pre>
          </div>}

        {status === 'ready' && <div style={{
    ...S.group,
    marginTop: '0.75rem'
  }}>
            <span style={S.label}>Upload to {tool.label}</span>
            <ol style={{
    margin: '0.25rem 0 0 1.1rem',
    padding: 0,
    color: text,
    fontSize: '0.82rem',
    lineHeight: 1.5
  }}>
              {tool.steps.map(st => <li key={st} style={{
    marginBottom: '0.15rem'
  }}>{st}</li>)}
            </ol>
          </div>}
      </div>
    </div>;
};

export const PoetrySource = ({namespace}) => <></>;

export const authTarget_0 = "the deployment scripts"

<LicenseBadge sku="EL-OSS-FWAL" />

Mobile device management (MDM) deployment lets an IT administrator configure many developer machines at once. You generate a self-contained script, push it through your MDM tool, and it configures each machine's package managers to route installations through Package Firewall. Developers do nothing, and existing package-manager configuration is preserved.

The scripts cover the following ecosystems.

<YamlTable>
  {`
    - Ecosystem: JavaScript
    Package managers: npm, pnpm, yarn, and bun
    - Ecosystem: Go
    Package managers: Go modules (through GOPROXY)
    - Ecosystem: Python
    Package managers: pip, uv, and Poetry
    - Ecosystem: Maven
    Package managers: Maven, and Gradle when it reads ~/.m2/settings.xml
    `}
</YamlTable>

They are idempotent, so they are safe to re-run on every MDM check-in, and they have no runtime dependencies beyond the shell.

<Tip>
  The scripts are available in the [Endor Labs MDM scripts repository](https://github.com/endorlabs/mdm-scripts) under the [`package-firewall/` directory](https://github.com/endorlabs/mdm-scripts/tree/main/package-firewall).
</Tip>

## Before you begin

Create an API key dedicated to the Package Firewall so that {authTarget_0} can authenticate to it. You can create it through one of the following methods:

* Using the Endor Labs user interface, with the **Package Firewall User** role. See [API keys](/platform-administration/api-keys#create-an-api-key-through-the-endor-labs-user-interface) to learn more.
* Using endorctl, with the `SYSTEM_ROLE_PACKAGE_FIREWALL` role. Make sure to [install and configure endorctl](/developers-api/cli/install-and-configure) before you create the key.

To create the key using endorctl, run the following command and replace:

* `<namespace>` with your namespace.
* `<API key name>` with the name of the API key for the Package Firewall use case.
* `<YYYY-MM-DDTHH:MM:SSZ>` with the API key expiration in ISO 8601 UTC format, for example `2026-12-31T23:59:59Z`.

```bash theme={null}
export NAMESPACE="<namespace>"
export KEY_NAME="<API key name>"

endorctl api create -r APIKey -n "$NAMESPACE" --data '{
  "meta": { "name": "'"$KEY_NAME"'" },
  "spec": {
    "permissions": { "roles": ["SYSTEM_ROLE_PACKAGE_FIREWALL"] },
    "expiration_time": "<YYYY-MM-DDTHH:MM:SSZ>"
  },
  "propagate": true
}'
```

## Generate the MDM scripts

Clone the generator repository and run it with your namespace and Package Firewall credentials. Select your platform for the matching commands.

<Tip>
  You can also generate the scripts in the browser using the [MDM script generator](#generate-your-mdm-script-in-the-browser). The generator fetches the real source files from the Endor Labs MDM scripts repository ([https://github.com/endorlabs/mdm-scripts](https://github.com/endorlabs/mdm-scripts)). The GitHub repository is the authoritative source for the scripts.
</Tip>

<Tabs>
  <Tab title="macOS / Linux">
    1. **Clone the generator repository**: Clone the repository and change into the bash generator directory.

       ```bash theme={null}
       git clone https://github.com/endorlabs/mdm-scripts
       cd mdm-scripts/package-firewall/bash
       ```

    2. **Generate the scripts**: Pass your credentials as environment variables. This keeps them out of your shell history.

       ```bash theme={null}
       ENDOR_NAMESPACE=<namespace> \
       ENDOR_API_KEY_ID=<api-key-id> \
       ENDOR_API_SECRET=<api-secret> \
       ./generate.sh
       ```

       Alternatively, store the variables in a `.env` file, add `.env` to `.gitignore`, and source it.

       ```bash theme={null}
       set -a; source .env; set +a
       ./generate.sh
       ```

    The generator writes `endor-js.sh`, `endor-python.sh`,`endor-go.sh`, `endor-maven.sh`, `endor-all.sh`, and `endor-remove.sh` to `out/<namespace>/`. Re-running `generate.sh` overwrites the same directory.
  </Tab>

  <Tab title="Windows">
    1. **Clone the generator repository**: Clone the repository and change into the PowerShell generator directory.

       ```powershell theme={null}
       git clone https://github.com/endorlabs/mdm-scripts
       cd mdm-scripts/package-firewall/powershell
       ```

    2. **Generate the scripts**: Set your credentials as environment variables, then run the generator.

       ```powershell theme={null}
       $env:ENDOR_NAMESPACE  = '<namespace>'
       $env:ENDOR_API_KEY_ID = '<api-key-id>'
       $env:ENDOR_API_SECRET = '<api-secret>'
       ./generate.ps1
       ```

    The generator writes `endor-js.ps1`, `endor-python.ps1`, `endor-go.ps1`, `endor-maven.ps1`, `endor-all.ps1`, and `endor-remove.ps1` to `out/<namespace>/`. Re-running `generate.ps1` overwrites the same directory.
  </Tab>
</Tabs>

<Warning>
  The generated scripts contain your API key and secret in plaintext, because the scripts need them to write credentials on each device. Treat the scripts, and the MDM policy that holds them, as secrets. Restrict access to the policy, add the `out/` directory to `.gitignore`, and rotate your API key if a script is exposed.
</Warning>

## Upload the scripts to your MDM tool

Each generated script is self-contained with no runtime dependencies, so you upload it directly to your MDM tool. On macOS and Linux the scripts use the `.sh` extension; on Windows they use `.ps1`.

<Tabs>
  <Tab title="macOS / Linux">
    Run the scripts as `root`. The script detects the logged-in console user and writes configuration files to the correct home directory.

    <AccordionGroup>
      <Accordion title="Kandji">
        1. **Add the script**: Go to **Library** > **Custom Scripts** > **Add Script**.
        2. **Provide the script**: Paste the script content or upload the file.
        3. **Set the run context**: Set **Run as** to **Root**.
        4. **Set the frequency**: Set **Execution Frequency** to **Run once per device**, or to every check-in for ongoing enforcement.
        5. **Assign the script**: Assign the script to the relevant device blueprint.
      </Accordion>

      <Accordion title="Jamf Pro">
        1. **Add the script**: Go to **Settings** > **Scripts** > **New** and paste the script content.
        2. **Create a policy**: Go to **Policies** > **New Policy** > **Scripts** and add your script.
        3. **Set the frequency**: Set the **Execution Frequency** as appropriate.
        4. **Scope the policy**: Scope the policy to the target devices.
      </Accordion>

      <Accordion title="Generic MDM">
        Upload the script file and run it as **root**. The script detects the logged-in console user and writes configuration files to the correct home directory.
      </Accordion>
    </AccordionGroup>
  </Tab>

  <Tab title="Windows">
    Run the scripts as **SYSTEM**. The script detects the logged-in console user through `explorer.exe` and writes configuration files to the correct user profile.

    <AccordionGroup>
      <Accordion title="Microsoft Intune">
        1. **Add the script**: Go to **Devices** > **Scripts and remediations** > **Platform scripts** > **Add**.
        2. **Upload the file**: Upload the `.ps1` file.
        3. **Set the run context**: Set **Run this script using the logged on credentials** to **No**, so the script runs as SYSTEM.
        4. **Set the signature check**: Set **Enforce script signature check** to **No**.
        5. **Set the PowerShell architecture**: Set **Run script in 64-bit PowerShell** to **Yes**.
        6. **Assign the script**: Assign the script to the target device group.

        <Note>
          Intune bypasses the execution policy for managed scripts, so you do not need to change the device policy.
        </Note>
      </Accordion>

      <Accordion title="Generic MDM">
        Upload the script file and run it as **SYSTEM**. The script detects the logged-in console user through `explorer.exe` and writes configuration files to the correct user profile.
      </Accordion>
    </AccordionGroup>
  </Tab>
</Tabs>

## How credentials are stored

The scripts write your credentials to a single source on each device and reference them from the package-manager configuration files. The storage mechanism depends on your platform.

<Tabs>
  <Tab title="macOS / Linux">
    The scripts write all credentials to `~/.config/endor/env.sh` and set the file permissions to `600`.

    ```bash theme={null}
    export ENDOR_API_KEY_ID="..."
    export ENDOR_API_SECRET="..."
    export ENDOR_AUTH_B64="..."          # base64(key:secret) for npm, pnpm, yarn, and bun
    export ENDOR_NPM_REGISTRY_URL="..."  # for npm and yarn 2+
    export ENDOR_PYPI_URL="..."          # for uv
    export ENDOR_GO_PROXY_URL="..."      # GOPROXY URL (also written literally to the go env file)
    export POETRY_HTTP_BASIC_ENDOR_FIREWALL_USERNAME="..."
    export POETRY_HTTP_BASIC_ENDOR_FIREWALL_PASSWORD="..."
    ```

    Each shell profile (`.zshrc`, `.bash_profile`, and `.bashrc`) gets a one-line block that sources this file. The configuration files reference these variables instead of embedding credentials, except `pip.conf` and the go env file, which cannot expand variables.

    To rotate credentials, redeploy the MDM script to update `env.sh` on the target machines. No configuration file changes are needed.
  </Tab>

  <Tab title="Windows">
    The scripts write persistent user-level environment variables to the `HKCU:\Environment` registry key.

    ```text theme={null}
    ENDOR_API_KEY_ID                          = <key-id>
    ENDOR_API_SECRET                          = <secret>
    ENDOR_AUTH_B64                            = <base64(key-id:secret)>
    ENDOR_NPM_REGISTRY_URL                    = https://factory.endorlabs.com/v1/namespaces/<namespace>/firewall/npm/
    ENDOR_PYPI_URL                            = https://<key-id>:<secret>@factory.endorlabs.com/v1/namespaces/<namespace>/firewall/pypi/simple/
    ENDOR_GO_PROXY_URL                        = https://<key-id>:<secret>@factory.endorlabs.com/v1/namespaces/<namespace>/firewall/go/,direct
    POETRY_HTTP_BASIC_ENDOR_FIREWALL_USERNAME = <key-id>
    POETRY_HTTP_BASIC_ENDOR_FIREWALL_PASSWORD = <secret>
    ```

    Every process the user starts inherits `HKCU:\Environment` variables, including Makefiles, git hooks, IDE terminals, and scheduled tasks. No shell profile sourcing is required.

    To rotate credentials, redeploy the MDM script. It updates `HKCU:\Environment`, plus `pip.ini` and the go env file, which hold literal credentials, in place.
  </Tab>
</Tabs>

## What the scripts do

Each install script writes the credential store and an Endor-managed block to the package-manager configuration files. The file locations depend on your platform.

<Tabs>
  <Tab title="macOS / Linux">
    The `endor-js.sh` script writes `~/.config/endor/env.sh` and an Endor-managed block to the following files.

    <YamlTable>
      {`
              - File: \`~/.npmrc\`
                Covers: npm (all), pnpm (8–11.x), yarn classic (1.x), bun
                Credentials: References \`ENDOR_AUTH_B64\`
              - File: \`~/.yarnrc.yml\`
                Covers: yarn 2+ (berry)
                Credentials: References \`ENDOR_API_KEY_ID\` and \`ENDOR_API_SECRET\`
              `}
    </YamlTable>

    The `endor-go.sh` script writes `~/.config/endor/env.sh` and an Endor-managed block to the following file.

    <YamlTable>
      {`
              - File: \`~/Library/Application Support/go/env\` (macOS), \`~/.config/go/env\` (Linux)
                Covers: go modules (all versions)
                Credentials: Literal
              `}
    </YamlTable>

    The `endor-python.sh` script writes `~/.config/endor/env.sh` and an Endor-managed block to the following files.

    <YamlTable>
      {`
              - File: \`~/.pip/pip.conf\`
                Covers: pip (legacy path)
                Credentials: Literal
              - File: \`~/.config/pip/pip.conf\`
                Covers: pip (XDG and Linux standard)
                Credentials: Literal
              - File: \`~/Library/Application Support/pip/pip.conf\`
                Covers: pip (macOS primary)
                Credentials: Literal
              - File: \`~/.config/uv/uv.toml\`
                Covers: uv (does not read pip.conf)
                Credentials: References \`ENDOR_PYPI_URL\`
              `}
    </YamlTable>

    The `endor-maven.sh` script writes `~/.config/endor/env.sh` and an Endor-managed block to the following file.

    <YamlTable>
      {`
              - File: \`~/.m2/settings.xml\`
                Covers: Maven (all versions), and Gradle when it reads ~/.m2
                Credentials: References \`ENDOR_API_KEY_ID\` and \`ENDOR_API_SECRET\`
              `}
    </YamlTable>
  </Tab>

  <Tab title="Windows">
    The `endor-js.ps1` script writes the registry environment variables and an Endor-managed block to the following files.

    <YamlTable>
      {`
              - File: \`%USERPROFILE%\\.npmrc\`
                Covers: npm (all), pnpm (8–11.x), yarn classic (1.x), bun
                Credentials: References \`ENDOR_AUTH_B64\`
              - File: \`%USERPROFILE%\\.yarnrc.yml\`
                Covers: yarn 2+ (berry, v3.1+)
                Credentials: References \`ENDOR_API_KEY_ID\` and \`ENDOR_API_SECRET\`
              `}
    </YamlTable>

    The `endor-go.ps1` script writes the registry environment variables and an Endor-managed block to the following file.

    <YamlTable>
      {`
              - File: \`%APPDATA%\\go\\env\` (path from \`go env GOENV\`)
                Covers: go modules (all versions)
                Credentials: Literal
              `}
    </YamlTable>

    The `endor-python.ps1` script writes the registry environment variables and an Endor-managed block to the following files.

    <YamlTable>
      {`
              - File: \`%APPDATA%\\pip\\pip.ini\`
                Covers: pip
                Credentials: Literal
              - File: \`%APPDATA%\\uv\\uv.toml\`
                Covers: uv
                Credentials: References \`ENDOR_PYPI_URL\`
              `}
    </YamlTable>

    The `endor-maven.ps1` script writes the registry environment variables and an Endor-managed block to the following file.

    <YamlTable>
      {`
              - File: \`%USERPROFILE%\\.m2\\settings.xml\`
                Covers: Maven (all versions), and Gradle when it reads ~/.m2
                Credentials: References \`ENDOR_API_KEY_ID\` and \`ENDOR_API_SECRET\`
              `}
    </YamlTable>
  </Tab>
</Tabs>

### How each package manager is configured

The scripts apply a few package-manager-specific behaviors that are the same on every platform.

* JavaScript: The scripts write `_auth` (base64) instead of `_authToken`, which bun requires. Yarn classic reads authentication from `.npmrc`, so the `.npmrc` write covers it. The scripts do not write the project-level `bunfig.toml`.
* Go: The scripts resolve the go env file path with `go env GOENV`, then write `GOPROXY` to it. If `go` is not installed, they fall back to the OS default path (`~/Library/Application Support/go/env` on macOS, `~/.config/go/env` on Linux, or `%APPDATA%\go\env` on Windows). Credentials are literal because go env files cannot expand environment variables. The `GOPROXY` value ends in `,direct`, so Go downloads a module directly from its source when the firewall does not serve it. The go env file applies to every `go` command regardless of shell, and it has lower precedence than the `GOPROXY` process variable, so project-level overrides still work.
* Python (pip): The scripts use a named `[endor-firewall]` section, so they preserve any existing `[global]` settings. Credentials are literal because pip cannot expand environment variables.
* Python (uv): uv ignores `pip.conf`, so the scripts write the user-level `uv.toml`, which references the `ENDOR_PYPI_URL` variable.
* Maven: The scripts write a user-level `settings.xml` (`~/.m2/settings.xml`, or `%USERPROFILE%\.m2\settings.xml` on Windows) with a `<mirror>` of `*` that points at the firewall, plus a matching `<server>`. Credentials are not baked into the file. The `<server>` references `${env.ENDOR_API_KEY_ID}` and `${env.ENDOR_API_SECRET}`, which Maven resolves at runtime from the environment variables the credential store already sets. Gradle uses this too when it reads `~/.m2/settings.xml`.
* Python (poetry): poetry reads credentials from the `POETRY_HTTP_BASIC_ENDOR_FIREWALL_*` environment variables, so no separate write step is needed. Add the source to your `pyproject.toml`:

Poetry reads the registry URL from `pyproject.toml` and the credentials from environment variables. Add the Package Firewall as a source in each project. The source includes the URL only, never the credentials.

```toml theme={null}
[[tool.poetry.source]]
name = "endor-firewall"
url = "https://factory.endorlabs.com/v1/namespaces/<namespace>/firewall/pypi/simple/"
priority = "primary"
```

## Preserve existing configuration

The scripts use a sentinel block pattern. Each script writes only a clearly delimited section to a configuration file and leaves everything else untouched, so existing settings survive every deployment.

The following example shows an `.npmrc` file that already contains administrator settings. The script adds only the Endor-managed block between the `BEGIN` and `END` markers.

```ini theme={null}
# Existing configuration — never touched
legacy-peer-deps=true

# ===== BEGIN ENDOR PACKAGE FIREWALL (managed — do not edit) =====
registry=https://factory.endorlabs.com/v1/namespaces/<namespace>/firewall/npm/
always-auth=true
//factory.endorlabs.com/v1/namespaces/<namespace>/firewall/npm/:_auth=${ENDOR_AUTH_B64}
# ===== END ENDOR PACKAGE FIREWALL =====
```

The following table describes how the scripts handle each configuration scenario.

<YamlTable>
  {`
    - Scenario: Fresh machine
    Behavior: The script creates the file with the Endor block only.
    - Scenario: Existing file, no Endor block
    Behavior: The script appends the Endor block and preserves existing content.
    - Scenario: Re-run or MDM check-in
    Behavior: The script replaces only the Endor block and leaves the rest of the file untouched.
    - Scenario: Administrator edits outside the block
    Behavior: The script preserves the changes.
    - Scenario: Administrator edits inside the block
    Behavior: The next MDM push overwrites the changes because the block is Endor-managed.
    - Scenario: Conflicting key outside the block
    Behavior: The script emits a warning to the MDM log so an administrator can resolve it manually.
    `}
</YamlTable>

## Remove the configuration

To offboard a machine, deploy the remove script. It strips the Endor block from every managed file and removes the credentials the install scripts added.

<Tabs>
  <Tab title="macOS / Linux">
    Deploy `endor-remove.sh`. It removes the Endor block from each configuration file and removes the credentials from `~/.config/endor/env.sh`.
  </Tab>

  <Tab title="Windows">
    Deploy `endor-remove.ps1`. It removes the Endor block from each configuration file, deletes the `ENDOR_*` and `POETRY_HTTP_BASIC_ENDOR_FIREWALL_*` values from `HKCU:\Environment`, and deletes configuration files that are empty after block removal. Preview the changes with `-DryRun` before you apply them.

    ```powershell theme={null}
    .\endor-remove.ps1 -DryRun
    .\endor-remove.ps1
    ```
  </Tab>
</Tabs>

## Security notes

The scripts store credentials on each device. Review the following before you deploy.

<YamlTable>
  {`
    - Item: \`~/.config/endor/env.sh\` (macOS and Linux)
    Note: Contains all credentials in plaintext, with file permissions set to \`600\`. This is the only credential file for npm, uv, yarn, and poetry.
    - Item: \`HKCU:\\Environment\` (Windows)
    Note: Contains credentials as plain registry strings. Default Windows access control restricts them to the owning user.
    - Item: \`pip.conf\` and \`pip.ini\`
    Note: Contain credentials in the index URL because pip cannot expand environment variables. The scripts restrict the file to the owner, and credentials can appear in verbose pip logs.
    - Item: Go env file
    Note: Contains credentials in the \`GOPROXY\` URL because go env files cannot expand environment variables. The scripts restrict the file to the owner.
    - Item: \`.npmrc\`, \`.yarnrc.yml\`, and \`uv.toml\`
    Note: Contain environment variable references only, with no credentials baked in.
    - Item: Shell profiles
    Note: Contain a single line that sources \`env.sh\`, with no credentials.
    - Item: Generated scripts
    Note: Contain the API key and secret in plaintext. Restrict access to the MDM policy and the generated \`out/\` directory, and never commit them to source control.
    `}
</YamlTable>

## Generate your MDM script in the browser

Select your platform, ecosystem, and MDM tool, then enter your namespace and API key. The script is generated entirely in your browser: your credentials are never sent to Endor Labs or GitHub, only baked into the script you download.

<PackageFirewallMdmGenerator />

<Warning>
  The generated script contains your API key and secret in plaintext, because they are needed to write credentials on each device. Treat the script, and the MDM policy that holds it, as secrets. Restrict who can view the policy, add generated scripts to `.gitignore`, and rotate your API key if a script is exposed.
</Warning>
