JB/web/src/lib/sources/adapters/lever.ts

125 lines
4.1 KiB
TypeScript

/**
* Lever ATS Adapter
*
* Interacts with public Lever job board APIs:
* https://api.lever.co/v0/postings/{board_token}?mode=json
*/
import { IJobSourceAdapter, DiscoveryResult, SourceHealth, RateLimitPolicy, NormalizedJobRecord } from "../types";
import { generateContentFingerprint, normalizeJobUrl, classifyExperienceLevel } from "../deduplication";
export class LeverSourceAdapter implements IJobSourceAdapter {
readonly id: string;
readonly name: string;
readonly sourceType = "ATS" as const;
readonly provider = "lever" as const;
private boardToken: string;
constructor(boardToken: string, companyName?: string) {
this.boardToken = boardToken.toLowerCase().trim();
this.name = companyName || `Lever (${boardToken})`;
this.id = `lever_${this.boardToken}`;
}
async healthCheck(): Promise<SourceHealth> {
const start = Date.now();
try {
const url = `https://api.lever.co/v0/postings/${this.boardToken}?mode=json`;
const res = await fetch(url, { headers: { "User-Agent": "JobsBoard-Acquisition/1.0" } });
return {
healthy: res.ok,
statusCode: res.status,
latencyMs: Date.now() - start,
errorMessage: res.ok ? undefined : `HTTP ${res.status}: ${res.statusText}`,
};
} catch (err: any) {
return {
healthy: false,
latencyMs: Date.now() - start,
errorMessage: err.message,
};
}
}
getRateLimitPolicy(): RateLimitPolicy {
return {
requestsPerMinute: 60,
burstLimit: 10,
retryAfterMs: 2000,
};
}
async discoverJobs(options: { limit?: number } = {}): Promise<DiscoveryResult> {
const limit = options.limit || 50;
const errors: string[] = [];
const normalizedJobs: NormalizedJobRecord[] = [];
try {
const url = `https://api.lever.co/v0/postings/${this.boardToken}?mode=json`;
const res = await fetch(url, { headers: { "User-Agent": "JobsBoard-Acquisition/1.0" } });
if (!res.ok) {
throw new Error(`Lever API error HTTP ${res.status}: ${res.statusText}`);
}
const rawList = await res.json();
if (!Array.isArray(rawList)) {
throw new Error(`Unexpected Lever API response format (expected array)`);
}
for (const item of rawList.slice(0, limit)) {
try {
const title = (item.text || "").trim();
const rawJobUrl = item.hostedUrl || "";
if (!title || !rawJobUrl) continue;
const jobUrl = normalizeJobUrl(rawJobUrl);
const categories = item.categories || {};
const location = categories.location || "Remote, USA";
const workplaceType = categories.workplaceType || "";
const isRemote =
workplaceType.toLowerCase().includes("remote") ||
location.toLowerCase().includes("remote") ||
title.toLowerCase().includes("remote");
const deptName = categories.department || "Software & Engineering";
const cleanDesc = (item.descriptionPlain || item.description || "")
.replace(/<[^>]+>/g, " ")
.replace(/\s+/g, " ")
.trim();
const fingerprint = generateContentFingerprint(this.name, title, location, isRemote);
normalizedJobs.push({
title,
company: this.name,
location,
isRemote,
department: deptName,
experienceLevel: classifyExperienceLevel(title),
description: cleanDesc.slice(0, 5000) || `Position at ${this.name}. Apply on Lever.`,
jobUrl,
datePosted: item.createdAt ? new Date(item.createdAt) : new Date(),
source: "lever",
sourceId: this.id,
fingerprintHash: fingerprint,
rawPayload: item,
});
} catch (itemErr: any) {
errors.push(`Failed to parse Lever job ${item.id}: ${itemErr.message}`);
}
}
} catch (err: any) {
errors.push(`Discovery failed for ${this.id}: ${err.message}`);
}
return {
sourceId: this.id,
provider: this.provider,
totalDiscovered: normalizedJobs.length,
jobs: normalizedJobs,
errors,
};
}
}