Files
santifer--career-ops/providers/greenhouse.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

77 lines
2.5 KiB
JavaScript

// @ts-check
/** @typedef {import('./_types.js').Provider} Provider */
// Greenhouse provider — hits the public boards-api JSON endpoint.
// Handles both explicit `api:` URLs and auto-detection from `careers_url`.
const ALLOWED_GREENHOUSE_HOSTS = new Set([
'boards-api.greenhouse.io',
'boards.greenhouse.io',
'job-boards.greenhouse.io',
'job-boards.eu.greenhouse.io',
]);
/** @param {string} url */
function assertGreenhouseUrl(url) {
let parsed;
try {
parsed = new URL(url);
} catch {
throw new Error(`greenhouse: invalid URL: ${url}`);
}
if (parsed.protocol !== 'https:') throw new Error(`greenhouse: URL must use HTTPS: ${url}`);
if (!ALLOWED_GREENHOUSE_HOSTS.has(parsed.hostname))
throw new Error(`greenhouse: untrusted hostname "${parsed.hostname}" — must be one of: ${[...ALLOWED_GREENHOUSE_HOSTS].join(', ')}`);
return url;
}
/** @param {import('./_types.js').PortalEntry} entry */
function resolveApiUrl(entry) {
if (entry.api) {
assertGreenhouseUrl(entry.api);
return entry.api;
}
const url = entry.careers_url || '';
const match = url.match(/job-boards(?:\.eu)?\.greenhouse\.io\/([^/?#]+)/);
if (match) return `https://boards-api.greenhouse.io/v1/boards/${match[1]}/jobs`;
return null;
}
// NaN-safe Date.parse — `|| undefined` would also coerce a valid epoch 0.
function toEpochMs(value) {
if (!value) return undefined;
const parsed = Date.parse(value);
return Number.isNaN(parsed) ? undefined : parsed;
}
/** @type {Provider} */
export default {
id: 'greenhouse',
detect(entry) {
try {
const apiUrl = resolveApiUrl(entry);
return apiUrl ? { url: apiUrl } : null;
} catch {
return null;
}
},
async fetch(entry, ctx) {
const apiUrl = resolveApiUrl(entry);
if (!apiUrl) throw new Error(`greenhouse: cannot derive API URL for ${entry.name}`);
assertGreenhouseUrl(apiUrl);
// redirect:'error' prevents SSRF via server-side redirects; combined with
// assertGreenhouseUrl above it guarantees the final hostname stays in the allowlist.
const json = /** @type {any} */ (await ctx.fetchJson(apiUrl, { redirect: 'error' }));
const jobs = Array.isArray(json?.jobs) ? json.jobs : [];
return jobs.filter(/** @param {any} j */ j => j.absolute_url).map(/** @param {any} j */ j => ({
title: j.title || '',
url: j.absolute_url,
company: entry.name,
location: j.location?.name || '',
postedAt: toEpochMs(j.first_published),
}));
},
};