Files
santifer--career-ops/providers/recruitee.mjs
T
wehub-resource-sync d083df1fdb
CodeQL Analysis / Analyze (javascript-typescript) (push) Failing after 2s
Web CI / web typecheck + build (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
CodeQL Analysis / Analyze (go) (push) Failing after 16s
chore: import upstream snapshot with attribution
2026-07-13 12:02:43 +08:00

115 lines
4.0 KiB
JavaScript

// @ts-check
/** @typedef {import('./_types.js').Provider} Provider */
// Recruitee provider — hits the public per-tenant offers API.
// Auto-detects from careers_url pattern `https://<slug>.recruitee.com`.
// Per-tenant subdomains are the variable part — SSRF defence uses a
// regex match on `<safe-slug>.recruitee.com` rather than a static
// allowlist.
const RECRUITEE_HOST_RE = /^[a-z0-9][a-z0-9-]*\.recruitee\.com$/;
function assertRecruiteeUrl(url) {
let parsed;
try {
parsed = new URL(url);
} catch {
throw new Error(`recruitee: invalid URL: ${url}`);
}
if (parsed.protocol !== 'https:') throw new Error(`recruitee: URL must use HTTPS: ${url}`);
if (!RECRUITEE_HOST_RE.test(parsed.hostname)) {
throw new Error(`recruitee: untrusted hostname "${parsed.hostname}" — must match <slug>.recruitee.com`);
}
return url;
}
function resolveApiUrl(entry) {
const raw = typeof entry.careers_url === 'string' ? entry.careers_url : '';
if (!raw) return null;
let parsed;
try {
parsed = new URL(raw);
} catch {
return null;
}
if (parsed.protocol !== 'https:') return null;
if (!RECRUITEE_HOST_RE.test(parsed.hostname)) return null;
return `https://${parsed.hostname}/api/offers/`;
}
/** @type {Provider} */
export default {
id: 'recruitee',
detect(entry) {
const apiUrl = resolveApiUrl(entry);
return apiUrl ? { url: apiUrl } : null;
},
async fetch(entry, ctx) {
const apiUrl = resolveApiUrl(entry);
if (!apiUrl) throw new Error(`recruitee: cannot derive API URL for ${entry.name}`);
assertRecruiteeUrl(apiUrl);
const json = await ctx.fetchJson(apiUrl, { redirect: 'error' });
return parseRecruiteeResponse(json, entry.name);
},
};
/**
* Parse a Recruitee /api/offers/ response. Exported for unit tests.
*
* Recruitee returns:
* { offers: [{ title, careers_url?, url?, city?, country?, remote?, location? }] }
*
* - url: prefer `careers_url`, fall back to `url`. Recruitee tenants commonly
* serve postings on their own custom domain (e.g. `careers.hostaway.com`),
* so this URL is NOT host-locked to `*.recruitee.com`. Unlike the API
* endpoint, the per-offer URL is display-only — it is written to the pipeline
* and scan history but never server-fetched here, so the SSRF rationale does
* not apply. It is sourced from the already-validated tenant API response.
* Requirement: a well-formed `https:` URL; a non-HTTPS or malformed URL is
* dropped (empty string returned per the Job contract).
* - location: prefer the explicit `location` field; else assemble from
* city/country, appending "Remote" when `remote` is true.
*
* @param {any} json
* @param {string} companyName
* @returns {Array<{title: string, url: string, company: string, location: string}>}
*/
export function parseRecruiteeResponse(json, companyName) {
const offers = json?.offers;
if (!Array.isArray(offers)) return [];
return offers.map(j => {
const city = j.city || '';
const country = j.country || '';
const remote = j.remote ? 'Remote' : '';
const location = j.location || [city, country, remote].filter(Boolean).join(', ');
// Resolve offer URL. Recruitee tenants commonly publish postings on their
// own custom domain (e.g. careers.hostaway.com), so the per-offer URL is
// NOT host-locked to *.recruitee.com — it is display-only (recorded in the
// pipeline/history, never server-fetched here) and comes from the already-
// validated tenant API response. Require only a well-formed https: URL;
// a non-https or malformed URL is dropped (empty string per the Job contract).
let url = '';
const rawUrl = j.careers_url || j.url || '';
if (typeof rawUrl === 'string' && rawUrl) {
try {
const parsed = new URL(rawUrl);
if (parsed.protocol === 'https:') {
url = parsed.href;
}
} catch {
// malformed URL → leave url = ''
}
}
return {
title: j.title || '',
url,
location,
company: companyName,
};
});
}