santifer/career-ops · error · Error

senjob: untrusted hostname "${parsed.hostname}" — must be ${

Error message

senjob: untrusted hostname "${parsed.hostname}" — must be ${TRUSTED_HOST}

What it means

All senjob requests are host-pinned to senjob.com (TRUSTED_HOST). assertSenjobUrl throws this error when the parsed hostname is anything else, including subdomains like www.senjob.com (the check is exact equality, not suffix matching). This prevents SSRF and stops the scraper from following a repointed domain.

Source

Thrown at providers/senjob.mjs:81

const HIDDEN_ISO_DATE_RE = /display:\s*none;?\s*"?>\s*(\d{4}-\d{2}-\d{2})\s*</i;

/** @param {any} ctx @param {number} ms */
function sleep(ctx, ms) {
  if (typeof ctx?.sleep === 'function') return ctx.sleep(ms);
  return new Promise((r) => setTimeout(r, ms));
}

/** @param {string} url */
function assertSenjobUrl(url) {
  let parsed;
  try {
    parsed = new URL(url);
  } catch {
    throw new Error(`senjob: invalid URL: ${url}`);
  }
  if (parsed.protocol !== 'https:') throw new Error(`senjob: URL must use HTTPS: ${url}`);
  if (parsed.hostname !== TRUSTED_HOST) {
    throw new Error(`senjob: untrusted hostname "${parsed.hostname}" — must be ${TRUSTED_HOST}`);
  }
  return url;
}

/**
 * Collapse a markup fragment to its visible text.
 * Comments are stripped FIRST: the anchor bodies carry `<!-- d ico postulez -->`
 * between the title and a spacer image, and a naive tag strip would leave the
 * comment body sitting inside the title.
 * @param {string} fragment
 * @returns {string}
 */
export function visibleText(fragment) {
  return decodeEntities(
    String(fragment ?? '')
      .replace(/<!--[\s\S]*?-->/g, ' ')
      .replace(/<[^>]+>/g, ' '),
  )

View on GitHub (pinned to 1696bec4d0)

Solutions

  1. Use the exact host senjob.com — build URLs with buildListUrl(page) so the host is fixed
  2. Do not pre-resolve redirects; the provider fetches with redirect:'error' and asserts before fetching
  3. If the board moves to another canonical host, update TRUSTED_HOST/LIST_URL in providers/senjob.mjs and re-verify

Example fix

// before
assertSenjobUrl('https://www.senjob.com/offres-d-emploi.php');
// after
assertSenjobUrl('https://senjob.com/offres-d-emploi.php');
Defensive patterns

Strategy: validation

Validate before calling

const TRUSTED_HOST = 'senjob.com';
function isSenjobHostUrl(url) {
  try { return new URL(url).hostname === TRUSTED_HOST; } catch { return false; }
}
if (!isSenjobHostUrl(url)) throw new Error(`refusing non-senjob.com host: ${url}`);

Type guard

function isSenjobHostUrl(url) {
  try { return new URL(url).hostname === 'senjob.com'; } catch { return false; }
}

Try / catch

try {
  assertSenjobUrl(url);
} catch (err) {
  if (String(err.message).startsWith('senjob: untrusted hostname')) {
    console.error(`Host pinning refused ${url} — only senjob.com (exact) is allowed`);
    return null;
  }
  throw err;
}

Prevention

When it happens

Trigger: assertSenjobUrl called with a URL on another host: www.senjob.com, a mirror/staging host, a typosquatted domain, or a URL rewritten from a redirect target. Exact-match means even legitimate subdomains are refused.

Common situations: Assuming www.senjob.com is equivalent to senjob.com; configuring a national variant domain; custom code following an http redirect to another host before assertion.

Related errors


AI-assisted analysis of santifer/career-ops@1696bec4d0 (2026-09-01). Data as JSON: /api/errors/84c91586037dc99d. Report an issue: GitHub.