nexu-io/open-design · error · Error

public_github_repository_metric only supports https://api.gi

Error message

public_github_repository_metric only supports https://api.github.com repository URLs

What it means

Thrown by selectGithubRepositoryApiUrl after the URL parses successfully but its protocol is not 'https:' or its hostname is not exactly 'api.github.com'. This is a defense-in-depth allow-list: the daemon will only ever issue requests to the GitHub REST API origin, never to arbitrary hosts or the github.com web frontend. It prevents SSRF and credential leakage via attacker-supplied URLs.

Source

Thrown at apps/daemon/src/live-artifacts/refresh.ts:661

    status: compactExecOutput(status),
    recentCommits: compactExecOutput(recentCommits),
    diffStat: compactExecOutput(diffStat),
  });
}

function selectGithubRepositoryApiUrl(input: PublicGithubRepositoryMetricInput): URL {
  const rawUrl = optionalString(input.url, 'input.url');
  if (rawUrl === undefined) throw new Error('public_github_repository_metric requires input.url');

  let url: URL;
  try {
    url = new URL(rawUrl);
  } catch {
    throw new Error('public_github_repository_metric input.url must be a valid URL');
  }

  if (url.protocol !== 'https:' || url.hostname !== 'api.github.com') {
    throw new Error('public_github_repository_metric only supports https://api.github.com repository URLs');
  }
  if (!/^\/repos\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(url.pathname)) {
    throw new Error('public_github_repository_metric only supports /repos/{owner}/{repo} URLs');
  }
  url.search = '';
  url.hash = '';
  url.username = '';
  url.password = '';
  return url;
}

function selectGithubFields(input: PublicGithubRepositoryMetricInput): string[] {
  if (input.fields === undefined) return ['stargazers_count', 'full_name', 'html_url', 'updated_at'];
  if (!Array.isArray(input.fields)) throw new Error('input.fields must be an array of strings');
  const fields = input.fields.filter((field): field is string => typeof field === 'string');
  if (fields.length !== input.fields.length) throw new Error('input.fields must be an array of strings');
  return fields.slice(0, 20);
}

View on GitHub (pinned to 5be4028344)

Solutions

  1. Convert the web URL to its API form: replace host 'github.com' with 'api.github.com' and prefix path with '/repos' if missing.
  2. Ensure the scheme is exactly 'https:' (not 'http:').
  3. Do not attempt to point this tool at a GitHub Enterprise host; the allow-list intentionally excludes enterprise origins.

Example fix

// before
input: { url: 'https://github.com/octocat/Hello-World' }
// after
input: { url: 'https://api.github.com/repos/octocat/Hello-World' }
Defensive patterns

Strategy: validation

Validate before calling

function toGithubApiUrl(repoUrl: string): URL {
  const u = new URL(repoUrl);
  if (u.hostname === 'github.com' && /^\/[\w.-]+\/[\w.-]+$/.test(u.pathname)) {
    return new URL(`https://api.github.com/repos${u.pathname}`);
  }
  if (u.protocol === 'https:' && u.hostname === 'api.github.com') return u;
  throw new Error('not a GitHub API URL');
}

Type guard

function isGithubApiUrl(v: unknown): v is string {
  if (typeof v !== 'string') return false;
  try { const u = new URL(v); return u.protocol === 'https:' && u.hostname === 'api.github.com'; } catch { return false; }
}

Prevention

When it happens

Trigger: input.url is a valid URL but points at 'https://github.com/...', 'http://api.github.com/...' (wrong scheme), a lookalike host like 'https://api.github.com.evil.com/...', or a non-GitHub host entirely.

Common situations: Model supplies the repository's HTML URL (github.com) instead of the API URL (api.github.com); developer pastes from browser address bar; proxy or staging environment replaces the host; confusion between REST API and GraphQL endpoints.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/788454e764e55a7e. Report an issue: GitHub.