/** * @jobo/scraper-sdk — Core Data Models * * These types define the shape of data that scrapers produce. * They map 1:1 with the C# models consumed by the Jobo platform. */ /** * A metadata value that can be either a simple string or a structured object. * * Use simple strings for scalar values (e.g. `"Full-time"`). * Use objects for structured data (e.g. `{ min: 80000, max: 120000, currency: "USD" }`). * * @example * ```ts * const meta: MetadataMap = { * employmentType: "Full-time", * compensation: { min: 80000, max: 120000, currency: "USD", period: "yearly" }, * departments: ["Engineering", "Product"], * }; * ``` */ type MetadataValue = string | number | boolean | null | MetadataValue[] | { [key: string]: MetadataValue; }; /** * A key-value dictionary of arbitrary metadata attached to a listing or job detail. * * Keys should use camelCase and be descriptive (e.g. `employmentType`, `experienceLevel`). * The platform indexes these for search and filtering. */ type MetadataMap = Record; /** * A geographic location associated with a job posting. * * At minimum, provide the raw `text` as it appears on the page. * The platform's location pipeline will normalize and geocode it. * Structured fields are optional but improve accuracy. * * @example * ```ts * // Minimal * { text: "San Francisco, CA" } * * // Structured * { text: "San Francisco, CA", city: "San Francisco", state: "CA", country: "US" } * * // Remote * { text: "Remote (US)", isRemote: true, country: "US" } * ``` */ interface JobLocation { /** Raw location string as displayed on the page. Always required. */ text: string; /** Parsed city name, if available. */ city?: string; /** Parsed state / province / region, if available. */ state?: string; /** Parsed country name or ISO 3166-1 code, if available. */ country?: string; /** Parsed postal / zip code, if available. */ postalCode?: string; /** Whether this position is remote-eligible. */ isRemote?: boolean; } /** * A single job listing discovered on a listing or search results page. * * This is the **minimum** data extracted during the listing-scrape phase. * For full-details scrapers, all required fields must be populated here. * For split scrapers, only `applyUrl` is strictly required — the rest * will be populated during the detail-scrape phase. * * @example * ```ts * // Full-details scraper — everything available on the listing page * { * externalId: "job-12345", * title: "Senior Software Engineer", * listingUrl: "https://ats.example.com/jobs/12345", * applyUrl: "https://ats.example.com/jobs/12345/apply", * description: "

We are looking for...

", * locations: [{ text: "San Francisco, CA" }], * postedAt: "2025-01-15T00:00:00Z", * meta: { employmentType: "Full-time", department: "Engineering" }, * } * * // Split scraper — minimal listing, details fetched separately * { * externalId: "job-12345", * applyUrl: "https://ats.example.com/jobs/12345/apply", * listingUrl: "https://ats.example.com/jobs/12345", * title: "Senior Software Engineer", * } * ``` */ interface JobListing { /** * ATS-specific unique identifier for this job. * Must be deterministic — the same job must always produce the same ID. * Used for deduplication across scrape runs. */ externalId: string; /** * Direct URL to apply for this job. * **Required for all scraper types.** This is the minimum viable field. */ applyUrl: string; /** * URL of the job listing / detail page (may differ from applyUrl). * Required for full-details scrapers; recommended for split scrapers. */ listingUrl: string; /** Job title. Required for full-details scrapers. */ title?: string; /** * Full job description, preferably as sanitized HTML. * Required for full-details scrapers. */ description?: string; /** One or more locations for this job. Required for full-details scrapers. */ locations?: JobLocation[]; /** Company name, if available at the listing level. */ companyName?: string; /** ISO 8601 date string when the job was posted. */ postedAt?: string; /** ISO 8601 date string when the job expires / closes. */ closesAt?: string; /** * Arbitrary metadata extracted from the listing page. * Common keys: `employmentType`, `experienceLevel`, `department`, * `salary`, `workplaceType`, `benefits`, `tags`. */ meta?: MetadataMap; } /** * Full job details — the complete picture of a single job posting. * * Returned by `scrapeJobDetails()` on split scrapers, or directly * populated in the listing for full-details scrapers. * * All fields from {@link JobListing} are inherited and should be populated. * Additional detail-only fields are available here. * * @example * ```ts * { * externalId: "job-12345", * title: "Senior Software Engineer", * listingUrl: "https://ats.example.com/jobs/12345", * applyUrl: "https://ats.example.com/jobs/12345/apply", * description: "

We are looking for a senior engineer...

", * locations: [{ text: "San Francisco, CA", city: "San Francisco", state: "CA", country: "US" }], * companyName: "Acme Corp", * postedAt: "2025-01-15T00:00:00Z", * meta: { * employmentType: "Full-time", * experienceLevel: "Senior", * department: "Engineering", * compensation: { min: 150000, max: 200000, currency: "USD", period: "yearly" }, * workplaceType: "Hybrid", * benefits: ["Health Insurance", "401k", "Unlimited PTO"], * }, * } * ``` */ interface JobDetails extends JobListing { /** * Job title. Required on details. * Overrides to make it non-optional at the detail level. */ title: string; /** * Full job description as sanitized HTML. Required on details. */ description: string; /** * One or more locations. Required on details. */ locations: JobLocation[]; } /** * Classified outcome of a scrape operation. * * Providers should set this on their result objects to communicate * the nature of the result to the platform. The runtime and C# backend * use this to determine scrape history status and auto-disable logic. * * - `success` — Scrape completed normally with listings/details. * - `not_found` — The company/board does not exist on the ATS (HTTP 404). * - `empty` — The company exists but has zero open listings. * - `rate_limited` — The ATS returned HTTP 429 or equivalent throttling. * - `auth_required` — The ATS requires authentication (HTTP 401/403). * - `parse_error` — The response could not be parsed (ATS changed format). * - `network_error` — Connection, DNS, or timeout failure. * - `error` — Any other unclassified error. */ type ScrapeOutcome = "success" | "not_found" | "empty" | "rate_limited" | "auth_required" | "parse_error" | "network_error" | "error"; /** * Pagination cursor returned by `scrapeListings()`. * * The platform calls `scrapeListings()` repeatedly, passing the previous * cursor back, until `hasMore` is `false` or `listings` is empty. * * @example * ```ts * // API-based pagination * { type: "offset", value: "50" } * * // Cursor-based pagination * { type: "cursor", value: "eyJsYXN0SWQiOiIxMjM0NSJ9" } * * // Page-number pagination * { type: "page", value: "3" } * ``` */ interface PaginationCursor { /** The pagination strategy. Informational — the scraper owns interpretation. */ type: "offset" | "cursor" | "page" | "url" | "custom"; /** Opaque value passed back to the next `scrapeListings()` call. */ value: string; } /** * Result of a single `scrapeListings()` invocation. */ interface ListingsResult { /** The listings discovered on this page / API response. */ listings: JobListing[]; /** Whether more pages are available. */ hasMore: boolean; /** Cursor to pass to the next call. Required when `hasMore` is `true`. */ nextCursor?: PaginationCursor; /** Total number of listings reported by the ATS, if known. */ totalCount?: number; /** Classified outcome of this scrape operation. */ outcome?: ScrapeOutcome; /** Human-readable explanation of the outcome (e.g. error message). */ outcomeMessage?: string; } /** * Result of a single `scrapeJobDetails()` invocation. */ interface JobDetailsResult { /** The fully populated job details. `null` if the job could not be found (404, expired, etc.). */ details: JobDetails | null; /** Whether the job posting was detected as expired or removed. */ isExpired?: boolean; /** Human-readable reason if details could not be retrieved. */ error?: string; /** Classified outcome of this detail-scrape operation. */ outcome?: ScrapeOutcome; /** Human-readable explanation of the outcome. */ outcomeMessage?: string; } /** * @jobo/scraper-sdk — Scraper Context * * The `ScraperContext` is the **only** way a scraper is allowed to make * HTTP requests or render pages. The platform injects a concrete implementation * that handles fingerprinting, proxy rotation, rate limiting, and retries. * * Scrapers must NEVER use `fetch`, `XMLHttpRequest`, or any other HTTP client * directly. All network access goes through the context. */ /** * Standard HTTP methods supported by the platform. */ type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'; /** * Options for an HTTP request made through the platform. * * @example * ```ts * const response = await ctx.http.request({ * url: "https://api.ats.com/v1/jobs?page=2", * method: "GET", * headers: { "Accept": "application/json" }, * }); * ``` */ interface HttpRequestOptions { /** Fully-qualified URL to request. */ url: string; /** HTTP method. Defaults to `"GET"`. */ method?: HttpMethod; /** Request headers. The platform may add/override headers for fingerprinting. */ headers?: Record; /** * Request body. Strings are sent as-is; objects are JSON-serialized * and `Content-Type: application/json` is set automatically. */ body?: string | Record; /** Query parameters appended to the URL. */ params?: Record; /** Request timeout in milliseconds. Platform default applies if omitted. */ timeoutMs?: number; /** * Expected response format. * - `"json"` — parse body as JSON (default). * - `"text"` — return body as a UTF-8 string. * - `"binary"` — return body as an ArrayBuffer. */ responseType?: 'json' | 'text' | 'binary'; } /** * Response from an HTTP request made through the platform. */ interface HttpResponse { /** HTTP status code. */ status: number; /** HTTP status text (e.g. `"OK"`, `"Not Found"`). */ statusText: string; /** Response headers (lowercased keys). */ headers: Record; /** Parsed response body. Shape depends on `responseType`. */ data: T; /** The final URL after any redirects. */ finalUrl: string; } /** * The HTTP client provided by the platform. * * All methods are convenience wrappers around `request()`. * The platform handles proxy selection, fingerprinting, cookie jars, * TLS configuration, and automatic retries on transient failures. */ interface HttpClient { /** * Send an HTTP request with full control over method, headers, and body. * * @typeParam T — Expected shape of the parsed response body. * @param options — Request configuration. * @returns The HTTP response. * @throws {ScraperError} On network failure, timeout, or non-retryable HTTP errors. * * @example * ```ts * const res = await ctx.http.request({ * url: "https://api.ats.com/v1/jobs", * headers: { "X-Api-Key": "abc123" }, * }); * console.log(res.data.jobs); * ``` */ request(options: HttpRequestOptions): Promise>; /** * Convenience: GET request. * * @example * ```ts * const res = await ctx.http.get("https://api.ats.com/v1/jobs?page=1"); * ``` */ get(url: string, options?: Omit): Promise>; /** * Convenience: POST request. * * @example * ```ts * const res = await ctx.http.post("https://api.ats.com/v1/search", { * body: { query: "engineer", location: "NYC" }, * }); * ``` */ post(url: string, options?: Omit): Promise>; } /** * Options for rendering a page in a headless browser. */ interface RenderPageOptions { /** URL to navigate to. */ url: string; /** * Wait strategy before returning the page content. * - `"load"` — wait for the `load` event (default). * - `"domcontentloaded"` — wait for `DOMContentLoaded`. * - `"networkidle"` — wait until network is idle (no requests for 500ms). * - `"selector"` — wait until `waitForSelector` is present in the DOM. */ waitUntil?: 'load' | 'domcontentloaded' | 'networkidle' | 'selector'; /** CSS selector to wait for when `waitUntil` is `"selector"`. */ waitForSelector?: string; /** Maximum time to wait in milliseconds. Platform default applies if omitted. */ timeoutMs?: number; /** * Whether to execute JavaScript on the page. * - `true` — full browser rendering with JS execution (default). * - `false` — static HTML fetch only (faster, lower resource cost). */ javascript?: boolean; /** Additional HTTP headers to send with the navigation request. */ headers?: Record; /** * JavaScript code to execute after the page has loaded but before * returning the content. Useful for clicking "show more" buttons, * dismissing cookie banners, etc. */ pageScript?: string; } /** * Result of rendering a page in the platform's headless browser. */ interface RenderedPage { /** The final URL after any redirects. */ finalUrl: string; /** HTTP status code of the navigation response. */ status: number; /** The full HTML content of the page after rendering. */ html: string; /** Page title from `` tag. */ title: string; } /** * The browser/rendering client provided by the platform. * * Use this when the ATS renders job listings client-side (React, Angular, etc.) * or when you need to interact with the page (click buttons, scroll, etc.). * * The platform manages the browser pool, fingerprinting, and proxy assignment. */ interface BrowserClient { /** * Render a page and return its HTML content. * * @param options — Page rendering configuration. * @returns The rendered page content. * @throws {ScraperError} On navigation failure or timeout. * * @example * ```ts * // Simple static fetch (no JS) * const page = await ctx.browser.renderPage({ * url: "https://ats.example.com/jobs", * javascript: false, * }); * * // Full JS rendering, wait for job cards * const page = await ctx.browser.renderPage({ * url: "https://ats.example.com/jobs", * waitUntil: "selector", * waitForSelector: ".job-card", * }); * ``` */ renderPage(options: RenderPageOptions): Promise<RenderedPage>; } /** * Structured logger provided by the platform. * * Log messages are captured, tagged with the provider ID and scrape run ID, * and stored for debugging and monitoring. */ interface ScraperLogger { /** Verbose debug information. Not shown in production by default. */ debug(message: string, data?: Record<string, MetadataValue>): void; /** General operational information. */ info(message: string, data?: Record<string, MetadataValue>): void; /** Potential issues that don't prevent scraping. */ warn(message: string, data?: Record<string, MetadataValue>): void; /** Errors that prevent scraping a listing or page. */ error(message: string, data?: Record<string, MetadataValue>): void; } /** * The scraper context — injected by the platform into every scraper method. * * This is the **single entry point** for all platform services. * Scrapers receive it as the first argument to `scrapeListings()`, * `scrapeJobDetails()`, and `initialize()`. * * @example * ```ts * import { ScraperContext, ListingsResult } from "@jobo/scraper-sdk"; * * export async function scrapeListings( * ctx: ScraperContext, * cursor?: PaginationCursor, * ): Promise<ListingsResult> { * const res = await ctx.http.get<ApiResponse>("https://api.ats.com/jobs"); * // ... parse and return listings * } * ``` */ interface ScraperContext { /** * HTTP client for making API requests. * Handles proxy rotation, fingerprinting, retries, and rate limiting. */ readonly http: HttpClient; /** * Browser client for rendering JavaScript-heavy pages. * Handles browser pool management, fingerprinting, and proxy assignment. */ readonly browser: BrowserClient; /** * Structured logger. Messages are tagged with provider and run metadata. */ readonly log: ScraperLogger; /** * The base URL of the ATS instance being scraped. * For multi-tenant ATS platforms, this is the company-specific subdomain. * * @example `"https://acme.greenhouse.io"` or `"https://jobs.lever.co/acme"` */ readonly baseUrl: string; /** * The company identifier extracted from the URL, if applicable. * Set by the platform based on the manifest's `companyIdFromUrl` pattern. * * @example `"acme"` for `https://acme.greenhouse.io` */ readonly companyId: string; /** * Arbitrary configuration passed from the platform to this scrape run. * Can include API keys, feature flags, or per-company overrides. */ readonly config: Record<string, MetadataValue>; } /** * @jobo/scraper-sdk — Provider Interface & Manifest * * Defines the contract that every scraper provider must implement, * and the manifest schema that describes a provider package. */ /** * The two fundamental scraper architectures. * * - `"full"` — All job details (title, description, locations, etc.) are * available on the listing/search page or API. A single pass produces * complete {@link JobDetails} records. No secondary detail fetch needed. * * - `"split"` — The listing page only provides basic identifiers (URL, ID, * maybe title). A secondary `scrapeJobDetails()` call is required to * fetch the full description, locations, and metadata for each job. */ type ScraperCategory = "full" | "split"; /** * How the scraper retrieves data from the ATS. * * - `"api"` — Calls a JSON/REST API directly via `ctx.http`. * - `"html"` — Fetches static HTML pages via `ctx.http` (no JS execution). * - `"browser"` — Renders pages with JavaScript via `ctx.browser` (for SPAs). */ type ScraperTransport = "api" | "html" | "browser"; /** * Describes how to extract a field (companyId or jobId) from a URL. * * Three extraction modes (exactly one should be set): * * - **Path pattern** (`urlPattern`): Match against the URL pathname using * Simplify-style path patterns. Supports `:paramName` captures, `*` for * a single path segment, `**` for globstar, and `(/*)` for optional trailing * path segments. The named param matching `param` (or the default field name) * is returned as the extracted value. * * - **Query parameter** (`searchParam`): Extract a URL query parameter by name. * e.g. `{ searchParam: "cid" }` extracts `cid` from `?cid=abc`. * * - **Hostname segment** (`hostSegment`): Extract a segment of the hostname by * index (0 = leftmost subdomain). e.g. `{ hostSegment: 0 }` extracts `acme` * from `acme.greenhouse.io`. * * @example * ```json * // Path-based: extract companyId from URL path * { "urlPattern": "/:companyId/jobs/:jobId" } * * // Query-based: extract from ?cid= parameter * { "searchParam": "cid" } * * // Subdomain-based: extract first hostname segment * { "hostSegment": 0 } * * // Path-based with custom charset (allows parens in captures) * { "urlPattern": "/:locale/:companyId/job/:jobId", "segmentCharset": "a-zA-Z0-9_~ %.()-" } * ``` */ interface UrlFieldExtractor { /** * Path pattern matched against the URL pathname only. * * Syntax: * - `:paramName` — Named capture (matches one path segment) * - `*` — Wildcard (matches one unnamed path segment) * - `**` — Globstar (matches zero or more path segments) * - `(/*)` — Optional trailing path (matches zero or more trailing segments) * * @example `"/:companyId/jobs/:jobId"`, `"/:locale/:companyId/job/**"` */ urlPattern?: string; /** * Extract value from a URL query parameter by name. * @example `"cid"` extracts the `cid` param from `?cid=abc&jobId=123` */ searchParam?: string; /** * Extract a hostname segment by index (0 = leftmost subdomain). * e.g. for `acme.bamboohr.com`, index 0 = `"acme"`, index 1 = `"bamboohr"`. * @example `0` */ hostSegment?: number; /** * Which named param to return from the `urlPattern` captures. * Defaults to the field name (`"companyId"` for companyIdExtractor, * `"jobId"` for jobIdExtractor). * Only relevant when `urlPattern` is set. * @example `"companyId"`, `"boardId"` */ param?: string; /** * Custom regex character class for `:paramName` captures in `urlPattern`. * Default: `a-zA-Z0-9\\-_~%.` * Set to a broader charset when URL segments contain special characters * (e.g. Workday job IDs with parentheses). * @example `"a-zA-Z0-9\\-_~ %.()"` — allows spaces and parentheses */ segmentCharset?: string; /** * Path template used to construct the canonical listing URL. * Named captures from the extractor match are substituted using `:paramName`. * The result is appended to the URL origin. * * For `urlPattern` extractors: all named captures are substituted. * For `hostSegment` / `searchParam` extractors: `:companyId` (or the * default field name) is substituted with the extracted value. * * @example `"/:companyId"`, `"/:locale/:companyId"`, `"/ux/ats/careersite"` */ listingUrlTemplate?: string; } /** * Provider manifest — the `manifest.json` that describes a scraper package. * * The platform reads this to register the provider, match URLs, and * configure scrape runs. Analogous to Apify's `actor.json`. * * @example * ```json * { * "providerId": "greenhouse", * "displayName": "Greenhouse", * "category": "split", * "transport": "api", * "urlPatterns": [ * "https://*.greenhouse.io/**", * "https://boards.greenhouse.io/**" * ], * "companyIdExtractor": { "urlPattern": "/:companyId(/**)" }, * "jobIdExtractor": { "urlPattern": "/:companyId/jobs/:jobId(/*)" }, * "runtimeConfig": { * "maxConcurrentDetails": 5, * "delayBetweenRequestsMs": 200 * } * } * ``` */ interface ScraperProviderManifest { /** Unique provider identifier (e.g. `"greenhouse"`, `"lever"`, `"workday"`). */ providerId: string; /** Human-readable display name (e.g. `"Greenhouse"`, `"Lever"`). */ displayName: string; /** * Scraper category. * - `"full"` — listings contain all details. * - `"split"` — listings are minimal; details fetched separately. */ category: ScraperCategory; /** * Primary data retrieval method. * - `"api"` — JSON/REST API. * - `"html"` — Static HTML (no JS). * - `"browser"` — Headless browser with JS. */ transport: ScraperTransport; /** * URL patterns that identify this ATS. * Supports glob syntax: `*` matches any segment, `**` matches any path. * * @example `["https://*.greenhouse.io/**", "https://boards.greenhouse.io/**"]` */ urlPatterns: string[]; /** * URL patterns to explicitly exclude from matching, even if they match `urlPatterns`. * Useful to filter out embed or iframe endpoints that share the same domain. * * @example `["https://boards.greenhouse.io/embed/**"]` */ excludeUrlPatterns?: string[]; /** * Structured extractor(s) for deriving the company identifier from a URL. * * Supports three extraction modes: path pattern, query parameter, or hostname * segment. When an array is provided, extractors are tried in order — first * successful extraction wins. * * @example * // Path-based * { "urlPattern": "/:companyId(/**)" } * * // Subdomain-based * { "hostSegment": 0 } * * // Query-param based * { "searchParam": "cid" } * * // Multiple fallback extractors (tried in order) * [{ "urlPattern": "/:locale/:companyId/job/**" }, { "urlPattern": "/:companyId" }] */ companyIdExtractor?: UrlFieldExtractor | UrlFieldExtractor[]; /** * Structured extractor(s) for deriving the job identifier from a job detail URL. * * Same extraction modes as `companyIdExtractor`. When an array is provided, * extractors are tried in order — first successful extraction wins. * * @example * // Path-based * { "urlPattern": "/:companyId/jobs/:jobId" } * * // Query-param based * { "searchParam": "jobId" } * * // Slug pattern with restricted charset * [{ "urlPattern": "/jobs/:jobId-*", "segmentCharset": "0-9" }, { "urlPattern": "/jobs/:jobId" }] */ jobIdExtractor?: UrlFieldExtractor | UrlFieldExtractor[]; /** * The global JS variable name for this provider when bundled as IIFE. * Defaults to `"{providerId}Scraper"` if omitted. */ jsProviderName?: string; /** * Test URLs for validating this scraper against live ATS endpoints. * Each URL should point to a real company's job board on this ATS. * Used by one-provider-at-a-time testing workflows. * * @example `["https://boards.greenhouse.io/anthropic", "https://boards.greenhouse.io/stripe"]` */ testUrls?: string[]; /** Runtime configuration hints for the platform. */ runtimeConfig?: ScraperRuntimeConfig; /** * Whether this provider is a draft / work-in-progress. * * Draft providers are blocked from being published to the registry. * Use this while developing a new scraper to prevent accidental * deployment of incomplete providers. * * @default false */ isDraft?: boolean; } /** * Runtime configuration that the platform reads from the manifest. * These are hints — the platform may override them based on load and policy. */ interface ScraperRuntimeConfig { /** * Maximum number of concurrent detail-fetch requests. * Only relevant for split scrapers. Default: `3`. */ maxConcurrentDetails?: number; /** * Minimum delay in milliseconds between consecutive HTTP requests. * Helps avoid rate limiting. Default: `0` (platform decides). */ delayBetweenRequestsMs?: number; /** * Delay in milliseconds after page navigation before scraping. * Relevant for browser-transport scrapers. Default: `0`. */ pageSettleDelayMs?: number; /** * Maximum number of listings to scrape per run. * `0` means no limit. Default: `0`. */ maxListingsPerRun?: number; /** * Maximum number of retry attempts for transient failures. * Default: `3`. */ maxRetries?: number; /** * Custom configuration keys specific to this provider. * Passed through to `ctx.config` at runtime. */ custom?: Record<string, string | number | boolean>; } /** * Result of validating whether a URL belongs to this provider. */ interface ValidationResult { /** Whether the URL is a valid job board for this provider. */ isValid: boolean; /** The extracted company identifier, if applicable. */ companyId?: string; /** The base URL to use for API requests. */ baseUrl?: string; /** Human-readable reason if validation failed. */ reason?: string; } /** * The core scraper provider interface. * * Every scraper must implement this contract. The platform calls these * methods in a well-defined lifecycle: * * 1. `validate(url)` — Check if a URL belongs to this provider. * 2. `initialize(ctx)` — One-time setup (optional). * 3. `scrapeListings(ctx, cursor?)` — Called repeatedly until `hasMore` is `false`. * 4. `scrapeJobDetails(ctx, listing)` — Called for each listing (split scrapers only). * * ``` * ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ * │ validate() │────▶│ initialize() │────▶│ scrapeListings()│──┐ * └─────────────┘ └──────────────┘ └─────────────────┘ │ * ▲ │ * │ hasMore? │ * └───────────────┘ * │ * (split only) * ▼ * ┌─────────────────────┐ * │ scrapeJobDetails() │ * │ (per listing) │ * └─────────────────────┘ * ``` * * @example * ```ts * import { * ScraperProvider, * ScraperContext, * ListingsResult, * JobDetailsResult, * JobListing, * ValidationResult, * PaginationCursor, * } from "@jobo/scraper-sdk"; * * const provider: ScraperProvider = { * async validate(url) { * return { isValid: url.includes("greenhouse.io") }; * }, * * async scrapeListings(ctx, cursor) { * const page = cursor ? parseInt(cursor.value) : 1; * const res = await ctx.http.get(`${ctx.baseUrl}/api/v1/jobs?page=${page}`); * // ... parse and return * }, * * async scrapeJobDetails(ctx, listing) { * const res = await ctx.http.get(`${ctx.baseUrl}/api/v1/jobs/${listing.externalId}`); * // ... parse and return * }, * }; * * export default provider; * ``` */ interface ScraperProvider { /** * Validate whether a URL belongs to this provider and extract identifiers. * * Called before any scraping begins. If `isValid` is `false`, the platform * will not proceed with this provider. * * @param url — The job board URL to validate. * @returns Validation result with optional extracted identifiers. */ validate(url: string): Promise<ValidationResult> | ValidationResult; /** * Optional one-time initialization hook. * * Called once per scrape run, after validation and before the first * `scrapeListings()` call. Use this for: * - Fetching API tokens or CSRF tokens. * - Discovering API versions or endpoints. * - Setting up session cookies. * * @param ctx — The scraper context with HTTP/browser clients. */ initialize?(ctx: ScraperContext): Promise<void>; /** * Scrape a page of job listings. * * The platform calls this method repeatedly, passing the `nextCursor` * from the previous result, until `hasMore` is `false` or `listings` * is empty. * * **For full-details scrapers:** Each listing in the result should be a * complete {@link JobDetails} (cast to {@link JobListing}). The platform * will not call `scrapeJobDetails()`. * * **For split scrapers:** Each listing needs at minimum `externalId` and * `applyUrl`. The platform will call `scrapeJobDetails()` for each. * * @param ctx — The scraper context. * @param cursor — Pagination cursor from the previous call. `undefined` on first call. * @returns A page of listings with pagination info. */ scrapeListings(ctx: ScraperContext, cursor?: PaginationCursor): Promise<ListingsResult>; /** * Scrape full details for a single job listing. * * **Only required for split scrapers** (`category: "split"`). * The platform calls this once per listing returned by `scrapeListings()`. * * @param ctx — The scraper context. * @param listing — The minimal listing to enrich with full details. * @returns The fully populated job details, or `null` if unavailable. */ scrapeJobDetails?(ctx: ScraperContext, listing: JobListing): Promise<JobDetailsResult>; } /** * Helper to define a scraper provider with full type checking. * * This is purely a type-narrowing utility — it returns the object as-is. * Use it in your provider's entry file for IDE autocompletion and validation. * * @example * ```ts * import { defineProvider } from "@jobo/scraper-sdk"; * * export default defineProvider({ * async validate(url) { ... }, * async scrapeListings(ctx, cursor) { ... }, * async scrapeJobDetails(ctx, listing) { ... }, * }); * ``` */ declare function defineProvider(provider: ScraperProvider): ScraperProvider; export { type BrowserClient as B, type HttpClient as H, type JobDetails as J, type ListingsResult as L, type MetadataMap as M, type PaginationCursor as P, type RenderPageOptions as R, type ScrapeOutcome as S, type UrlFieldExtractor as U, type ValidationResult as V, type HttpMethod as a, type HttpRequestOptions as b, type HttpResponse as c, type JobDetailsResult as d, type JobListing as e, type JobLocation as f, type MetadataValue as g, type RenderedPage as h, type ScraperCategory as i, type ScraperContext as j, type ScraperLogger as k, type ScraperProvider as l, type ScraperProviderManifest as m, type ScraperRuntimeConfig as n, type ScraperTransport as o, defineProvider as p };