init
Release Please / release-please (push) Failing after 4m2s
Web CI / web typecheck + build (push) Successful in 3m39s
CodeQL Analysis / Analyze (javascript-typescript) (push) Failing after 1h1m49s
CodeQL Analysis / Analyze (go) (push) Failing after 1h1m49s

This commit is contained in:
redlineo committed 2026-07-13 10:14:00 +03:00
commit d295c7f139
1527 files changed
+270726

No files matched your search

+167
View File
@@ -0,0 +1,167 @@
// @ts-check
/** @typedef {import('./_types.js').Provider} Provider */
// NoFluffJobs provider — hits the public search posting API.
// It intentionally returns only the core scanner job fields; richer skill and
// salary metadata can be added later if the provider contract is expanded.
const ALLOWED_HOSTS = new Set(['nofluffjobs.com']);
const API_URL = 'https://nofluffjobs.com/api/search/posting';
const JOB_BASE = 'https://nofluffjobs.com/pl/job/';
const PAGE_SIZE = 20;
const MAX_PAGES = 5;
function assertNoFluffUrl(url) {
let parsed;
try {
parsed = new URL(url);
} catch {
throw new Error(`nofluffjobs: invalid URL: ${url}`);
}
if (parsed.protocol !== 'https:') throw new Error(`nofluffjobs: URL must use HTTPS: ${url}`);
if (!ALLOWED_HOSTS.has(parsed.hostname)) {
throw new Error(`nofluffjobs: untrusted hostname "${parsed.hostname}" — must be nofluffjobs.com`);
}
return parsed;
}
function detectUrl(entry) {
const url = entry.api || entry.careers_url || '';
if (typeof url !== 'string' || !url.trim()) return null;
try {
return { url: assertNoFluffUrl(url).href };
} catch {
return null;
}
}
function normalizeLocation(posting) {
const parts = [];
if (posting?.fullyRemote || posting?.location?.fullyRemote) parts.push('Remote');
if (Array.isArray(posting?.location?.places)) {
for (const place of posting.location.places) {
const city = String(place?.city || '').trim();
const province = String(place?.province || '').trim();
const country = String(place?.country?.name || '').trim();
if (city) parts.push(city);
else if (province) parts.push(province);
else if (country) parts.push(country);
}
}
return [...new Set(parts.filter(Boolean))].join(', ');
}
function postedAtMillis(value) {
const number = Number(value);
return Number.isFinite(number) ? number : undefined;
}
function buildRequest(entry, pageTo) {
const apiUrl = assertNoFluffUrl(entry.api || API_URL);
apiUrl.pathname = '/api/search/posting';
apiUrl.search = '';
apiUrl.searchParams.set('sort', 'newest');
apiUrl.searchParams.set('withSalaryMatch', 'true');
apiUrl.searchParams.set('pageTo', String(pageTo));
apiUrl.searchParams.set('pageSize', String(Number(entry.page_size || PAGE_SIZE)));
apiUrl.searchParams.set('salaryCurrency', String(entry.currency || 'PLN').toUpperCase());
apiUrl.searchParams.set('salaryPeriod', String(entry.salary_period || 'month'));
apiUrl.searchParams.set('region', String(entry.region || 'pl'));
apiUrl.searchParams.set('language', String(entry.language || 'pl-PL'));
const rawSearch = String(entry.search || entry.raw_search || '').trim();
const body = rawSearch
? {
criteria: '',
url: { searchParam: rawSearch },
rawSearch,
pageSize: Number(entry.page_size || PAGE_SIZE),
withSalaryMatch: true,
}
: {
criteriaSearch: {
country: [],
withSalaryMatch: [],
city: [],
more: [],
employment: [],
requirement: [],
salary: [],
jobPosition: [],
applicationStatus: [],
province: [],
company: [],
id: [],
category: [],
keyword: [],
jobLanguage: [],
seniority: [],
},
pageSize: Number(entry.page_size || PAGE_SIZE),
withSalaryMatch: true,
};
return { url: apiUrl.href, body };
}
export function parseNoFluffJobsResponse(json) {
if (!json || !Array.isArray(json.postings)) {
throw new Error(`nofluffjobs: unexpected API response — expected { postings: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]`);
}
return json.postings
.filter(posting => posting && typeof posting === 'object')
.map(posting => {
const title = String(posting.title || '').trim();
const company = String(posting.name || '').trim();
const slug = String(posting.url || posting.id || '').trim();
if (!title || !slug) return null;
return {
title,
url: `${JOB_BASE}${slug}`,
company,
location: normalizeLocation(posting),
postedAt: postedAtMillis(posting.posted),
};
})
.filter(Boolean);
}
/** @type {Provider} */
export default {
id: 'nofluffjobs',
detect(entry) {
return detectUrl(entry);
},
async fetch(entry, ctx) {
if (!detectUrl(entry)) throw new Error('nofluffjobs: careers_url or api must be a trusted nofluffjobs.com URL');
const maxPages = Number(entry.max_pages || MAX_PAGES);
const jobs = [];
const seen = new Set();
for (let pageTo = 1; pageTo <= maxPages; pageTo++) {
const { url, body } = buildRequest(entry, pageTo);
const json = await ctx.fetchJson(url, {
method: 'POST',
body: JSON.stringify(body),
headers: {
accept: 'application/json, text/plain, */*',
'content-type': 'application/infiniteSearch+json',
},
redirect: 'error',
});
for (const job of parseNoFluffJobsResponse(json)) {
if (seen.has(job.url)) continue;
seen.add(job.url);
jobs.push(job);
}
const totalPages = Number(json?.totalPages || 0);
if (totalPages && pageTo >= totalPages) break;
if (json.postings.length === 0) break;
}
return jobs;
},
};