> ## 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 relying on an intermediary registry.

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 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 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, 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: []
      }
    },
    notes: []
  }, {
    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: []
      }
    },
    notes: []
  }, {
    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.']
  }, {
    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]
      }
    },
    notes: [POETRY_NAME_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{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.</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 = "package manager"

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

Direct integration routes package installation requests from your package managers through the Package Firewall directly, without an intermediary registry such as JFrog Artifactory. 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` with the block reason when the package is in the Endor Labs malware database, or a policy condition matches **Block**. 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 returns one of the following reasons 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 registry such as JFrog Artifactory.

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.

For details about blocked and warned packages, see [View Package Firewall logs](/package-firewall/logs).

## Package Firewall support matrix

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

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

    - Package_manager: pnpm
    Ecosystem: JavaScript, TypeScript
    Registry: npm
    Configuration_files: \`.npmrc\`

    - Package_manager: Yarn Classic
    Ecosystem: JavaScript, TypeScript
    Registry: npm
    Configuration_files: \`.npmrc\`, \`.yarnrc\`

    - Package_manager: Yarn Berry
    Ecosystem: JavaScript, TypeScript
    Registry: npm
    Configuration_files: \`.yarnrc.yml\`

    - Package_manager: Bun
    Ecosystem: JavaScript, TypeScript
    Registry: npm
    Configuration_files: \`.npmrc\`, \`bunfig.toml\`

    - Package_manager: pip
    Ecosystem: Python
    Registry: PyPI
    Configuration_files: \`pip.conf\`, \`pip.ini\`

    - Package_manager: Poetry
    Ecosystem: Python
    Registry: PyPI
    Configuration_files: \`pyproject.toml\`

    - Package_manager: uv
    Ecosystem: Python
    Registry: PyPI
    Configuration_files: \`uv.toml\`, \`pyproject.toml\`

    - Package_manager: Go
    Ecosystem: Go
    Registry: Go
    Configuration_files:

    - Package_manager: Maven
    Ecosystem: Java
    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.

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

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