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

132 lines
5.0 KiB
JavaScript

// @ts-check
/** @typedef {import('./_types.js').Provider} Provider */
// SmartRecruiters provider — hits the public postings API.
// Auto-detects from careers_url pattern
// `https://(careers|jobs).smartrecruiters.com/<slug>`. A tracked_companies
// entry can also set `provider: smartrecruiters` explicitly to bypass
// detection (useful when the public careers URL is a branded custom domain).
const ALLOWED_SMARTRECRUITERS_HOSTS = new Set(['api.smartrecruiters.com']);
const SR_CAREERS_HOSTS = new Set(['careers.smartrecruiters.com', 'jobs.smartrecruiters.com']);
const SR_PAGE_SIZE = 100;
const SR_MAX_PAGES = 50; // safety cap (5000 postings @ 100/page)
function assertSmartRecruitersUrl(url) {
let parsed;
try {
parsed = new URL(url);
} catch {
throw new Error(`smartrecruiters: invalid URL: ${url}`);
}
if (parsed.protocol !== 'https:') throw new Error(`smartrecruiters: URL must use HTTPS: ${url}`);
if (!ALLOWED_SMARTRECRUITERS_HOSTS.has(parsed.hostname)) {
throw new Error(`smartrecruiters: untrusted hostname "${parsed.hostname}" — must be one of: ${[...ALLOWED_SMARTRECRUITERS_HOSTS].join(', ')}`);
}
return url;
}
function resolveSlug(entry) {
// entry.api takes precedence over careers_url (mirrors greenhouse/ashby) so a
// branded page (e.g. https://jobs.continental.com) can stay as careers_url
// while the SmartRecruiters slug is pinned via
// api: https://careers.smartrecruiters.com/<slug> in portals.yml.
for (const raw of [entry.api, entry.careers_url]) {
if (typeof raw !== 'string' || !raw) continue;
let parsed;
try {
parsed = new URL(raw);
} catch {
continue;
}
if (parsed.protocol !== 'https:') continue;
if (!SR_CAREERS_HOSTS.has(parsed.hostname)) continue;
const slug = parsed.pathname.split('/').filter(Boolean)[0];
if (slug) return slug;
}
return null;
}
function buildPostingsUrl(slug, offset = 0) {
return `https://api.smartrecruiters.com/v1/companies/${slug}/postings?limit=${SR_PAGE_SIZE}&offset=${offset}&status=PUBLIC`;
}
function resolveApiUrl(entry) {
const slug = resolveSlug(entry);
return slug ? buildPostingsUrl(slug, 0) : null;
}
/** @type {Provider} */
export default {
id: 'smartrecruiters',
detect(entry) {
const apiUrl = resolveApiUrl(entry);
return apiUrl ? { url: apiUrl } : null;
},
async fetch(entry, ctx) {
const slug = resolveSlug(entry);
if (!slug) throw new Error(`smartrecruiters: cannot derive API URL for ${entry.name}`);
const all = [];
for (let page = 0; page < SR_MAX_PAGES; page++) {
const apiUrl = buildPostingsUrl(slug, page * SR_PAGE_SIZE);
assertSmartRecruitersUrl(apiUrl);
const json = await ctx.fetchJson(apiUrl, { redirect: 'error' });
const parsed = parseSmartRecruitersResponse(json, entry.name);
if (parsed.length === 0) break;
all.push(...parsed);
if (parsed.length < SR_PAGE_SIZE) break; // last page (short)
}
return all;
},
};
/**
* Parse a SmartRecruiters /postings response. Exported for unit tests.
*
* SmartRecruiters returns:
* { content: [{ id, name, ref, location: { fullLocation?, city?, region?, country?, remote? } }] }
*
* - location: prefer `fullLocation`; else assemble from city/region/country
* parts (skipping empties); append "Remote" when `location.remote` is true.
* - url: `j.ref` is an `api.smartrecruiters.com/v1/companies/<slug>/postings/<id>`
* URL — rewrite to the public `jobs.smartrecruiters.com/<slug>/postings/<id>`.
* If `ref` is missing, synthesise a URL from the company slug + posting id.
*
* @param {any} json
* @param {string} companyName
* @returns {Array<{title: string, url: string, company: string, location: string}>}
*/
export function parseSmartRecruitersResponse(json, companyName) {
const items = json?.content;
if (!Array.isArray(items)) return [];
return items.map(j => {
const loc = j.location || {};
const fullLocation = loc.fullLocation || [loc.city, loc.region, loc.country].filter(Boolean).join(', ');
const remote = loc.remote ? 'Remote' : '';
const location = [fullLocation, remote].filter(Boolean).join(', ');
const slugified = (j.name || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
let url = '';
if (typeof j.ref === 'string') {
let parsedRef;
try { parsedRef = new URL(j.ref); } catch { parsedRef = null; }
if (parsedRef
&& parsedRef.protocol === 'https:'
&& parsedRef.hostname === 'api.smartrecruiters.com'
&& parsedRef.pathname.startsWith('/v1/companies/')) {
const restOfPath = parsedRef.pathname.slice('/v1/companies/'.length);
url = `https://jobs.smartrecruiters.com/${restOfPath}`;
}
}
if (!url && j.id) {
const companySlug = (companyName || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
if (companySlug) {
url = `https://jobs.smartrecruiters.com/${companySlug}/${j.id}-${slugified}`;
}
}
return { title: j.name || '', url, location, company: companyName };
});
}