> ## 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.

# Configure the Package Firewall with direct integration

> <Badge color="green">Beta</Badge> <br /> Configure a direct integration to route package installation requests through the Package Firewall without a private package registry. Includes an interactive package manager configuration wizard.

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 renderSuccessIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" aria-hidden="true">
      <path d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM10 17L5 12L6.41 10.59L10 14.17L17.59 6.58L19 8L10 17Z" fill="currentColor" />
    </svg>;
  const renderFailureIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" aria-hidden="true">
      <path fillRule="evenodd" clipRule="evenodd" d="M11.9902 1.98633C12.5208 1.9864 13.0426 2.12743 13.501 2.39453C13.9581 2.66107 14.3372 3.04388 14.5986 3.50391L22.5967 17.5L22.6895 17.6738C22.8921 18.0851 22.9979 18.5387 22.9981 18.999C22.9981 19.5253 22.8605 20.0431 22.5977 20.499C22.3348 20.9549 21.9555 21.3332 21.5 21.5967C21.0445 21.8601 20.5272 21.9994 20.001 22H4.00001C3.47453 22.0031 2.95699 21.868 2.50001 21.6084C2.04032 21.3471 1.65712 20.9684 1.39063 20.5117C1.12431 20.055 0.983643 19.5355 0.982429 19.0068C0.981281 18.4793 1.11967 17.9611 1.38282 17.5039L9.38184 3.50391C9.64344 3.04359 10.023 2.66111 10.4805 2.39453C10.9388 2.12755 11.4598 1.98636 11.9902 1.98633ZM12 16.9004C11.3925 16.9004 10.9004 17.3925 10.9004 18C10.9004 18.6075 11.3925 19.0996 12 19.0996H12.0098C12.6173 19.0996 13.1104 18.6075 13.1104 18C13.1104 17.3925 12.6173 16.9004 12.0098 16.9004H12ZM12 5.90039C11.3925 5.9004 10.9004 6.39249 10.9004 7V13C10.9004 13.6075 11.3925 14.0996 12 14.0996C12.6075 14.0996 13.0996 13.6075 13.0996 13V7C13.0996 6.39249 12.6075 5.90039 12 5.90039Z" fill="currentColor" />
    </svg>;
  const renderPartialSuccessIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" aria-hidden="true">
      <path d="M12 1C18.0751 1 23 5.92487 23 12C23 18.0751 18.0751 23 12 23C5.92487 23 1 18.0751 1 12C1 5.92487 5.92487 1 12 1ZM12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3ZM12 5C13.8565 5 15.6374 5.73705 16.9502 7.0498C18.263 8.36256 19 10.1435 19 12C19 13.8565 18.263 15.6374 16.9502 16.9502C15.6374 18.263 13.8565 19 12 19C11.4477 19 11 18.5523 11 18V6C11 5.73478 11.1054 5.4805 11.293 5.29297C11.4805 5.10543 11.7348 5 12 5Z" fill="currentColor" />
    </svg>;
  const renderPendingIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
      <path d="M10.1 2.182a10 10 0 0 1 3.8 0" />
      <path d="M13.9 21.818a10 10 0 0 1-3.8 0" />
      <path d="M17.609 3.721a10 10 0 0 1 2.69 2.7" />
      <path d="M2.182 13.9a10 10 0 0 1 0-3.8" />
      <path d="M20.279 17.609a10 10 0 0 1-2.7 2.69" />
      <path d="M21.818 10.1a10 10 0 0 1 0 3.8" />
      <path d="M3.721 6.391a10 10 0 0 1 2.7-2.69" />
      <path d="M6.391 20.279a10 10 0 0 1-2.69-2.7" />
    </svg>;
  const renderRunningIcon = () => <svg className="yt-status-running-svg" viewBox="22 22 44 44" width="100%" height="100%" aria-hidden="true">
      <circle className="yt-status-running-circle" cx="44" cy="44" r="20.2" fill="none" stroke="currentColor" strokeWidth="3.6" />
    </svg>;
  const renderSkippedIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <circle cx="12" cy="12" r="10" />
      <path d="M8 12h8" />
    </svg>;
  const STATUS_ICON_DEFS = [{
    re: /^-status-success-$/i,
    colorClass: 'yt-status-color-success',
    label: 'Success',
    render: renderSuccessIcon
  }, {
    re: /^-status-failure-$/i,
    colorClass: 'yt-status-color-failure',
    label: 'Failure',
    render: renderFailureIcon
  }, {
    re: /^-status-partial-success-$/i,
    colorClass: 'yt-status-color-partial',
    label: 'Partial success',
    render: renderPartialSuccessIcon
  }, {
    re: /^-status-pending-$/i,
    colorClass: 'yt-status-color-neutral',
    label: 'Pending',
    render: renderPendingIcon
  }, {
    re: /^-status-running-$/i,
    colorClass: 'yt-status-color-neutral',
    label: 'Running',
    render: renderRunningIcon
  }, {
    re: /^-status-skipped-$/i,
    colorClass: 'yt-status-color-neutral',
    label: 'Skipped',
    render: renderSkippedIcon
  }];
  const renderStatusBadge = def => <span className={`yt-status-icon ${def.colorClass}`} role="img" aria-label={def.label} title={def.label}>
      {def.render()}
    </span>;
  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>;
    const statusBadge = STATUS_ICON_DEFS.find(def => def.re.test(text));
    if (statusBadge) return renderStatusBadge(statusBadge);
    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);
  const scrollWrapRef = useRef(null);
  const [isScrollable, setIsScrollable] = 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]);
  useEffect(() => {
    const wrap = scrollWrapRef.current;
    if (!wrap) return undefined;
    const updateScrollable = () => {
      setIsScrollable(wrap.scrollWidth > wrap.clientWidth);
    };
    updateScrollable();
    const observer = new ResizeObserver(updateScrollable);
    observer.observe(wrap);
    return () => observer.disconnect();
  }, [mounted, data]);
  if (!mounted) return null;
  if (!data || data.length === 0) return null;
  const rowKey = row => columns.map(c => row[c] || '').join('|');
  return <div ref={scrollWrapRef} style={{
    overflowX: 'auto',
    margin: '1.75rem 0'
  }} role={isScrollable ? 'region' : undefined} aria-label={isScrollable ? 'Scrollable table' : undefined} tabIndex={isScrollable ? 0 : undefined}>
      <table style={{
    display: 'table',
    width: '100%',
    minWidth: '100%',
    margin: 0
  }}>
        <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>
    </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 PackageFirewallWizard = ({defaultNamespace = ''}) => {
  const COPY_FEEDBACK_TIMEOUT_MS = 2000;
  const NAMESPACE_MAX = 100;
  const NAMESPACE_TOKEN = '{{namespace}}';
  const DEFAULT_NAMESPACE_LABEL = '<namespace>';
  const GROUP_JS = 'JavaScript';
  const GROUP_PY = 'Python';
  const GROUP_GO = 'Go';
  const GROUP_JAVA = 'Java';
  const GROUP_ORDER = [GROUP_JS, GROUP_PY, GROUP_GO, GROUP_JAVA];
  const REPLACE_SAVED = 'Replace `<api-key>` and `<api-secret>` with the credentials you saved in the [previous step](#create-an-api-key-for-the-package-firewall).';
  const YARN_CLASSIC_NOTE = 'Yarn Classic reads credentials from `.npmrc`. Just the `.yarnrc` file alone fails with a `401 Unauthorized` error.';
  const POETRY_NAME_NOTE = 'Ensure to use hyphens in the source name. Poetry derives the credential environment-variable name from it, so `endor-firewall` becomes `ENDOR_FIREWALL`.';
  const NPM_LOCKFILE_NOTE = 'npm writes the Package Firewall URL, including your namespace, into `package-lock.json`, so `npm ci` fails in CI without credentials. To install from the lockfile in CI/CD, see [Configure npm for CI/CD](/package-firewall/direct-integration#configure-npm-for-ci/cd).';
  const PNPM_LOCKFILE_NOTE = 'pnpm uses the Package Firewall registry in `.npmrc`. Set the same CI credentials as npm. To install in CI/CD, see [Configure pnpm for CI/CD](/package-firewall/direct-integration#configure-pnpm-for-ci/cd).';
  const POETRY_LOCKFILE_NOTE = 'Poetry writes the Package Firewall URL, including your namespace, into `poetry.lock`, so `poetry install` fails in CI without credentials. To install from the lockfile in CI/CD, see [Configure Poetry for CI/CD](/package-firewall/direct-integration#configure-poetry-for-ci/cd).';
  const BASE64_ENCODE_CMD = 'echo -n "<api-key>:<api-secret>" | base64';
  const BASE64_ENCODE_TEXT = 'Run the following command in your terminal to encode your API key and API secret as a Base64 string. Replace `<api-key>` and `<api-secret>` with the credentials you saved in the [previous step](#create-an-api-key-for-the-package-firewall).';
  const NPMRC_BASE64_TEXT = 'Add the following lines to your `.npmrc` file at the project level, or in the user-level file at `~/.npmrc`. Replace `<base64-credentials>` with the Base64 string you generated.';
  const NPMRC_BASE64_BLOCK = 'registry=https://factory.endorlabs.com/v1/namespaces/{{namespace}}/firewall/npm/\n' + '//factory.endorlabs.com/v1/namespaces/{{namespace}}/firewall/npm/:_auth=<base64-credentials>\n' + 'always-auth=true';
  const WIN_NPMRC_URL_TEXT = 'Add the following line to the `.npmrc` file in your project root, or to your user-level `.npmrc` at `%USERPROFILE%\\.npmrc`, for example `C:\\Users\\johndoe\\.npmrc`. Replace `<api-key>` and `<api-secret>` with the credentials you saved in the [previous step](#create-an-api-key-for-the-package-firewall).';
  const WIN_NPMRC_URL_CODE = 'registry=https://<api-key>:<api-secret>@factory.endorlabs.com/v1/namespaces/{{namespace}}/firewall/npm/';
  const WIN_CLI_ALT_TEXT = 'Alternatively, you can run the following command in your terminal to configure the registry URL.';
  const MAVEN_SETTINGS_XML = '<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"\n' + '          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n' + '          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0\n' + '              https://maven.apache.org/xsd/settings-1.2.0.xsd">\n' + '  <mirrors>\n' + '    <mirror>\n' + '      <id>endor-firewall</id>\n' + '      <name>Endor Package Firewall — <project-name></name>\n' + '      <mirrorOf>central</mirrorOf>\n' + '      <url>${env.ENDOR_FIREWALL_MVN_URL}</url>\n' + '    </mirror>\n' + '  </mirrors>\n' + '  <servers>\n' + '    <server>\n' + '      <id>endor-firewall</id>\n' + '      <username>${env.ENDOR_API_KEY}</username>\n' + '      <password>${env.ENDOR_API_SECRET}</password>\n' + '    </server>\n' + '  </servers>\n' + '</settings>';
  const REQUIRED_COLOR = '#d32f2f';
  const BRAND_GREEN = '#26D07C';
  const GREEN_HUE = '38,208,124';
  const greenAlpha = alpha => 'rgba(' + GREEN_HUE + ',' + alpha + ')';
  const greenBorder = alpha => '1px solid ' + greenAlpha(alpha);
  const WIZARD_TITLE = 'Package Manager Wizard';
  const WIZARD_SUBTITLE = 'Select your operating system and package manager client, then enter your namespace to get setup instructions for your configuration files.';
  const OS_OPTIONS = [{
    id: 'macos',
    label: 'macOS'
  }, {
    id: 'linux',
    label: 'Linux'
  }, {
    id: 'windows',
    label: 'Windows'
  }];
  const PACKAGE_MANAGERS = [{
    id: 'npm',
    label: 'npm',
    group: GROUP_JS,
    steps: [{
      text: BASE64_ENCODE_TEXT,
      code: BASE64_ENCODE_CMD
    }, {
      text: NPMRC_BASE64_TEXT,
      code: NPMRC_BASE64_BLOCK
    }],
    osOverrides: {
      windows: {
        steps: [{
          unnumbered: true,
          text: WIN_NPMRC_URL_TEXT,
          code: WIN_NPMRC_URL_CODE
        }, {
          unnumbered: true,
          text: WIN_CLI_ALT_TEXT,
          code: 'npm config set registry "https://<api-key>:<api-secret>@factory.endorlabs.com/v1/namespaces/{{namespace}}/firewall/npm/"'
        }],
        notes: [NPM_LOCKFILE_NOTE]
      }
    },
    notes: [NPM_LOCKFILE_NOTE]
  }, {
    id: 'pnpm',
    label: 'pnpm',
    group: GROUP_JS,
    steps: [{
      text: BASE64_ENCODE_TEXT,
      code: BASE64_ENCODE_CMD
    }, {
      text: NPMRC_BASE64_TEXT,
      code: NPMRC_BASE64_BLOCK
    }],
    osOverrides: {
      windows: {
        steps: [{
          unnumbered: true,
          text: WIN_NPMRC_URL_TEXT,
          code: WIN_NPMRC_URL_CODE
        }, {
          unnumbered: true,
          text: WIN_CLI_ALT_TEXT,
          code: 'pnpm config set registry "https://<api-key>:<api-secret>@factory.endorlabs.com/v1/namespaces/{{namespace}}/firewall/npm/"'
        }],
        notes: [PNPM_LOCKFILE_NOTE]
      }
    },
    notes: [PNPM_LOCKFILE_NOTE]
  }, {
    id: 'yarn-classic',
    label: 'Yarn Classic (v1)',
    group: GROUP_JS,
    steps: [{
      text: 'Run the following commands in your terminal to encode your credentials and API secret as Base64 strings. Replace `<api-key>` and `<api-secret>` with the credentials you saved in the [previous step](#create-an-api-key-for-the-package-firewall).',
      code: 'echo -n "<api-key>:<api-secret>" | base64\n' + 'echo -n "<api-secret>" | base64'
    }, {
      text: 'Add the following lines to your `.npmrc` file at the project level, or in the user-level file at `~/.npmrc`. Replace `<base64-credentials>` with the Base64-encoded `<api-key>:<api-secret>` string, `<api-key>` with your API key, and `<base64-secret>` with the Base64-encoded API secret.',
      code: 'registry=https://factory.endorlabs.com/v1/namespaces/{{namespace}}/firewall/npm/\n' + 'always-auth=true\n' + '//factory.endorlabs.com/v1/namespaces/{{namespace}}/firewall/npm/:_auth=<base64-credentials>\n' + '//factory.endorlabs.com/v1/namespaces/{{namespace}}/firewall/npm/:username=<api-key>\n' + '//factory.endorlabs.com/v1/namespaces/{{namespace}}/firewall/npm/:_password=<base64-secret>'
    }, {
      text: 'Add the following lines to your `.yarnrc` file at the project level, or in the user-level file at `~/.yarnrc`.',
      code: 'registry "https://factory.endorlabs.com/v1/namespaces/{{namespace}}/firewall/npm/"\n' + 'always-auth true'
    }],
    osOverrides: {
      windows: {
        steps: [{
          unnumbered: true,
          text: WIN_NPMRC_URL_TEXT,
          code: WIN_NPMRC_URL_CODE
        }, {
          unnumbered: true,
          text: WIN_CLI_ALT_TEXT,
          code: 'yarn config set registry "https://<api-key>:<api-secret>@factory.endorlabs.com/v1/namespaces/{{namespace}}/firewall/npm/"'
        }],
        notes: []
      }
    },
    notes: [YARN_CLASSIC_NOTE]
  }, {
    id: 'yarn-berry',
    label: 'Yarn Berry (v2+)',
    group: GROUP_JS,
    steps: [{
      text: BASE64_ENCODE_TEXT,
      code: BASE64_ENCODE_CMD
    }, {
      text: 'Add the following lines to your `.yarnrc.yml` file at the project level, or in the user-level file at `~/.yarnrc.yml`. Replace `<base64-credentials>` with the Base64 string you generated.',
      code: 'npmRegistryServer: "https://factory.endorlabs.com/v1/namespaces/{{namespace}}/firewall/npm/"\n' + 'npmAlwaysAuth: true\n' + 'npmAuthIdent: "<base64-credentials>"'
    }],
    osOverrides: {
      windows: {
        steps: [{
          unnumbered: true,
          text: 'Add the following lines to the `.yarnrc.yml` file in your project root, or to your user-level `.yarnrc.yml` at `%USERPROFILE%\\.yarnrc.yml`, for example `C:\\Users\\johndoe\\.yarnrc.yml`. Replace `<api-key>` and `<api-secret>` with the credentials you saved in the [previous step](#create-an-api-key-for-the-package-firewall).',
          code: 'npmRegistryServer: "https://factory.endorlabs.com/v1/namespaces/{{namespace}}/firewall/npm/"\n' + 'npmAlwaysAuth: true\n' + 'npmAuthIdent: "<api-key>:<api-secret>"'
        }],
        notes: ['Use the user level configuration file at `%USERPROFILE%\\.yarnrc.yml` for Yarn 3.1 or later. For older versions, configure `.yarnrc` in each project.']
      }
    },
    notes: ['Use the home directory `~/.yarnrc.yml` for Yarn 3.1 or later. For older versions, configure `.yarnrc` in each project.']
  }, {
    id: 'bun',
    label: 'Bun',
    group: GROUP_JS,
    steps: [{
      unnumbered: true,
      text: 'Using the `.npmrc` file:'
    }, {
      text: BASE64_ENCODE_TEXT,
      code: BASE64_ENCODE_CMD
    }, {
      text: NPMRC_BASE64_TEXT,
      code: NPMRC_BASE64_BLOCK
    }, {
      unnumbered: true,
      text: 'Using the `bunfig.toml` file:'
    }, {
      unnumbered: true,
      text: 'Add the following to your project root or `~/.bunfig.toml`. Replace `<api-key>` and `<api-secret>` with the credentials you saved in the [previous step](#create-an-api-key-for-the-package-firewall).',
      code: '[install]\n' + 'registry = {\n' + '  url = "https://factory.endorlabs.com/v1/namespaces/{{namespace}}/firewall/npm/",\n' + '  username = "<api-key>",\n' + '  password = "<api-secret>"\n' + '}'
    }],
    osOverrides: {
      windows: {
        steps: [{
          unnumbered: true,
          text: 'Add the following to the `bunfig.toml` file in your project root, or to your user-level `bunfig.toml` at `%USERPROFILE%\\.bunfig.toml`, for example `C:\\Users\\johndoe\\.bunfig.toml`. Replace `<api-key>` and `<api-secret>` with the credentials you saved in the [previous step](#create-an-api-key-for-the-package-firewall).',
          code: '[install]\n' + 'registry = "https://<api-key>:<api-secret>@factory.endorlabs.com/v1/namespaces/{{namespace}}/firewall/npm/"'
        }],
        notes: []
      }
    },
    notes: []
  }, {
    id: 'pip',
    label: 'pip',
    group: GROUP_PY,
    steps: [{
      unnumbered: true,
      text: 'Add the following to your {{pipFile}} file at {{pipConfigPath}}. ' + REPLACE_SAVED,
      code: '[global]\n' + 'index-url = https://<api-key>:<api-secret>@factory.endorlabs.com/v1/namespaces/{{namespace}}/firewall/pypi/simple/\n' + 'trusted-host = factory.endorlabs.com'
    }],
    osTokens: {
      macos: {
        pipFile: '`pip.conf`',
        pipConfigPath: '`~/.config/pip/pip.conf`'
      },
      linux: {
        pipFile: '`pip.conf`',
        pipConfigPath: '`~/.config/pip/pip.conf`'
      },
      windows: {
        pipFile: '`pip.ini`',
        pipConfigPath: '`%APPDATA%\\pip\\pip.ini`, for example `C:\\Users\\johndoe\\AppData\\Roaming\\pip\\pip.ini`'
      }
    },
    notes: []
  }, {
    id: 'uv',
    label: 'uv',
    group: GROUP_PY,
    steps: [{
      unnumbered: true,
      text: 'Using the `uv.toml` file:'
    }, {
      unnumbered: true,
      text: 'Add the following to your `uv.toml` file at the project level, or in the user-level file at {{uvConfigPath}}. ' + REPLACE_SAVED,
      code: '[[index]]\n' + 'url = "https://<api-key>:<api-secret>@factory.endorlabs.com/v1/namespaces/{{namespace}}/firewall/pypi/simple/"\n' + 'default = true'
    }, {
      unnumbered: true,
      text: 'Using the `pyproject.toml` file:'
    }, {
      unnumbered: true,
      text: 'Add the following to your `pyproject.toml` file at your project root. ' + REPLACE_SAVED,
      code: '[[tool.uv.index]]\n' + 'url = "https://<api-key>:<api-secret>@factory.endorlabs.com/v1/namespaces/{{namespace}}/firewall/pypi/simple/"\n' + 'default = true'
    }],
    osTokens: {
      macos: {
        uvConfigPath: '`~/.config/uv/uv.toml`'
      },
      linux: {
        uvConfigPath: '`~/.config/uv/uv.toml`'
      },
      windows: {
        uvConfigPath: '`%APPDATA%\\uv\\uv.toml`, for example `C:\\Users\\johndoe\\AppData\\Roaming\\uv\\uv.toml`'
      }
    },
    notes: ['uv writes the Package Firewall URL, including your namespace, into `uv.lock`, so it fails for other namespaces or CI. Regenerate it with `uv lock` when the index changes. To install from the lockfile in CI/CD, see [Configure uv for CI/CD](/package-firewall/direct-integration#configure-uv-for-ci/cd).']
  }, {
    id: 'poetry',
    label: 'Poetry',
    group: GROUP_PY,
    steps: [{
      text: 'Run the following command in your terminal to set your credentials. ' + REPLACE_SAVED,
      code: 'poetry config http-basic.endor-firewall <api-key> <api-secret>'
    }, {
      text: 'Add the following to your `pyproject.toml` file at your project root.',
      code: '[[tool.poetry.source]]\n' + 'name = "endor-firewall"\n' + 'url = "https://factory.endorlabs.com/v1/namespaces/{{namespace}}/firewall/pypi/simple/"\n' + 'priority = "primary"'
    }],
    osOverrides: {
      windows: {
        steps: [{
          text: 'Set your credentials as environment variables in PowerShell. ' + REPLACE_SAVED,
          code: '$env:POETRY_HTTP_BASIC_ENDOR_FIREWALL_USERNAME = "<api-key>"\n' + '$env:POETRY_HTTP_BASIC_ENDOR_FIREWALL_PASSWORD = "<api-secret>"'
        }, {
          text: 'Add the following to your `pyproject.toml` file at your project root.',
          code: '[[tool.poetry.source]]\n' + 'name = "endor-firewall"\n' + 'url = "https://factory.endorlabs.com/v1/namespaces/{{namespace}}/firewall/pypi/simple/"\n' + 'priority = "primary"'
        }, {
          unnumbered: true,
          text: 'Alternatively, you can run the following command in your terminal to add the source.',
          code: 'poetry source add endor-firewall "https://factory.endorlabs.com/v1/namespaces/{{namespace}}/firewall/pypi/simple/" --priority=primary'
        }],
        notes: [POETRY_NAME_NOTE, POETRY_LOCKFILE_NOTE]
      }
    },
    notes: [POETRY_NAME_NOTE, POETRY_LOCKFILE_NOTE]
  }, {
    id: 'go',
    label: 'Go',
    group: GROUP_GO,
    steps: [{
      unnumbered: true,
      text: 'Export the `GOPROXY` environment variable to the Package Firewall URL in your terminal. ' + REPLACE_SAVED,
      code: 'export GOPROXY=https://<api-key>:<api-secret>@factory.endorlabs.com/v1/namespaces/{{namespace}}/firewall/go/,direct'
    }, {
      unnumbered: true,
      text: 'To persist the setting across sessions, write it to your Go environment.',
      code: 'go env -w GOPROXY=https://<api-key>:<api-secret>@factory.endorlabs.com/v1/namespaces/{{namespace}}/firewall/go/,direct'
    }],
    osOverrides: {
      windows: {
        steps: [{
          unnumbered: true,
          text: 'Set the `GOPROXY` environment variable to the Package Firewall URL in PowerShell. ' + REPLACE_SAVED,
          code: '$env:GOPROXY="https://<api-key>:<api-secret>@factory.endorlabs.com/v1/namespaces/{{namespace}}/firewall/go/,direct"'
        }, {
          unnumbered: true,
          text: 'To persist the setting across sessions, write it to your Go environment.',
          code: 'go env -w GOPROXY=https://<api-key>:<api-secret>@factory.endorlabs.com/v1/namespaces/{{namespace}}/firewall/go/,direct'
        }]
      }
    },
    notes: []
  }, {
    id: 'maven',
    label: 'Maven',
    group: GROUP_JAVA,
    steps: [{
      text: 'Export the Package Firewall URL, your API key, and your API secret as environment variables in your terminal. ' + REPLACE_SAVED,
      code: 'export ENDOR_FIREWALL_MVN_URL=https://factory.endorlabs.com/v1/namespaces/{{namespace}}/firewall/maven/\n' + 'export ENDOR_API_KEY=<api-key>\n' + 'export ENDOR_API_SECRET=<api-secret>'
    }, {
      text: 'Add the following lines to your `settings.xml` file at the project level, or to the global file at `${mvn_home}/conf/settings.xml`. You can find `mvn_home` in the output of `mvn -v`. Replace `<project-name>` with the name of your application or project.',
      code: MAVEN_SETTINGS_XML
    }],
    osOverrides: {
      windows: {
        steps: [{
          text: 'Set the Package Firewall URL, your API key, and your API secret as environment variables in PowerShell. ' + REPLACE_SAVED,
          code: '$env:ENDOR_FIREWALL_MVN_URL="https://factory.endorlabs.com/v1/namespaces/{{namespace}}/firewall/maven/"\n' + '$env:ENDOR_API_KEY="<api-key>"\n' + '$env:ENDOR_API_SECRET="<api-secret>"'
        }, {
          text: 'Add the following to your `settings.xml` file. Replace `<project-name>` with the name of your application or project.',
          code: MAVEN_SETTINGS_XML
        }]
      }
    },
    notes: []
  }];
  const sanitizeNamespace = input => {
    if (!input) {
      return '';
    }
    return String(input).trim().replaceAll(/[^a-zA-Z0-9._-]/g, '').substring(0, NAMESPACE_MAX);
  };
  const [os, setOs] = useState('');
  const [packageManager, setPackageManager] = useState('');
  const [namespace, setNamespace] = useState(defaultNamespace);
  const [generated, setGenerated] = useState(false);
  const [generatedNamespace, setGeneratedNamespace] = useState('');
  const [isDark, setIsDark] = useState(false);
  const [copyStates, setCopyStates] = useState({});
  const [hoveredCopyKey, setHoveredCopyKey] = useState(null);
  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();
  }, []);
  const selectedPm = useMemo(() => PACKAGE_MANAGERS.find(p => p.id === packageManager) || null, [packageManager]);
  const sanitizedNamespace = useMemo(() => sanitizeNamespace(namespace), [namespace]);
  const groupedPms = useMemo(() => {
    return GROUP_ORDER.map(group => ({
      group,
      items: PACKAGE_MANAGERS.filter(p => p.group === group)
    })).filter(g => g.items.length > 0);
  }, []);
  const tokenMap = useMemo(() => ({
    namespace: generatedNamespace || DEFAULT_NAMESPACE_LABEL,
    ...selectedPm?.osTokens?.[os] || ({})
  }), [generatedNamespace, selectedPm, os]);
  const substitute = str => {
    if (!str) {
      return str;
    }
    let out = str.replaceAll(NAMESPACE_TOKEN, tokenMap.namespace);
    Object.keys(tokenMap).forEach(key => {
      out = out.replaceAll('{{' + key + '}}', tokenMap[key]);
    });
    return out;
  };
  const activeSteps = useMemo(() => {
    if (!selectedPm) {
      return [];
    }
    return selectedPm.osOverrides?.[os]?.steps || selectedPm.steps;
  }, [selectedPm, os]);
  const resolvedSteps = useMemo(() => activeSteps.map(step => ({
    text: substitute(step.text),
    configFile: substitute(step.configFile),
    code: substitute(step.code),
    unnumbered: step.unnumbered
  })), [activeSteps, tokenMap]);
  const activeNotes = useMemo(() => {
    if (!selectedPm) {
      return [];
    }
    return selectedPm.osOverrides?.[os]?.notes || selectedPm.notes || [];
  }, [selectedPm, os]);
  const resolvedNotes = useMemo(() => activeNotes.map(substitute), [activeNotes, tokenMap]);
  const canGenerate = Boolean(os && selectedPm && sanitizedNamespace);
  const canReset = Boolean(os || selectedPm || namespace || generated);
  const ready = Boolean(os && selectedPm && generated);
  const handleGenerate = useCallback(() => {
    if (!canGenerate) {
      return;
    }
    setGeneratedNamespace(sanitizedNamespace);
    setGenerated(true);
  }, [canGenerate, sanitizedNamespace]);
  const handleNamespaceChange = useCallback(value => {
    setNamespace(value);
    setGenerated(false);
  }, []);
  const markCopied = useCallback(key => {
    setCopyStates(prev => ({
      ...prev,
      [key]: true
    }));
    setTimeout(() => setCopyStates(prev => ({
      ...prev,
      [key]: false
    })), COPY_FEEDBACK_TIMEOUT_MS);
  }, []);
  const handleCopyToClipboard = useCallback((text, key) => {
    navigator.clipboard.writeText(text).then(() => markCopied(key)).catch(() => {
      alert('Unable to copy. Select and copy the text manually.');
    });
  }, [markCopied]);
  const handleReset = useCallback(() => {
    setOs('');
    setPackageManager('');
    setNamespace('');
    setGenerated(false);
    setGeneratedNamespace('');
  }, []);
  const backgroundColor = isDark ? '#0d1117' : '#ffffff';
  const backgroundSecondary = isDark ? '#161b22' : '#f6f8fa';
  const sectionBg = isDark ? '#161b22' : '#ffffff';
  const textColor = isDark ? '#e6edf3' : '#1f2937';
  const mutedText = isDark ? 'rgba(230,237,243,0.7)' : 'rgba(31,41,55,0.7)';
  const cardBorder = '2px solid ' + greenAlpha(isDark ? '0.4' : '0.5');
  const cardShadow = '0 2px 8px ' + greenAlpha(isDark ? '0.05' : '0.1');
  const activeGradient = 'linear-gradient(to bottom, rgba(50,225,140,0.92), rgba(30,190,110,0.92))';
  const activeTextColor = isDark ? '#000' : '#fff';
  const disabledBtnBg = isDark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.05)';
  const styles = {
    formGroup: {
      background: sectionBg,
      borderRadius: '12px',
      padding: '0.75rem',
      border: greenBorder('0.3')
    },
    label: {
      display: 'block',
      fontWeight: 600,
      marginBottom: '0.4rem',
      color: textColor,
      fontSize: '0.75rem',
      textTransform: 'uppercase',
      letterSpacing: '0.04em'
    },
    helpText: {
      fontSize: '0.7rem',
      color: mutedText,
      lineHeight: 1.3,
      marginTop: '0.25rem'
    },
    select: {
      padding: '0.5rem 0.75rem',
      border: greenBorder('0.45'),
      borderRadius: '8px',
      fontSize: '0.85rem',
      backgroundColor,
      color: textColor,
      width: '100%',
      cursor: 'pointer',
      outline: 'none',
      appearance: 'none',
      WebkitAppearance: 'none',
      MozAppearance: 'none',
      backgroundImage: "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2326D07C' d='M6 9L1 4h10z'/%3E%3C/svg%3E\")",
      backgroundRepeat: 'no-repeat',
      backgroundPosition: 'right 0.75rem center',
      paddingRight: '2.5rem'
    },
    option: {
      backgroundColor,
      color: textColor
    },
    input: {
      padding: '0.5rem 0.75rem',
      border: greenBorder('0.45'),
      borderRadius: '8px',
      fontSize: '0.85rem',
      backgroundColor,
      color: textColor,
      width: '100%',
      outline: 'none',
      boxSizing: 'border-box'
    },
    codeBox: {
      position: 'relative',
      marginTop: '0.5rem'
    },
    pre: {
      background: backgroundColor,
      color: textColor,
      padding: '0.75rem',
      borderRadius: '8px',
      fontFamily: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace",
      fontSize: '0.8rem',
      lineHeight: 1.4,
      overflowX: 'auto',
      margin: 0,
      whiteSpace: 'pre-wrap',
      wordWrap: 'break-word',
      border: greenBorder('0.2')
    },
    copyBtn: (copied, hovered) => {
      let color = mutedText;
      if (copied) color = BRAND_GREEN; else if (hovered) color = isDark ? '#ffffff' : '#000000';
      return {
        position: 'absolute',
        top: '0.4rem',
        right: '0.4rem',
        zIndex: 2,
        display: 'inline-flex',
        alignItems: 'center',
        justifyContent: 'center',
        width: '1.5rem',
        height: '1.5rem',
        padding: 0,
        lineHeight: 1,
        border: 'none',
        borderRadius: '6px',
        background: 'transparent',
        color,
        cursor: 'pointer',
        transition: 'color 120ms ease'
      };
    },
    copyTooltip: {
      position: 'absolute',
      top: '-1.7rem',
      right: 0,
      zIndex: 3,
      padding: '0.2rem 0.5rem',
      fontSize: '0.7rem',
      lineHeight: 1.2,
      fontWeight: 600,
      borderRadius: '4px',
      whiteSpace: 'nowrap',
      pointerEvents: 'none',
      background: BRAND_GREEN,
      color: '#0a2e1d'
    },
    osBtn: (active, hasLeftBorder) => {
      const base = {
        flex: 1,
        padding: '0.5rem 0.75rem',
        border: 'none',
        cursor: 'pointer',
        fontSize: '0.85rem',
        fontWeight: 600
      };
      if (hasLeftBorder) {
        base.borderLeft = greenBorder('0.45');
      }
      base.background = active ? activeGradient : backgroundSecondary;
      base.color = active ? activeTextColor : textColor;
      return base;
    },
    actionBtn: enabled => ({
      padding: '0.55rem 1.25rem',
      background: enabled ? activeGradient : disabledBtnBg,
      color: enabled ? activeTextColor : mutedText,
      border: greenBorder(enabled ? '0.5' : '0.2'),
      borderRadius: '10px',
      fontSize: '0.85rem',
      fontWeight: 600,
      cursor: enabled ? 'pointer' : 'not-allowed'
    }),
    stepText: {
      color: textColor,
      fontSize: '0.85rem',
      lineHeight: 1.5,
      marginBottom: '0.25rem'
    },
    fileLine: {
      color: mutedText,
      fontSize: '0.75rem',
      marginBottom: '0.1rem'
    },
    inlineCode: {
      background: backgroundSecondary,
      padding: '0.1rem 0.3rem',
      borderRadius: '4px',
      fontSize: '0.8em'
    },
    link: {
      color: BRAND_GREEN,
      textDecoration: 'underline'
    },
    noteBox: {
      marginTop: '0.75rem',
      padding: '0.5rem 0.75rem',
      background: isDark ? '#0d2818' : '#eafaf1',
      border: greenBorder('0.35'),
      borderRadius: '8px'
    },
    note: {
      color: textColor,
      fontSize: '0.78rem',
      lineHeight: 1.45,
      margin: '0.15rem 0'
    }
  };
  const requiredMark = <span style={{
    color: REQUIRED_COLOR
  }}> *</span>;
  const richTextNode = match => {
    const key = 'rt-' + match.index;
    if (match[1] !== undefined) {
      return <code key={key} style={styles.inlineCode}>{match[1]}</code>;
    }
    return <a key={key} href={match[3]} style={styles.link}>{match[2]}</a>;
  };
  const renderRichText = str => {
    if (!str) {
      return str;
    }
    const nodes = [];
    const pattern = /`([^`]+)`|\[([^\]]+)\]\(([^)]+)\)/g;
    let lastIndex = 0;
    let match = pattern.exec(str);
    while (match !== null) {
      if (match.index > lastIndex) {
        nodes.push(str.slice(lastIndex, match.index));
      }
      nodes.push(richTextNode(match));
      lastIndex = pattern.lastIndex;
      match = pattern.exec(str);
    }
    if (lastIndex < str.length) {
      nodes.push(str.slice(lastIndex));
    }
    return nodes;
  };
  const renderOsSelector = () => <div style={styles.formGroup}>
      <div style={styles.label} id="pfw-os-label">Operating system{requiredMark}</div>
      <fieldset aria-labelledby="pfw-os-label" style={{
    margin: 0,
    padding: 0,
    display: 'flex',
    gap: 0,
    borderRadius: '8px',
    overflow: 'hidden',
    border: greenBorder('0.45')
  }}>
        {OS_OPTIONS.map((option, index) => <button key={option.id} type="button" aria-pressed={os === option.id} onClick={() => setOs(option.id)} style={styles.osBtn(os === option.id, index > 0)}>
            {option.label}
          </button>)}
      </fieldset>
      <div style={styles.helpText}>Select your operating system.</div>
    </div>;
  const renderPmSelector = () => <div style={styles.formGroup}>
      <label style={styles.label} htmlFor="pfw-pm-select">Package manager client{requiredMark}</label>
      <select id="pfw-pm-select" value={packageManager} onChange={e => setPackageManager(e.target.value)} style={styles.select} aria-required="true">
        <option value="" disabled style={styles.option}>Select...</option>
        {groupedPms.map(g => <optgroup key={g.group} label={g.group} style={styles.option}>
            {g.items.map(pm => <option key={pm.id} value={pm.id} style={styles.option}>{pm.label}</option>)}
          </optgroup>)}
      </select>
      <div style={styles.helpText}>Select your package manager client.</div>
    </div>;
  const renderNamespaceInput = () => <div style={styles.formGroup}>
      <label style={styles.label} htmlFor="pfw-namespace">Namespace{requiredMark}</label>
      <input id="pfw-namespace" type="text" maxLength={NAMESPACE_MAX} value={namespace} onChange={e => handleNamespaceChange(e.target.value)} placeholder="your-namespace" style={styles.input} aria-required="true" />
      <div style={styles.helpText}>Your Endor Labs namespace. Only alphanumeric characters, dots, hyphens, and underscores are allowed.</div>
    </div>;
  const renderCopyIcon = copied => copied ? <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
        <polyline points="20 6 9 17 4 12"></polyline>
      </svg> : <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
        <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
        <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
      </svg>;
  const renderCodeBlock = (code, copyKey) => {
    const copied = copyStates[copyKey];
    const label = copied ? 'Copied!' : 'Copy';
    const isHovered = hoveredCopyKey === copyKey;
    return <div style={styles.codeBox}>
        <pre style={styles.pre}>{code}</pre>
        {isHovered && <span style={styles.copyTooltip} role="tooltip">{label}</span>}
        <button type="button" onClick={() => handleCopyToClipboard(code, copyKey)} onMouseEnter={() => setHoveredCopyKey(copyKey)} onMouseLeave={() => setHoveredCopyKey(prev => prev === copyKey ? null : prev)} onFocus={() => setHoveredCopyKey(copyKey)} onBlur={() => setHoveredCopyKey(prev => prev === copyKey ? null : prev)} style={styles.copyBtn(copied, isHovered)} aria-label={label}>
          {renderCopyIcon(copied)}
        </button>
      </div>;
  };
  const renderStep = (step, index, displayNumber, reserveNumberColumn) => {
    const key = packageManager + '-' + os + '-' + index;
    const numberStyle = {
      ...styles.stepText,
      fontWeight: 600,
      marginBottom: 0
    };
    return <li key={key} style={{
      marginBottom: '0.75rem',
      listStyle: 'none',
      display: 'flex',
      gap: '0.5rem'
    }}>
        {displayNumber !== null && <span style={numberStyle}>{displayNumber}.</span>}
        {displayNumber === null && reserveNumberColumn && <span style={{
      ...numberStyle,
      visibility: 'hidden'
    }} aria-hidden="true">0.</span>}
        <div style={{
      flex: 1,
      minWidth: 0
    }}>
          <div style={styles.stepText}>{renderRichText(step.text)}</div>
          {step.configFile && <div style={styles.fileLine}>File: <code style={styles.inlineCode}>{step.configFile}</code></div>}
          {step.code && renderCodeBlock(step.code, key)}
        </div>
      </li>;
  };
  const renderNotes = indent => <div style={{
    ...styles.noteBox,
    marginLeft: indent
  }}>
      {resolvedNotes.map(note => <p key={note} style={styles.note}>{renderRichText(note)}</p>)}
    </div>;
  const renderOutput = () => {
    let stepNumber = 0;
    const hasNumbered = resolvedSteps.some(step => !step.unnumbered);
    const items = resolvedSteps.map((step, index) => {
      let displayNumber = null;
      if (!step.unnumbered) {
        stepNumber += 1;
        displayNumber = stepNumber;
      }
      return renderStep(step, index, displayNumber, hasNumbered);
    });
    const noteIndent = hasNumbered ? '1.25rem' : '0';
    return <div style={{
      marginTop: '1rem'
    }}>
        <ol style={{
      paddingLeft: 0,
      margin: 0
    }}>{items}</ol>
        {resolvedNotes.length > 0 && renderNotes(noteIndent)}
      </div>;
  };
  return <div className="not-prose pfw-wizard" style={{
    margin: '1rem 0',
    fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif'
  }}>
      <div className="pfw-card" style={{
    background: backgroundColor,
    border: cardBorder,
    borderRadius: '16px',
    padding: '0.75rem 1rem',
    color: textColor,
    boxShadow: cardShadow
  }}>
        <div style={{
    marginBottom: '0.75rem'
  }}>
          <h3 style={{
    color: textColor,
    margin: '0 0 0.25rem 0',
    fontWeight: 600,
    fontSize: '1.1rem'
  }}>{WIZARD_TITLE}</h3>
          <p style={{
    color: textColor,
    opacity: 0.8,
    margin: 0,
    fontSize: '0.85rem',
    lineHeight: 1.4
  }}>{WIZARD_SUBTITLE}</p>
        </div>

        <div className="pfw-grid" style={{
    display: 'grid',
    gridTemplateColumns: 'repeat(3, 1fr)',
    gap: '0.75rem'
  }}>
          {renderOsSelector()}
          {renderPmSelector()}
          {renderNamespaceInput()}
        </div>

        {ready && renderOutput()}

        <div style={{
    display: 'flex',
    justifyContent: 'flex-start',
    alignItems: 'center',
    gap: '0.5rem',
    marginTop: '0.75rem'
  }}>
          <button type="button" onClick={handleReset} disabled={!canReset} aria-disabled={!canReset} style={styles.actionBtn(canReset)}>{'↺'} Reset</button>
          {!generated && <button type="button" onClick={handleGenerate} disabled={!canGenerate} aria-disabled={!canGenerate} style={styles.actionBtn(canGenerate)}>Generate</button>}
        </div>
      </div>
    </div>;
};

export const authTarget_0 = "your package manager client"

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

This setup is a package manager client deployment.

Direct integration routes package installation requests from your package manager clients through the Package Firewall directly, without a private package registry such as JFrog Artifactory, Google Artifact Registry, or Sonatype Nexus Repository. The Package Firewall evaluates each package request based on the malware check and the configured [Package Firewall policy](/package-firewall/policy) conditions, and handles each request in one of three ways:

* Block the installation and return an `HTTP 403` when the package is in the Endor Labs malware database, or a policy condition matches **Block**. The developer sees a generic `403` response without the specific reason. The Package Firewall records a log with the package, version, and reason.
* Allow the installation if a policy condition matches with **Warn**. The Package Firewall records a warning log with the package, version, and reason.
* Allow the installation if no malware is detected. No log is recorded.

The Package Firewall records one of the following reasons in the log when it blocks a package:

* `malware detected for package <name>@<version>`: Endor Labs classified the package as malware.
* `package has a CVSS vulnerability at or above the configured severity threshold`: The package has a known vulnerability at or above the CVSS severity threshold set in your policy. Endor Labs uses CVSS 3.x by default and evaluates vulnerability severity using that version. You can change the CVSS version for your namespace in [system settings](/platform-administration/configure-system-settings/#configure-cvss-score-version).
* `package license is restricted`: The license violates your Package Firewall policy.
* `package does not meet min_age_hours requirement`: The package is newer than the minimum age set in your policy.

Configure the direct integration for Package Firewall if your organization does not use a private package registry such as JFrog Artifactory, Google Artifact Registry, or Sonatype Nexus Repository.

IT administrators can use Mobile Device Management (MDM) scripts to deploy Package Firewall configurations to developer machines, eliminating the need for manual setup on each machine. These scripts update package manager configuration files with the Package Firewall URL and credentials. They also encode an attribution label into those credentials, so the firewall attributes every request to a developer and machine instead of the shared API key. The label defaults to `<user>@<machine>`, and you can customize it. See [User attribution](/package-firewall/mdm-deployment#user-attribution) to learn how the label works.

The Package Firewall logs show the attributed user on each event, so you can identify who requested a warned or blocked package. See [View Package Firewall logs](/package-firewall/logs) to learn more.

## Package Firewall support matrix

The following table outlines the package manager clients and ecosystems the Package Firewall supports through direct integration.

<YamlTable>
  {`
    - Package_manager_client: npm
    Ecosystem: JavaScript, TypeScript
    Public_registry: npm
    Configuration_files: \`.npmrc\`

    - Package_manager_client: pnpm
    Ecosystem: JavaScript, TypeScript
    Public_registry: npm
    Configuration_files: \`.npmrc\`

    - Package_manager_client: Yarn Classic
    Ecosystem: JavaScript, TypeScript
    Public_registry: npm
    Configuration_files: \`.npmrc\`, \`.yarnrc\`

    - Package_manager_client: Yarn Berry
    Ecosystem: JavaScript, TypeScript
    Public_registry: npm
    Configuration_files: \`.yarnrc.yml\`

    - Package_manager_client: Bun
    Ecosystem: JavaScript, TypeScript
    Public_registry: npm
    Configuration_files: \`.npmrc\`, \`bunfig.toml\`

    - Package_manager_client: pip
    Ecosystem: Python
    Public_registry: PyPI
    Configuration_files: \`pip.conf\`, \`pip.ini\`

    - Package_manager_client: Poetry
    Ecosystem: Python
    Public_registry: PyPI
    Configuration_files: \`pyproject.toml\`

    - Package_manager_client: uv
    Ecosystem: Python
    Public_registry: PyPI
    Configuration_files: \`uv.toml\`, \`pyproject.toml\`

    - Package_manager_client: Go
    Ecosystem: Go
    Public_registry: Go
    Configuration_files:

    - Package_manager_client: Maven
    Ecosystem: Java
    Public_registry: Maven Central
    Configuration_files: \`settings.xml\`
    `}
</YamlTable>

## Configure the Package Firewall

Complete the following steps to configure direct integration with the Package Firewall:

1. [Create an API key for the Package Firewall](#create-an-api-key-for-the-package-firewall).
2. [Configure your package manager configuration file](#configure-your-package-manager-configuration-file).
3. [Verify your setup](#verify-your-setup).

### Create an API key for the Package Firewall

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
}'
```

From the response, save the following values in a secure location. Use them as your Package Firewall credentials when you configure your package manager configuration file.

* **API key:** `spec.key`
* **API secret:** `spec.secret`

### Configure your package manager configuration file

Configure your package manager configuration file with the Package Firewall URL, your API key as the username, and your API secret as the password. The Package Firewall authenticates installation requests by validating the username and password pair. Replace `<api-key>` and `<api-secret>` with the credentials you saved in the previous step.

<PackageFirewallWizard />

### Verify your setup

To verify your setup, install a package that Endor Labs has classified as malware. The Package Firewall should block the installation and return an `HTTP 403`.

The following examples show the test command for each package manager client.

<AccordionGroup>
  <Accordion title="npm">
    Run the following command to test the Package Firewall with npm.

    ```bash theme={null}
    npm install endor-firewall-test@1.0.0
    ```

    When the Package Firewall blocks the package, the output looks similar to the following. The `403 Forbidden` response confirms that the firewall blocked the package.

    ```bash theme={null}
    npm error 403 403 Forbidden - GET https://factory.endorlabs.com/v1/namespaces/johndoe/firewall/npm/endor-firewall-test/-/endor-firewall-test-1.0.0.tgz - Forbidden
    npm error 403 In most cases, you or one of your dependencies are requesting a package version that is forbidden by your security policy, or on a server you do not have access to.
    npm error A complete log of this run can be found in: /Users/johndoe/.npm/_logs/2026-06-04T15_04_29_131Z-debug-0.log
    ```
  </Accordion>

  <Accordion title="pnpm">
    Run the following command to test the Package Firewall with pnpm.

    ```bash theme={null}
    pnpm add endor-firewall-test@1.0.0
    ```

    When the Package Firewall blocks the package, the output looks similar to the following. The `Forbidden - 403` response confirms that the firewall blocked the package.

    ```bash theme={null}
    Packages: +1
    +
    [ERR_PNPM_FETCH_403] GET https://factory.endorlabs.com/v1/namespaces/johndoe/firewall/npm/endor-firewall-test/-/endor-firewall-test-1.0.0.tgz: Forbidden - 403

    This error happened while installing a direct dependency of /Users/local-dev/Code/sample-projecr

    //factory.endorlabs.com/v1/namespaces/johndoe/firewall/npm/:_auth=ZW5k[hidden]
    //factory.endorlabs.com/v1/namespaces/johndoe/firewall/npm/:_username=xxxxx
    //factory.endorlabs.com/v1/namespaces/johndoe/firewall/npm/:_password=[hidden]
    Progress: resolved 1, reused 0, downloaded 0, added 0

    ```
  </Accordion>

  <Accordion title="Yarn Classic (v1)">
    Run the following command to test the Package Firewall with Yarn Classic.

    ```bash theme={null}
    yarn add endor-firewall-test@1.0.0
    ```

    When the Package Firewall blocks the package, the output looks similar to the following. The `Forbidden - 403` response confirms that the firewall blocked the package.

    ```bash theme={null}
    yarn add v1.22.22
    info No lockfile found.
    [1/4] 🔍  Resolving packages...
    [2/4] 🚚  Fetching packages...
    error Error: https://factory.endorlabs.com/v1/namespaces/johndoe/firewall/npm/endor-firewall-test/-/endor-firewall-test-1.0.0.tgz: Request failed "403 Forbidden"
        at ResponseError.ExtendableBuiltin (/Users/johndoe/.nvm/versions/node/v24.6.0/lib/node_modules/yarn/lib/cli.js:696:66)
        at new ResponseError (/Users/johndoe/.nvm/versions/node/v24.6.0/lib/node_modules/yarn/lib/cli.js:802:124)
        at Request.<anonymous> (/Users/johndoe/.nvm/versions/node/v24.6.0/lib/node_modules/yarn/lib/cli.js:66750:16)
        at Request.emit (node:events:508:28)
        at module.exports.Request.onRequestResponse (/Users/johndoe/.nvm/versions/node/v24.6.0/lib/node_modules/yarn/lib/cli.js:142287:10)
        at ClientRequest.emit (node:events:508:28)
        at HTTPParser.parserOnIncomingClient (node:_http_client:772:27)
        at HTTPParser.parserOnHeadersComplete (node:_http_common:117:17)
        at TLSSocket.socketOnData (node:_http_client:614:22)
        at TLSSocket.emit (node:events:508:28)
    info Visit https://yarnpkg.com/en/docs/cli/add for documentation about this command.
    ```
  </Accordion>

  <Accordion title="Yarn Berry (v2+)">
    Run the following command to test the Package Firewall with Yarn Berry.

    ```bash theme={null}
    yarn add endor-firewall-test@1.0.0
    ```

    When the Package Firewall blocks the package, the output looks similar to the following. The `403 (Forbidden)` response confirms that the firewall blocked the package.

    ```bash theme={null}
    ➤ YN0000: · Yarn 4.16.0
    ➤ YN0000: ┌ Resolution step
    ➤ YN0085: │ + endor-firewall-test@npm:1.0.0
    ➤ YN0000: └ Completed
    ➤ YN0000: ┌ Fetch step
    ➤ YN0035: │ endor-firewall-test@npm:1.0.0: The remote server failed to provide the requested resource
    ➤ YN0035: │   Response Code: 403 (Forbidden)
    ➤ YN0035: │   Request Method: GET
    ➤ YN0035: │   Request URL: https://factory.endorlabs.com/v1/namespaces/johndoe/firewall/npm/endor-firewall-test/-/endor-firewall-test-1.0.0.tgz
    ➤ YN0000: └ Completed in 0s 839ms
    ➤ YN0000: · Failed with errors in 0s 853ms
    ```
  </Accordion>

  <Accordion title="Bun">
    Run the following command to test the Package Firewall with Bun.

    ```bash theme={null}
    bun add endor-firewall-test@1.0.0
    ```

    When the Package Firewall blocks the package, the output looks similar to the following. The `403` response confirms that the firewall blocked the package.

    ```bash theme={null}
    bun add v1.3.14 (0d9b296a)
    error: GET https://factory.endorlabs.com/v1/namespaces/johndoe/firewall/npm/endor-firewall-test/-/endor-firewall-test-1.0.0.tgz - 403
    ```
  </Accordion>

  <Accordion title="pip">
    Run the following command to test the Package Firewall with pip.

    ```bash theme={null}
    pip install endor-firewall-test==1.0.0
    ```

    When the Package Firewall blocks the package, the output looks similar to the following. The `403` response confirms that the firewall blocked the package.

    ```bash theme={null}

    Looking in indexes:
    https://<credentials>@factory.endorlabs.com/v1/namespaces/johndoe/firewall/pypi/simple/

    Collecting endor_firewall_test==1.0.0

    ERROR: HTTP error 403 while getting:
    https://factory.endorlabs.com/v1/namespaces/johndoe/firewall/pypi/packages/61/05/6e99035fec6c7e407fffc052a0060495f6a2fcae2143db3239c7399d5b6e/endor_firewall_test-1.0.0-py3-none-any.whl.metadata

    ERROR: 403 Client Error: Forbidden for URL:
    https://factory.endorlabs.com/v1/namespaces/johndoe/firewall/pypi/packages/61/05/6e99035fec6c7e407fffc052a0060495f6a2fcae2143db3239c7399d5b6e/endor_firewall_test-1.0.0-py3-none-any.whl.metadata
    ```
  </Accordion>

  <Accordion title="uv">
    Run the following command to test the Package Firewall with uv.

    ```bash theme={null}
    uv pip install endor-firewall-test==1.0.0
    ```

    When the Package Firewall blocks the package, the output looks similar to the following. The `(403 Forbidden)` response confirms that the firewall blocked the package.

    ```bash theme={null}
    error: Failed to fetch:
    https://factory.endorlabs.com/v1/namespaces/johndoe/firewall/pypi/packages/61/05/6e99035fec6c7e407fffc052a0060495f6a2fcae2143db3239c7399d5b6e/endor_firewall_test-1.0.0-py3-none-any.whl.metadata

    Caused by:

    HTTP status client error (403 Forbidden) for URL:
    https://factory.endorlabs.com/v1/namespaces/johndoe/firewall/pypi/packages/61/05/6e99035fec6c7e407fffc052a0060495f6a2fcae2143db3239c7399d5b6e/endor_firewall_test-1.0.0-py3-none-any.whl.metadata
    ```
  </Accordion>

  <Accordion title="Poetry">
    Run the following command to test the Package Firewall with Poetry.

    ```bash theme={null}
    poetry add endor-firewall-test==1.0.0
    ```

    When the Package Firewall blocks the package, the output looks similar to the following. The `403 Forbidden` response confirms that the firewall blocked the package.

    ```bash theme={null}
    Updating dependencies
    Resolving dependencies... (0.8s)
    Source (endor-firewall): Failed to retrieve metadata at:
    https://factory.endorlabs.com/v1/namespaces/johndoe/firewall/pypi/packages/61/05/6e99035fec6c7e407fffc052a0060495f6a2fcae2143db3239c7399d5b6e/endor_firewall_test-1.0.0-py3-none-any.whl.metadata

    Resolving dependencies... (2.8s)
    Source (endor-firewall): Failed to retrieve metadata at:
    https://factory.endorlabs.com/v1/namespaces/johndoe/firewall/pypi/packages/61/05/6e99035fec6c7e407fffc052a0060495f6a2fcae2143db3239c7399d5b6e/endor_firewall_test-1.0.0-py3-none-any.whl.metadata

    Resolving dependencies... (3.9s)
    Source (endor-firewall): Failed to retrieve metadata at:
    https://factory.endorlabs.com/v1/namespaces/johndoe/firewall/pypi/packages/61/05/6e99035fec6c7e407fffc052a0060495f6a2fcae2143db3239c7399d5b6e/endor_firewall_test-1.0.0-py3-none-any.whl.metadata

    Resolving dependencies... (4.2s)

    403 Client Error: Forbidden for URL:
    https://factory.endorlabs.com/v1/namespaces/johndoe/firewall/pypi/packages/61/05/6e99035fec6c7e407fffc052a0060495f6a2fcae2143db3239c7399d5b6e/endor_firewall_test-1.0.0-py3-none-any.whl
    ```
  </Accordion>

  <Accordion title="Go">
    Run the following command to test the Package Firewall with Go.

    ```bash theme={null}
    go install github.com/endorlabstest/endor-firewall-test@v1.0.0
    ```

    When the Package Firewall blocks the package, the output looks similar to the following. The `403 Forbidden` response confirms that the firewall blocked the package.

    ```bash theme={null}
    go: github.com/endorlabstest/endor-firewall-test@v1.0.0:
    reading https://<credentials>@factory.endorlabs.com/v1/namespaces/johndoe/firewall/go/github.com/endorlabstest/endor-firewall-test/@v/v1.0.0.info:
    403 Forbidden
    ```
  </Accordion>

  <Accordion title="Maven">
    Add `io.github.endorlabs:endor-java-webapp-demo:4.1` as a dependency in your `pom.xml`, then run one of the following commands to test the Package Firewall with Maven.

    For a project-level `settings.xml` in your project directory, pass it explicitly with `-s`:

    ```bash theme={null}
    mvn dependency:resolve -s settings.xml
    ```

    For a global `settings.xml` at `${mvn_home}/conf/settings.xml`, Maven picks it up automatically:

    ```bash theme={null}
    mvn dependency:resolve
    ```

    When the Package Firewall blocks the package, the output looks similar to the following. The `Forbidden (403)` response confirms that the firewall blocked the package.

    ```bash theme={null}
    [INFO] Scanning for projects...
    [INFO]
    [INFO] -----------------------< com.example:my-app >------------------------
    [INFO] Building my-app 1.0.0
    [INFO]   from pom.xml
    [INFO] --------------------------------[ jar ]---------------------------------
    Downloading from endor-firewall: https://factory.endorlabs.com/v1/namespaces/johndoe/firewall/maven/io/github/endorlabs/endor-java-webapp-demo/4.1/endor-java-webapp-demo-4.1.pom
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD FAILURE
    [INFO] ------------------------------------------------------------------------
    [INFO] Total time:  1.600 s
    [INFO] Finished at: 2026-06-09T19:37:32+05:30
    [INFO] ------------------------------------------------------------------------
    [ERROR] Failed to execute goal on project my-app: Could not collect dependencies for project com.example:my-app:jar:1.0.0
    [ERROR] Failed to read artifact descriptor for io.github.endorlabs:endor-java-webapp-demo:jar:4.1
    [ERROR] 	Caused by: The following artifacts could not be resolved: io.github.endorlabs:endor-java-webapp-demo:pom:4.1 (absent): Could not transfer artifact io.github.endorlabs:endor-java-webapp-demo:pom:4.1 from/to endor-firewall (https://factory.endorlabs.com/v1/namespaces/johndoe/firewall/maven): status code: 403, reason phrase: Forbidden (403)
    [ERROR]
    ```
  </Accordion>
</AccordionGroup>

### Store Package Firewall credentials as CI/CD secrets

Store your Package Firewall API key and API secret as secrets in your CI/CD system, such as repository or runner secrets. Expose them to pipeline jobs as environment variables. Don't write credentials directly in pipeline steps or commit them to your repository.

The following table maps each package manager configuration file to the environment variables it reads. The uv and Poetry variable names derive from the `endor-firewall` index or source name used in the following sections.

<YamlTable>
  {`
    - Configuration_file: \`.npmrc\` (npm and pnpm)
    Environment_variables: \`NPM_AUTH\`
    Value: Base64 encoding of \`<api-key>:<api-secret>\`
    Reference: [npm CI/CD documentation](https://docs.npmjs.com/using-private-packages-in-a-ci-cd-workflow#set-the-token-as-an-environment-variable-on-the-cicd-server)

    - Configuration_file: \`pyproject.toml\` or \`uv.toml\` (uv)
    Environment_variables: \`UV_INDEX_ENDOR_FIREWALL_USERNAME\` and \`UV_INDEX_ENDOR_FIREWALL_PASSWORD\`
    Value: API key and API secret
    Reference: [uv environment variable reference](https://docs.astral.sh/uv/reference/environment/#uv_index_name_password)

    - Configuration_file: \`pyproject.toml\` (Poetry)
    Environment_variables: \`POETRY_HTTP_BASIC_ENDOR_FIREWALL_USERNAME\` and \`POETRY_HTTP_BASIC_ENDOR_FIREWALL_PASSWORD\`
    Value: API key and API secret
    Reference: [Poetry repositories documentation](https://python-poetry.org/docs/repositories/)
    `}
</YamlTable>

To store secrets and map them to environment variables, refer to the documentation for your CI/CD system, for example [GitHub Actions](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions), [GitLab CI/CD](https://docs.gitlab.com/ci/variables/), [Azure DevOps](https://learn.microsoft.com/en-us/azure/devops/pipelines/process/set-secret-variables), [CircleCI](https://circleci.com/docs/guides/security/env-vars/), [Jenkins](https://www.jenkins.io/doc/book/using/using-credentials/), or [Harness](https://developer.harness.io/docs/platform/secrets/add-use-text-secrets/).

### Configure uv for CI/CD

When you run `uv lock` with the Package Firewall configured, uv writes the Firewall URL, including your namespace, into `uv.lock`. Every package resolves through Endor Labs instead of a canonical PyPI source.

Since the lockfile references authenticated Firewall URLs, reproducible installs fail in CI/CD when [credentials](#create-an-api-key-for-the-package-firewall) to `factory.endorlabs.com` are absent. uv commands that require a connection to the lockfile URLs, such as `uv sync --locked` or `uv sync --frozen`, return `401 Unauthorized` unless credentials are provided.

To install from the lockfile in CI/CD without committing credentials to your repository, give your index a name and pass the credentials as environment variables.

1. In your existing uv index in `pyproject.toml` or `uv.toml`, add a name and remove the credentials from the URL. uv matches credentials to an index by this name.

   <AccordionGroup>
     <Accordion title="pyproject.toml">
       ```toml theme={null}
       [[tool.uv.index]]
       name = "endor-firewall"
       url = "https://factory.endorlabs.com/v1/namespaces/<your-namespace>/firewall/pypi/simple/"
       default = true
       ```
     </Accordion>

     <Accordion title="uv.toml">
       ```toml theme={null}
       [[index]]
       name = "endor-firewall"
       url = "https://factory.endorlabs.com/v1/namespaces/<your-namespace>/firewall/pypi/simple/"
       default = true
       ```
     </Accordion>
   </AccordionGroup>

2. In your CI/CD system, store the credentials as secrets and expose them to jobs as the following environment variables. uv derives the variable names from the index name in uppercase, so `endor-firewall` becomes `ENDOR_FIREWALL`. For more information, refer to the [uv environment variable reference](https://docs.astral.sh/uv/reference/environment/#uv_index_name_password).

   * `UV_INDEX_ENDOR_FIREWALL_USERNAME`: Your Package Firewall API key.
   * `UV_INDEX_ENDOR_FIREWALL_PASSWORD`: Your Package Firewall API secret.

### Configure npm for CI/CD

When you run `npm install` with the Package Firewall configured, npm writes the Firewall URL, including your namespace, into `package-lock.json`. Every package resolves through Endor Labs instead of a canonical npm registry.

Since the lockfile references Firewall URLs, reproducible installs fail in CI/CD when [credentials](#create-an-api-key-for-the-package-firewall) to `factory.endorlabs.com` are absent. Commands that install from the lockfile, such as `npm ci`, return `401 Unauthorized` unless credentials are provided.

To install from the lockfile in CI/CD without committing credentials to your repository, keep the registry URL in `.npmrc` and pass the encoded credentials as an environment variable.

1. In the project `.npmrc` file, set the Package Firewall registry and read `_auth` from an environment variable.

   ```ini theme={null}
   registry=https://factory.endorlabs.com/v1/namespaces/<your-namespace>/firewall/npm/
   //factory.endorlabs.com/v1/namespaces/<your-namespace>/firewall/npm/:_auth=${NPM_AUTH}
   always-auth=true
   ```

2. Generate the Base64 encoding of `<api-key>:<api-secret>` with your Package Firewall API key and API secret. This is the same encoding as the configuration wizard.

   ```bash theme={null}
   echo -n "<api-key>:<api-secret>" | base64
   ```

3. In your CI/CD system, store the encoded value as a secret and expose it to jobs as the `NPM_AUTH` environment variable. For more information, refer to the [npm CI/CD documentation](https://docs.npmjs.com/using-private-packages-in-a-ci-cd-workflow#set-the-token-as-an-environment-variable-on-the-cicd-server).

### Configure pnpm for CI/CD

pnpm reads the same `.npmrc` registry as npm. When that file points at Package Firewall, `pnpm install` and `pnpm add` go through Endor Labs. By default, `pnpm-lock.yaml` stores integrity hashes and does not store tarball URLs.

Set the same `.npmrc` credentials in CI/CD as you do for npm. See [Configure npm for CI/CD](/package-firewall/direct-integration#configure-npm-for-ci/cd) to set `NPM_AUTH`. Commands such as `pnpm install --frozen-lockfile` then resolve through Package Firewall.

If the [`lockfile-include-tarball-url`](https://pnpm.io/settings) setting is `true`, pnpm writes Firewall tarball URLs into `pnpm-lock.yaml`. Reproducible installs then return `401 Unauthorized` when credentials to `factory.endorlabs.com` are absent.

### Configure Poetry for CI/CD

When you run `poetry lock` with Package Firewall set as a source, Poetry writes the Firewall URL, including your namespace, into `poetry.lock`. Every package resolves through Endor Labs instead of a canonical PyPI source.

Since the lockfile references Firewall URLs, reproducible installs fail in CI/CD when [credentials](#create-an-api-key-for-the-package-firewall) to `factory.endorlabs.com` are absent. Commands that install from the lockfile, such as `poetry install`, return `401 Unauthorized` unless credentials are provided.

To install from the lockfile in CI/CD without committing credentials to your repository, keep the source URL in `pyproject.toml` and pass the credentials as environment variables.

1. In `pyproject.toml`, add the Package Firewall source with a name and no credentials in the URL. Poetry matches credentials to a source by this name.

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

2. In your CI/CD system, store the credentials as secrets and expose them to jobs as the following environment variables. Poetry derives the variable names from the source name in uppercase, so `endor-firewall` becomes `ENDOR_FIREWALL`. For more information, refer to the [Poetry repositories documentation](https://python-poetry.org/docs/repositories/).

   * `POETRY_HTTP_BASIC_ENDOR_FIREWALL_USERNAME`: Your Package Firewall API key.
   * `POETRY_HTTP_BASIC_ENDOR_FIREWALL_PASSWORD`: Your Package Firewall API secret.

## Next steps

* Configure which packages the firewall flags and how it responds. See [Package Firewall policy](/package-firewall/policy) to learn more.
* Review the events the firewall records. See [View Package Firewall logs](/package-firewall/logs) to learn more.

<draft>
  ## Troubleshooting and FAQ

  <AccordionGroup>
    <Accordion title="Why does Poetry return 401 errors when my environment variables are set?">
      The variables aren't in your current shell scope. On Windows, set them with `[System.Environment]::SetEnvironmentVariable` using the `User` scope, then restart your terminal. For a single session, you can set `$env:POETRY_HTTP_BASIC_ENDOR_FIREWALL_USERNAME` and `$env:POETRY_HTTP_BASIC_ENDOR_FIREWALL_PASSWORD` in PowerShell instead.
    </Accordion>
  </AccordionGroup>
</draft>
