/** * Ashby ATS Adapter * * Interacts with public Ashby job board posting APIs: * https://api.ashbyhq.com/posting-api/job-board/{board_token} */ import { IJobSourceAdapter, DiscoveryResult, SourceHealth, RateLimitPolicy, NormalizedJobRecord } from "../types"; import { generateContentFingerprint, normalizeJobUrl, classifyExperienceLevel } from "../deduplication"; export class AshbySourceAdapter implements IJobSourceAdapter { readonly id: string; readonly name: string; readonly sourceType = "ATS" as const; readonly provider = "ashby" as const; private boardToken: string; constructor(boardToken: string, companyName?: string) { this.boardToken = boardToken.toLowerCase().trim(); this.name = companyName || `Ashby (${boardToken})`; this.id = `ashby_${this.boardToken}`; } async healthCheck(): Promise { const start = Date.now(); try { const url = `https://api.ashbyhq.com/posting-api/job-board/${this.boardToken}`; 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 { const limit = options.limit || 50; const errors: string[] = []; const normalizedJobs: NormalizedJobRecord[] = []; try { const url = `https://api.ashbyhq.com/posting-api/job-board/${this.boardToken}`; const res = await fetch(url, { headers: { "User-Agent": "JobsBoard-Acquisition/1.0" } }); if (!res.ok) { throw new Error(`Ashby API error HTTP ${res.status}: ${res.statusText}`); } const data = await res.json(); const rawList = data.jobs || []; for (const item of rawList.slice(0, limit)) { try { const title = (item.title || "").trim(); const rawJobUrl = item.jobUrl || `https://jobs.ashbyhq.com/${this.boardToken}/${item.id}`; if (!title) continue; const jobUrl = normalizeJobUrl(rawJobUrl); const location = item.location || (item.isRemote ? "Remote, USA" : "United States"); const isRemote = !!item.isRemote; const deptName = item.department || "Software & Engineering"; const cleanDesc = (item.descriptionPlain || item.descriptionHtml || "") .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 Ashby.`, jobUrl, datePosted: item.publishedAt ? new Date(item.publishedAt) : new Date(), source: "ashby", sourceId: this.id, fingerprintHash: fingerprint, rawPayload: item, }); } catch (itemErr: any) { errors.push(`Failed to parse Ashby 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, }; } }