santifer/career-ops · error

refusing to archive restricted destination: ${preGuard.reaso

Error message

refusing to archive restricted destination: ${preGuard.reason}

What it means

Thrown by archiveUrl() in archive-posting.mjs before any Playwright navigation starts. It runs rejectPrivateOrInvalid() (exported from liveness-browser.mjs) — the repo's SSRF/egress guard — which rejects (a) unparseable URLs, (b) non-http(s) protocols, and (c) hosts matching private/loopback ranges: localhost, 0.0.0.0, 127/8, 10/8, 172.16-31, 192.168/16, 169.254/16, ::1, ::, fc00::/7 (fc..), link-local fe80::, including ::ffff:-mapped IPv4 forms. The appended reason is one of: 'invalid URL', 'unsupported protocol <p>', 'blocked host <h>'.

Source

Thrown at archive-posting.mjs:310

    try {
      await validateUrlSecurity(requestUrl);
      return route.continue();
    } catch (err) {
      console.warn(`   Blocked request to restricted destination (DNS): ${requestUrl} - ${err.message}`);
      return route.abort('blockedbyclient');
    }
  });
}

export async function archiveUrl(browser, url, { company: companyHint, role: roleHint } = {}) {
  console.log(`\n🔗  ${url}`);

  // Refuse before launching any navigation, so an obviously-internal target
  // never reaches Playwright at all.
  const preGuard = rejectPrivateOrInvalid(url);
  if (preGuard) {
    throw new Error(`refusing to archive restricted destination: ${preGuard.reason}`);
  }

  const context = await browser.newContext();
  await installEgressGuard(context);
  const page = await context.newPage();

  try {
    const response = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
    const httpStatus = response?.status() ?? 0;

    // Re-check where we actually landed. The route guard already inspects every
    // redirect hop, so this is defence-in-depth: a first-hop-only check is the
    // classic miss here, and asserting on the settled URL costs nothing.
    const landedUrl = page.url();
    const postGuard = rejectPrivateOrInvalid(landedUrl);
    if (postGuard) {
      throw new Error(`refusing to archive restricted destination after redirect: ${postGuard.reason}`);
    }

View on GitHub (pinned to 60398d6549)

Solutions

  1. Archive the public https:// URL of the posting instead of an internal mirror
  2. If the URL lost its scheme, re-add it: https://<host>/<path> — scheme-less strings fail URL parsing outright
  3. For genuinely internal portals the guard is by design: save the page manually (print to PDF) into jds/ instead of routing it through archive-posting
  4. Never 'fix' this by pointing at a public DNS name that resolves to a private IP — the DNS-level egress guard will still block it

Example fix

# before
node archive-posting.mjs http://localhost:3000/jobs/123

# after
node archive-posting.mjs https://boards.greenhouse.io/acme/jobs/123
Defensive patterns

Strategy: validation

Validate before calling

import { rejectPrivateOrInvalid } from './liveness-browser.mjs';
function assertPublicHttpUrl(url) {
  const rejected = rejectPrivateOrInvalid(url);
  if (rejected) throw new Error(`URL refused (${rejected.code}): ${url}`);
}
assertPublicHttpUrl(url);
await archiveUrl(browser, url);

Type guard

function isArchivableUrl(url) {
  try { return rejectPrivateOrInvalid(url) === null; } catch { return false; }
}

Prevention

When it happens

Trigger: `node archive-posting.mjs http://10.0.0.7/jobs/1` or any RFC1918/link-local/localhost target; ftp:// or file:// URLs; a pasted URL missing its scheme ('boards.greenhouse.io/jobs/123' fails new URL() → 'invalid URL').

Common situations: Testing against a locally running ATS or a staging instance on an internal network; intranet job boards that are private by nature; stripping the https:// prefix while pasting; corporate portals behind link-local addresses.

Related errors


AI-assisted analysis of santifer/career-ops@60398d6549 (2026-08-20). Data as JSON: /api/errors/9928ffc8581a53ce. Report an issue: GitHub.