openings 0.1.18 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -58,7 +58,7 @@ export async function verifyCandidates(candidates: SourceCandidate[], options: V
58
58
  const providerFetch = providerCooldowns.get(source.ats)!.wrap(fetcher);
59
59
  try {
60
60
  const evidence = await probe(candidate, source, providerFetch, timeoutMs, options.resolveHost, options.headTransport, options.pageTransport);
61
- const replayed = evidence.identityEvidence === "company_redirect" || evidence.identityEvidence === "company_page_link";
61
+ const replayed = evidence.identityEvidence === "company_redirect" || evidence.identityEvidence === "company_page_link" || evidence.providerLinksDomain;
62
62
  if (!replayed && !identityMatches(candidate.companyName, candidate.companyDomain, evidence.observedCompanyName, source.token)) {
63
63
  rejected.push({ index, value: rejection(candidate, "identity_mismatch", `Expected ${candidate.companyName}; observed ${evidence.observedCompanyName}`) });
64
64
  continue;
@@ -164,23 +164,32 @@ async function probe(candidate: SourceCandidate, source: ResolvedSource, fetcher
164
164
  throw new VerificationError("unreachable", `HTTP ${response.status}`);
165
165
  }
166
166
  const contentType = response.headers.get("content-type") ?? "unknown";
167
- let body: unknown;
168
- try { body = await response.json(); } catch { throw new VerificationError("invalid_payload", "Endpoint did not return JSON"); }
169
167
  const spec = providerSpec(source.ats);
168
+ let body: unknown;
169
+ try { body = spec?.bodyFormat === "text" ? await response.text() : await response.json(); } catch { throw new VerificationError("invalid_payload", "Endpoint did not return JSON"); }
170
170
  const jobs = spec ? spec.jobsFromBody(body) : source.ats === "greenhouse" ? recordArray(body, "jobs") : source.ats === "lever" ? array(body) : source.ats === "workday" ? recordArray(body, "jobPostings") : source.ats === "recruitee" ? recordArray(body, "offers") : recordArray(body, "jobs");
171
171
  if (!jobs) throw new VerificationError("invalid_payload", "Payload does not contain the expected jobs array");
172
172
  if (jobs.length === 0) throw new VerificationError("empty_board", "Source has no jobs, so identity cannot be verified");
173
- const providerName = spec ? spec.providerName(jobs, body) : source.ats === "greenhouse" ? majority(jobs.map((job) => stringField(job, "company_name")).filter(Boolean)) : source.ats === "workday" ? workday!.tenant : "";
173
+ let providerName = spec ? spec.providerName(jobs, body) : source.ats === "greenhouse" ? majority(jobs.map((job) => stringField(job, "company_name")).filter(Boolean)) : source.ats === "workday" ? workday!.tenant : "";
174
+ let infoWebsite = "";
175
+ if (spec?.companyInfo) {
176
+ const info = await spec.companyInfo(source.token, async (url, format = "json") => { const reply = await fetchProbeWithRetry(fetcher, url, { signal: controller.signal }); if (!reply.ok) { await reply.body?.cancel().catch(() => undefined); throw new VerificationError("unreachable", `HTTP ${reply.status}`); } return format === "text" ? reply.text() : reply.json(); }).catch((): { name: string; website?: string } => ({ name: "" }));
177
+ providerName = providerName || info.name; infoWebsite = info.website ?? "";
178
+ }
179
+ const expectedDomain = candidate.companyDomain.toLowerCase().replace(/^www\./, "");
180
+ // The provider's own tenant record names the company website: that is the company's claim on its own board, stronger than any name heuristic.
181
+ const providerLinksDomain = Boolean(infoWebsite) && (infoWebsite === expectedDomain || infoWebsite.endsWith(`.${expectedDomain}`));
174
182
  const tenantMatches = source.ats === "workday" && identityMatches(candidate.companyName, candidate.companyDomain, workday!.tenant, source.token);
175
183
  const namedProvider = source.ats === "greenhouse" || tenantMatches || Boolean(spec && providerName);
176
- const hasDomainLink = !namedProvider && structuredIdentityLinksDomain(jobs, candidate.companyDomain);
184
+ const hasDomainLink = providerLinksDomain || !namedProvider && structuredIdentityLinksDomain(jobs, candidate.companyDomain);
177
185
  const hasRedirectEvidence = !namedProvider && await verifiedCompanyRedirect(candidate, source, resolveHost, headTransport, timeoutMs);
178
186
  const hasPageLink = !namedProvider && !hasDomainLink && !hasRedirectEvidence && await verifiedCompanyPageLink(candidate, source, resolveHost, pageTransport, timeoutMs);
179
187
  if (!namedProvider && !hasDomainLink && !hasRedirectEvidence && !hasPageLink) throw new VerificationError("identity_mismatch", `Neither structured identity fields, a verified company redirect, nor a company careers-page link point to ${candidate.companyDomain}`);
180
188
  const observedCompanyName = namedProvider && providerName ? providerName : candidate.companyName;
181
189
  return {
182
190
  observedCompanyName,
183
- identityEvidence: tenantMatches ? "provider_tenant" as const : (namedProvider && providerName ? "provider_company_name" as const : hasDomainLink ? "structured_domain_link" as const : hasRedirectEvidence ? "company_redirect" as const : "company_page_link" as const),
191
+ providerLinksDomain,
192
+ identityEvidence: providerLinksDomain ? "structured_domain_link" as const : tenantMatches ? "provider_tenant" as const : (namedProvider && providerName ? "provider_company_name" as const : hasDomainLink ? "structured_domain_link" as const : hasRedirectEvidence ? "company_redirect" as const : "company_page_link" as const),
184
193
  contentType,
185
194
  payloadVersion: spec ? spec.payloadVersion(body) : source.ats === "greenhouse" ? "greenhouse-job-board:v1" : source.ats === "lever" ? "lever-postings:v0" : source.ats === "workday" ? "workday-cxs:v1" : source.ats === "recruitee" ? "recruitee-careers:v1" : `ashby-job-board:${isRecord(body) && typeof body.apiVersion === "string" ? body.apiVersion : "unknown"}`,
186
195
  jobCount: source.ats === "workday" && isRecord(body) && typeof body.total === "number" ? body.total : jobs.length,
package/src/types.ts CHANGED
@@ -1,4 +1,5 @@
1
- export const ALL_PROVIDERS = ["greenhouse", "lever", "ashby", "workday", "recruitee", "smartrecruiters", "workable", "breezy", "freshteam"] as const;
1
+ /** ATS boards plus "jobposting": a company's own career site read only through its schema.org JobPosting markup (token = careers URL). */
2
+ export const ALL_PROVIDERS = ["greenhouse", "lever", "ashby", "workday", "recruitee", "smartrecruiters", "workable", "breezy", "freshteam", "keka", "zohorecruit", "jobposting"] as const;
2
3
  export type Ats = (typeof ALL_PROVIDERS)[number];
3
4
 
4
5
  export interface DomainEvidence {
@@ -40,7 +41,7 @@ export interface SourceVerification {
40
41
  checkedAt: string;
41
42
  canonicalSourceUrl: string;
42
43
  observedCompanyName: string;
43
- identityEvidence: "provider_company_name" | "provider_tenant" | "structured_domain_link" | "company_redirect" | "company_page_link" | "provider_board";
44
+ identityEvidence: "provider_company_name" | "provider_tenant" | "structured_domain_link" | "company_redirect" | "company_page_link" | "provider_board" | "company_site";
44
45
  contentType: string;
45
46
  payloadVersion: string;
46
47
  jobCount: number;
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.1.18";
1
+ export const VERSION = "0.1.19";