{"version":3,"sources":["../src/types/provider.ts","../src/types/errors.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @jobo/scraper-sdk — Provider Interface & Manifest\n *\n * Defines the contract that every scraper provider must implement,\n * and the manifest schema that describes a provider package.\n */\n\nimport type { ScraperContext } from \"./context\";\nimport type {\n  JobListing,\n  ListingsResult,\n  JobDetailsResult,\n  PaginationCursor,\n} from \"./models\";\n\n// ---------------------------------------------------------------------------\n// Scraper Category\n// ---------------------------------------------------------------------------\n\n/**\n * The two fundamental scraper architectures.\n *\n * - `\"full\"` — All job details (title, description, locations, etc.) are\n *   available on the listing/search page or API. A single pass produces\n *   complete {@link JobDetails} records. No secondary detail fetch needed.\n *\n * - `\"split\"` — The listing page only provides basic identifiers (URL, ID,\n *   maybe title). A secondary `scrapeJobDetails()` call is required to\n *   fetch the full description, locations, and metadata for each job.\n */\nexport type ScraperCategory = \"full\" | \"split\";\n\n/**\n * How the scraper retrieves data from the ATS.\n *\n * - `\"api\"` — Calls a JSON/REST API directly via `ctx.http`.\n * - `\"html\"` — Fetches static HTML pages via `ctx.http` (no JS execution).\n * - `\"browser\"` — Renders pages with JavaScript via `ctx.browser` (for SPAs).\n */\nexport type ScraperTransport = \"api\" | \"html\" | \"browser\";\n\n// ---------------------------------------------------------------------------\n// URL Field Extractor\n// ---------------------------------------------------------------------------\n\n/**\n * Describes how to extract a field (companyId or jobId) from a URL.\n *\n * Three extraction modes (exactly one should be set):\n *\n * - **Path pattern** (`urlPattern`): Match against the URL pathname using\n *   Simplify-style path patterns. Supports `:paramName` captures, `*` for\n *   a single path segment, `**` for globstar, and `(/*)` for optional trailing\n *   path segments. The named param matching `param` (or the default field name)\n *   is returned as the extracted value.\n *\n * - **Query parameter** (`searchParam`): Extract a URL query parameter by name.\n *   e.g. `{ searchParam: \"cid\" }` extracts `cid` from `?cid=abc`.\n *\n * - **Hostname segment** (`hostSegment`): Extract a segment of the hostname by\n *   index (0 = leftmost subdomain). e.g. `{ hostSegment: 0 }` extracts `acme`\n *   from `acme.greenhouse.io`.\n *\n * @example\n * ```json\n * // Path-based: extract companyId from URL path\n * { \"urlPattern\": \"/:companyId/jobs/:jobId\" }\n *\n * // Query-based: extract from ?cid= parameter\n * { \"searchParam\": \"cid\" }\n *\n * // Subdomain-based: extract first hostname segment\n * { \"hostSegment\": 0 }\n *\n * // Path-based with custom charset (allows parens in captures)\n * { \"urlPattern\": \"/:locale/:companyId/job/:jobId\", \"segmentCharset\": \"a-zA-Z0-9_~ %.()-\" }\n * ```\n */\nexport interface UrlFieldExtractor {\n  /**\n   * Path pattern matched against the URL pathname only.\n   *\n   * Syntax:\n   * - `:paramName` — Named capture (matches one path segment)\n   * - `*` — Wildcard (matches one unnamed path segment)\n   * - `**` — Globstar (matches zero or more path segments)\n   * - `(/*)` — Optional trailing path (matches zero or more trailing segments)\n   *\n   * @example `\"/:companyId/jobs/:jobId\"`, `\"/:locale/:companyId/job/**\"`\n   */\n  urlPattern?: string;\n\n  /**\n   * Extract value from a URL query parameter by name.\n   * @example `\"cid\"` extracts the `cid` param from `?cid=abc&jobId=123`\n   */\n  searchParam?: string;\n\n  /**\n   * Extract a hostname segment by index (0 = leftmost subdomain).\n   * e.g. for `acme.bamboohr.com`, index 0 = `\"acme\"`, index 1 = `\"bamboohr\"`.\n   * @example `0`\n   */\n  hostSegment?: number;\n\n  /**\n   * Which named param to return from the `urlPattern` captures.\n   * Defaults to the field name (`\"companyId\"` for companyIdExtractor,\n   * `\"jobId\"` for jobIdExtractor).\n   * Only relevant when `urlPattern` is set.\n   * @example `\"companyId\"`, `\"boardId\"`\n   */\n  param?: string;\n\n  /**\n   * Custom regex character class for `:paramName` captures in `urlPattern`.\n   * Default: `a-zA-Z0-9\\\\-_~%.`\n   * Set to a broader charset when URL segments contain special characters\n   * (e.g. Workday job IDs with parentheses).\n   * @example `\"a-zA-Z0-9\\\\-_~ %.()\"` — allows spaces and parentheses\n   */\n  segmentCharset?: string;\n  /**\n   * Path template used to construct the canonical listing URL.\n   * Named captures from the extractor match are substituted using `:paramName`.\n   * The result is appended to the URL origin.\n   *\n   * For `urlPattern` extractors: all named captures are substituted.\n   * For `hostSegment` / `searchParam` extractors: `:companyId` (or the\n   * default field name) is substituted with the extracted value.\n   *\n   * @example `\"/:companyId\"`, `\"/:locale/:companyId\"`, `\"/ux/ats/careersite\"`\n   */\n  listingUrlTemplate?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Provider Manifest\n// ---------------------------------------------------------------------------\n\n/**\n * Provider manifest — the `manifest.json` that describes a scraper package.\n *\n * The platform reads this to register the provider, match URLs, and\n * configure scrape runs. Analogous to Apify's `actor.json`.\n *\n * @example\n * ```json\n * {\n *   \"providerId\": \"greenhouse\",\n *   \"displayName\": \"Greenhouse\",\n *   \"category\": \"split\",\n *   \"transport\": \"api\",\n *   \"urlPatterns\": [\n *     \"https://*.greenhouse.io/**\",\n *     \"https://boards.greenhouse.io/**\"\n *   ],\n *   \"companyIdExtractor\": { \"urlPattern\": \"/:companyId(/**)\" },\n *   \"jobIdExtractor\": { \"urlPattern\": \"/:companyId/jobs/:jobId(/*)\" },\n *   \"runtimeConfig\": {\n *     \"maxConcurrentDetails\": 5,\n *     \"delayBetweenRequestsMs\": 200\n *   }\n * }\n * ```\n */\nexport interface ScraperProviderManifest {\n  /** Unique provider identifier (e.g. `\"greenhouse\"`, `\"lever\"`, `\"workday\"`). */\n  providerId: string;\n\n  /** Human-readable display name (e.g. `\"Greenhouse\"`, `\"Lever\"`). */\n  displayName: string;\n\n  /**\n   * Scraper category.\n   * - `\"full\"` — listings contain all details.\n   * - `\"split\"` — listings are minimal; details fetched separately.\n   */\n  category: ScraperCategory;\n\n  /**\n   * Primary data retrieval method.\n   * - `\"api\"` — JSON/REST API.\n   * - `\"html\"` — Static HTML (no JS).\n   * - `\"browser\"` — Headless browser with JS.\n   */\n  transport: ScraperTransport;\n\n  /**\n   * URL patterns that identify this ATS.\n   * Supports glob syntax: `*` matches any segment, `**` matches any path.\n   *\n   * @example `[\"https://*.greenhouse.io/**\", \"https://boards.greenhouse.io/**\"]`\n   */\n  urlPatterns: string[];\n\n  /**\n   * URL patterns to explicitly exclude from matching, even if they match `urlPatterns`.\n   * Useful to filter out embed or iframe endpoints that share the same domain.\n   *\n   * @example `[\"https://boards.greenhouse.io/embed/**\"]`\n   */\n  excludeUrlPatterns?: string[];\n\n  /**\n   * Structured extractor(s) for deriving the company identifier from a URL.\n   *\n   * Supports three extraction modes: path pattern, query parameter, or hostname\n   * segment. When an array is provided, extractors are tried in order — first\n   * successful extraction wins.\n   *\n   * @example\n   * // Path-based\n   * { \"urlPattern\": \"/:companyId(/**)\" }\n   *\n   * // Subdomain-based\n   * { \"hostSegment\": 0 }\n   *\n   * // Query-param based\n   * { \"searchParam\": \"cid\" }\n   *\n   * // Multiple fallback extractors (tried in order)\n   * [{ \"urlPattern\": \"/:locale/:companyId/job/**\" }, { \"urlPattern\": \"/:companyId\" }]\n   */\n  companyIdExtractor?: UrlFieldExtractor | UrlFieldExtractor[];\n\n  /**\n   * Structured extractor(s) for deriving the job identifier from a job detail URL.\n   *\n   * Same extraction modes as `companyIdExtractor`. When an array is provided,\n   * extractors are tried in order — first successful extraction wins.\n   *\n   * @example\n   * // Path-based\n   * { \"urlPattern\": \"/:companyId/jobs/:jobId\" }\n   *\n   * // Query-param based\n   * { \"searchParam\": \"jobId\" }\n   *\n   * // Slug pattern with restricted charset\n   * [{ \"urlPattern\": \"/jobs/:jobId-*\", \"segmentCharset\": \"0-9\" }, { \"urlPattern\": \"/jobs/:jobId\" }]\n   */\n  jobIdExtractor?: UrlFieldExtractor | UrlFieldExtractor[];\n\n  /**\n   * The global JS variable name for this provider when bundled as IIFE.\n   * Defaults to `\"{providerId}Scraper\"` if omitted.\n   */\n  jsProviderName?: string;\n\n  /**\n   * Test URLs for validating this scraper against live ATS endpoints.\n   * Each URL should point to a real company's job board on this ATS.\n   * Used by one-provider-at-a-time testing workflows.\n   *\n   * @example `[\"https://boards.greenhouse.io/anthropic\", \"https://boards.greenhouse.io/stripe\"]`\n   */\n  testUrls?: string[];\n\n  /** Runtime configuration hints for the platform. */\n  runtimeConfig?: ScraperRuntimeConfig;\n\n  /**\n   * Whether this provider is a draft / work-in-progress.\n   *\n   * Draft providers are blocked from being published to the registry.\n   * Use this while developing a new scraper to prevent accidental\n   * deployment of incomplete providers.\n   *\n   * @default false\n   */\n  isDraft?: boolean;\n}\n\n/**\n * Runtime configuration that the platform reads from the manifest.\n * These are hints — the platform may override them based on load and policy.\n */\nexport interface ScraperRuntimeConfig {\n  /**\n   * Maximum number of concurrent detail-fetch requests.\n   * Only relevant for split scrapers. Default: `3`.\n   */\n  maxConcurrentDetails?: number;\n\n  /**\n   * Minimum delay in milliseconds between consecutive HTTP requests.\n   * Helps avoid rate limiting. Default: `0` (platform decides).\n   */\n  delayBetweenRequestsMs?: number;\n\n  /**\n   * Delay in milliseconds after page navigation before scraping.\n   * Relevant for browser-transport scrapers. Default: `0`.\n   */\n  pageSettleDelayMs?: number;\n\n  /**\n   * Maximum number of listings to scrape per run.\n   * `0` means no limit. Default: `0`.\n   */\n  maxListingsPerRun?: number;\n\n  /**\n   * Maximum number of retry attempts for transient failures.\n   * Default: `3`.\n   */\n  maxRetries?: number;\n\n  /**\n   * Custom configuration keys specific to this provider.\n   * Passed through to `ctx.config` at runtime.\n   */\n  custom?: Record<string, string | number | boolean>;\n}\n\n// ---------------------------------------------------------------------------\n// Validation Result\n// ---------------------------------------------------------------------------\n\n/**\n * Result of validating whether a URL belongs to this provider.\n */\nexport interface ValidationResult {\n  /** Whether the URL is a valid job board for this provider. */\n  isValid: boolean;\n  /** The extracted company identifier, if applicable. */\n  companyId?: string;\n  /** The base URL to use for API requests. */\n  baseUrl?: string;\n  /** Human-readable reason if validation failed. */\n  reason?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Scraper Provider Interface\n// ---------------------------------------------------------------------------\n\n/**\n * The core scraper provider interface.\n *\n * Every scraper must implement this contract. The platform calls these\n * methods in a well-defined lifecycle:\n *\n * 1. `validate(url)` — Check if a URL belongs to this provider.\n * 2. `initialize(ctx)` — One-time setup (optional).\n * 3. `scrapeListings(ctx, cursor?)` — Called repeatedly until `hasMore` is `false`.\n * 4. `scrapeJobDetails(ctx, listing)` — Called for each listing (split scrapers only).\n *\n * ```\n * ┌─────────────┐     ┌──────────────┐     ┌─────────────────┐\n * │  validate()  │────▶│ initialize() │────▶│ scrapeListings()│──┐\n * └─────────────┘     └──────────────┘     └─────────────────┘  │\n *                                                ▲               │\n *                                                │  hasMore?     │\n *                                                └───────────────┘\n *                                                      │\n *                                              (split only)\n *                                                      ▼\n *                                          ┌─────────────────────┐\n *                                          │ scrapeJobDetails()   │\n *                                          │ (per listing)        │\n *                                          └─────────────────────┘\n * ```\n *\n * @example\n * ```ts\n * import {\n *   ScraperProvider,\n *   ScraperContext,\n *   ListingsResult,\n *   JobDetailsResult,\n *   JobListing,\n *   ValidationResult,\n *   PaginationCursor,\n * } from \"@jobo/scraper-sdk\";\n *\n * const provider: ScraperProvider = {\n *   async validate(url) {\n *     return { isValid: url.includes(\"greenhouse.io\") };\n *   },\n *\n *   async scrapeListings(ctx, cursor) {\n *     const page = cursor ? parseInt(cursor.value) : 1;\n *     const res = await ctx.http.get(`${ctx.baseUrl}/api/v1/jobs?page=${page}`);\n *     // ... parse and return\n *   },\n *\n *   async scrapeJobDetails(ctx, listing) {\n *     const res = await ctx.http.get(`${ctx.baseUrl}/api/v1/jobs/${listing.externalId}`);\n *     // ... parse and return\n *   },\n * };\n *\n * export default provider;\n * ```\n */\nexport interface ScraperProvider {\n  /**\n   * Validate whether a URL belongs to this provider and extract identifiers.\n   *\n   * Called before any scraping begins. If `isValid` is `false`, the platform\n   * will not proceed with this provider.\n   *\n   * @param url — The job board URL to validate.\n   * @returns Validation result with optional extracted identifiers.\n   */\n  validate(url: string): Promise<ValidationResult> | ValidationResult;\n\n  /**\n   * Optional one-time initialization hook.\n   *\n   * Called once per scrape run, after validation and before the first\n   * `scrapeListings()` call. Use this for:\n   * - Fetching API tokens or CSRF tokens.\n   * - Discovering API versions or endpoints.\n   * - Setting up session cookies.\n   *\n   * @param ctx — The scraper context with HTTP/browser clients.\n   */\n  initialize?(ctx: ScraperContext): Promise<void>;\n\n  /**\n   * Scrape a page of job listings.\n   *\n   * The platform calls this method repeatedly, passing the `nextCursor`\n   * from the previous result, until `hasMore` is `false` or `listings`\n   * is empty.\n   *\n   * **For full-details scrapers:** Each listing in the result should be a\n   * complete {@link JobDetails} (cast to {@link JobListing}). The platform\n   * will not call `scrapeJobDetails()`.\n   *\n   * **For split scrapers:** Each listing needs at minimum `externalId` and\n   * `applyUrl`. The platform will call `scrapeJobDetails()` for each.\n   *\n   * @param ctx — The scraper context.\n   * @param cursor — Pagination cursor from the previous call. `undefined` on first call.\n   * @returns A page of listings with pagination info.\n   */\n  scrapeListings(\n    ctx: ScraperContext,\n    cursor?: PaginationCursor,\n  ): Promise<ListingsResult>;\n\n  /**\n   * Scrape full details for a single job listing.\n   *\n   * **Only required for split scrapers** (`category: \"split\"`).\n   * The platform calls this once per listing returned by `scrapeListings()`.\n   *\n   * @param ctx — The scraper context.\n   * @param listing — The minimal listing to enrich with full details.\n   * @returns The fully populated job details, or `null` if unavailable.\n   */\n  scrapeJobDetails?(\n    ctx: ScraperContext,\n    listing: JobListing,\n  ): Promise<JobDetailsResult>;\n}\n\n// ---------------------------------------------------------------------------\n// Provider Definition Helper\n// ---------------------------------------------------------------------------\n\n/**\n * Helper to define a scraper provider with full type checking.\n *\n * This is purely a type-narrowing utility — it returns the object as-is.\n * Use it in your provider's entry file for IDE autocompletion and validation.\n *\n * @example\n * ```ts\n * import { defineProvider } from \"@jobo/scraper-sdk\";\n *\n * export default defineProvider({\n *   async validate(url) { ... },\n *   async scrapeListings(ctx, cursor) { ... },\n *   async scrapeJobDetails(ctx, listing) { ... },\n * });\n * ```\n */\nexport function defineProvider(provider: ScraperProvider): ScraperProvider {\n  return provider;\n}\n","/**\n * @jobo/scraper-sdk — Error Types\n *\n * Typed error classes that scrapers should throw to communicate\n * specific failure modes to the platform. The platform uses the error\n * type to decide retry strategy, alerting, and reporting.\n */\n\n/**\n * Base error class for all scraper errors.\n * Extends the native `Error` with a machine-readable `code` field.\n */\nexport class ScraperError extends Error {\n  /** Machine-readable error code for programmatic handling. */\n  readonly code: string;\n  /** Whether the platform should retry this operation. */\n  readonly retryable: boolean;\n\n  constructor(message: string, code: string, retryable = false) {\n    super(message);\n    this.name = 'ScraperError';\n    this.code = code;\n    this.retryable = retryable;\n  }\n}\n\n/**\n * Thrown when an HTTP request fails due to a network-level issue\n * (DNS resolution, connection refused, TLS handshake, etc.).\n *\n * Always retryable — the platform will rotate proxies and retry.\n */\nexport class NetworkError extends ScraperError {\n  /** The URL that failed. */\n  readonly url: string;\n\n  constructor(message: string, url: string) {\n    super(message, 'NETWORK_ERROR', true);\n    this.name = 'NetworkError';\n    this.url = url;\n  }\n}\n\n/**\n * Thrown when the ATS returns an HTTP error status (4xx / 5xx).\n *\n * - 429 (Too Many Requests) and 5xx are retryable.\n * - 401/403 are retryable (proxy rotation may help).\n * - 404 is not retryable.\n * - Other 4xx are not retryable.\n */\nexport class HttpError extends ScraperError {\n  /** HTTP status code. */\n  readonly status: number;\n  /** The URL that returned the error. */\n  readonly url: string;\n  /** Response body, if available. */\n  readonly responseBody?: string;\n\n  constructor(message: string, status: number, url: string, responseBody?: string) {\n    const retryable = status === 429 || status >= 500 || status === 401 || status === 403;\n    super(message, 'HTTP_ERROR', retryable);\n    this.name = 'HttpError';\n    this.status = status;\n    this.url = url;\n    this.responseBody = responseBody;\n  }\n}\n\n/**\n * Thrown when the ATS rate-limits the scraper (HTTP 429 or equivalent).\n *\n * Always retryable. The platform will back off and retry with a different proxy.\n * If `retryAfterMs` is provided, the platform will wait at least that long.\n */\nexport class RateLimitError extends ScraperError {\n  /** Suggested wait time in milliseconds before retrying. */\n  readonly retryAfterMs?: number;\n  /** The URL that was rate-limited. */\n  readonly url: string;\n\n  constructor(message: string, url: string, retryAfterMs?: number) {\n    super(message, 'RATE_LIMITED', true);\n    this.name = 'RateLimitError';\n    this.url = url;\n    this.retryAfterMs = retryAfterMs;\n  }\n}\n\n/**\n * Thrown when the scraper cannot parse the response from the ATS.\n *\n * This indicates the ATS changed its HTML structure or API schema.\n * Not retryable — requires a scraper code update.\n */\nexport class ParseError extends ScraperError {\n  /** The selector or JSON path that failed to match. */\n  readonly selector?: string;\n  /** A snippet of the content that could not be parsed. */\n  readonly contentSnippet?: string;\n\n  constructor(message: string, selector?: string, contentSnippet?: string) {\n    super(message, 'PARSE_ERROR', false);\n    this.name = 'ParseError';\n    this.selector = selector;\n    this.contentSnippet = contentSnippet?.substring(0, 500);\n  }\n}\n\n/**\n * Thrown when the ATS requires authentication (login wall, CAPTCHA, etc.)\n * and the scraper cannot proceed.\n *\n * Not retryable without human intervention or credential provisioning.\n */\nexport class AuthenticationError extends ScraperError {\n  /** The type of authentication barrier encountered. */\n  readonly barrier: 'login' | 'captcha' | 'mfa' | 'cookie_consent' | 'unknown';\n\n  constructor(message: string, barrier: AuthenticationError['barrier'] = 'unknown') {\n    super(message, 'AUTH_REQUIRED', false);\n    this.name = 'AuthenticationError';\n    this.barrier = barrier;\n  }\n}\n\n/**\n * Thrown when a page or job posting is detected as expired, removed, or unavailable.\n *\n * Not retryable — the content no longer exists.\n */\nexport class ExpiredError extends ScraperError {\n  /** The external ID of the expired job, if known. */\n  readonly externalId?: string;\n\n  constructor(message: string, externalId?: string) {\n    super(message, 'EXPIRED', false);\n    this.name = 'ExpiredError';\n    this.externalId = externalId;\n  }\n}\n"]}