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

151 lines
5.9 KiB
JavaScript

// @ts-check
/** @typedef {import('./_types.js').Provider} Provider */
// Comeet (Spark Hire Recruit) provider — hits the public, no-auth careers API.
//
// The positions endpoint needs BOTH a company-uid and a per-tenant token:
// https://www.comeet.co/careers-api/2.0/company/<uid>/positions?token=<token>
// Neither is derivable from a branded careers URL, so unlike greenhouse/recruitee
// there is no slug→API shortcut: the full API URL must be supplied via the
// `api:` field (or pasted into `careers_url`). The API host is a single fixed
// origin, so the SSRF defence pins hostname to www.comeet.co AND requires the
// /careers-api/ path prefix (rather than a per-tenant subdomain regex).
const COMEET_API_HOST = 'www.comeet.co';
/** @param {unknown} raw */
function isComeetApiUrl(raw) {
if (typeof raw !== 'string' || !raw) return false;
let parsed;
try {
parsed = new URL(raw);
} catch {
return false;
}
return parsed.protocol === 'https:' && parsed.hostname === COMEET_API_HOST && parsed.pathname.startsWith('/careers-api/');
}
/** @param {string} url */
function assertComeetUrl(url) {
let parsed;
try {
parsed = new URL(url);
} catch {
throw new Error(`comeet: invalid URL: ${redactToken(url)}`);
}
if (parsed.protocol !== 'https:') throw new Error(`comeet: URL must use HTTPS: ${redactToken(url)}`);
if (parsed.hostname !== COMEET_API_HOST)
throw new Error(`comeet: untrusted hostname "${parsed.hostname}" — must be ${COMEET_API_HOST}`);
if (!parsed.pathname.startsWith('/careers-api/'))
throw new Error(`comeet: URL path must be the careers-api endpoint: ${redactToken(url)}`);
return url;
}
// Redact the per-tenant ?token= so neither the (informational, possibly-logged)
// DetectHit url nor a thrown validation error carries the secret. Best-effort:
// falls back to a regex strip when the value can't be parsed as a URL.
function redactToken(url) {
try {
const parsed = new URL(url);
if (parsed.searchParams.has('token')) parsed.searchParams.set('token', 'REDACTED');
return parsed.href;
} catch {
return typeof url === 'string' ? url.replace(/([?&]token=)[^&#]*/gi, '$1REDACTED') : url;
}
}
/** @param {import('./_types.js').PortalEntry} entry */
function resolveApiUrl(entry) {
if (isComeetApiUrl(entry.api)) return entry.api;
// Fall back to careers_url only when it already IS the full careers-api URL
// (the branded www.comeet.com/jobs/... page carries no token, so it can't be
// turned into an API call).
if (isComeetApiUrl(entry.careers_url)) return entry.careers_url;
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: 'comeet',
detect(entry) {
const apiUrl = resolveApiUrl(entry);
// The DetectHit url is informational (the framework may log it), so strip
// the secret ?token= before returning it — fetch() re-resolves the real
// URL from the entry, so redaction here is safe.
return apiUrl ? { url: redactToken(apiUrl) } : null;
},
async fetch(entry, ctx) {
const apiUrl = resolveApiUrl(entry);
if (!apiUrl) throw new Error(`comeet: cannot derive API URL for ${entry.name} (set api: to the full careers-api positions URL)`);
assertComeetUrl(apiUrl);
// redirect:'error' prevents SSRF via server-side redirects; combined with
// assertComeetUrl above it guarantees the final hostname stays www.comeet.co.
const json = await ctx.fetchJson(apiUrl, { redirect: 'error' });
return parseComeetResponse(json, entry.name);
},
};
/**
* Parse a Comeet careers-api positions response. Exported for unit tests.
*
* Comeet returns a top-level ARRAY of position objects:
* [{ name, location: { name, is_remote }, url_active_page,
* url_comeet_hosted_page, time_updated, ... }]
*
* - url: prefer `url_active_page` (the tenant's live careers page), fall back to
* `url_comeet_hosted_page` (the Comeet-hosted page). Both are public, display-
* only URLs (recorded in the pipeline/history, never server-fetched here), so
* they are NOT host-locked. Require a well-formed https: URL; a position whose
* URL is missing/non-https/malformed is dropped (url is the dedup key).
* - location: `location.name`, appending "Remote" when `location.is_remote`.
*
* @param {any} json
* @param {string} companyName
* @returns {Array<{title: string, url: string, company: string, location: string, postedAt?: number}>}
*/
export function parseComeetResponse(json, companyName) {
const positions = Array.isArray(json) ? json : [];
return positions
.map(row => {
// Coerce each row to a safe object so null / non-object members can't throw.
const j = (row && typeof row === 'object') ? row : {};
// Resolve a display-only https URL; drop the position if none is usable.
let url = '';
const rawUrl = j.url_active_page || j.url_comeet_hosted_page || '';
if (typeof rawUrl === 'string' && rawUrl) {
try {
const parsed = new URL(rawUrl);
if (parsed.protocol === 'https:') url = parsed.href;
} catch {
// malformed URL → leave url = ''
}
}
const loc = j.location || {};
const remote = loc.is_remote ? 'Remote' : '';
const base = (typeof loc.name === 'string' && loc.name.trim()) ? loc.name.trim() : '';
// Append "Remote" only when the base location doesn't already say so.
const location = remote && !/remote/i.test(base)
? [base, remote].filter(Boolean).join(', ')
: base;
return {
title: (typeof j.name === 'string' ? j.name.trim() : ''),
url,
location,
company: companyName,
postedAt: toEpochMs(j.time_updated),
};
})
.filter(job => job.title && job.url);
}