init
This commit is contained in:
commit
d295c7f139
1527 files changed
+270726
No files matched your search
@@ -0,0 +1,103 @@
|
||||
// @ts-check
|
||||
/** @typedef {import('./_types.js').Provider} Provider */
|
||||
|
||||
// Workable provider — hits the public markdown feed at /<slug>/jobs.md.
|
||||
// Workable's documented JSON API requires an auth token; the markdown feed
|
||||
// is the only no-auth public surface. Auto-detects from careers_url pattern
|
||||
// `https://apply.workable.com/<slug>`. A tracked_companies entry can also
|
||||
// set `provider: workable` explicitly to bypass detection.
|
||||
|
||||
const ALLOWED_WORKABLE_HOSTS = new Set(['apply.workable.com']);
|
||||
|
||||
function assertWorkableUrl(url) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(url);
|
||||
} catch {
|
||||
throw new Error(`workable: invalid URL: ${url}`);
|
||||
}
|
||||
if (parsed.protocol !== 'https:') throw new Error(`workable: URL must use HTTPS: ${url}`);
|
||||
if (!ALLOWED_WORKABLE_HOSTS.has(parsed.hostname)) {
|
||||
throw new Error(`workable: untrusted hostname "${parsed.hostname}" — must be one of: ${[...ALLOWED_WORKABLE_HOSTS].join(', ')}`);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function resolveFeedUrl(entry) {
|
||||
const raw = typeof entry.careers_url === 'string' ? entry.careers_url : '';
|
||||
if (!raw) return null;
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(raw);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (parsed.protocol !== 'https:') return null;
|
||||
if (parsed.hostname !== 'apply.workable.com') return null;
|
||||
const slug = parsed.pathname.split('/').filter(Boolean)[0];
|
||||
if (!slug) return null;
|
||||
return `https://apply.workable.com/${slug}/jobs.md`;
|
||||
}
|
||||
|
||||
/** @type {Provider} */
|
||||
export default {
|
||||
id: 'workable',
|
||||
|
||||
detect(entry) {
|
||||
const feedUrl = resolveFeedUrl(entry);
|
||||
return feedUrl ? { url: feedUrl } : null;
|
||||
},
|
||||
|
||||
async fetch(entry, ctx) {
|
||||
const feedUrl = resolveFeedUrl(entry);
|
||||
if (!feedUrl) throw new Error(`workable: cannot derive feed URL for ${entry.name}`);
|
||||
assertWorkableUrl(feedUrl);
|
||||
// redirect:'error' prevents SSRF via server-side redirects; combined with
|
||||
// assertWorkableUrl above it guarantees the final hostname stays in the allowlist.
|
||||
const text = await ctx.fetchText(feedUrl, { redirect: 'error' });
|
||||
return parseWorkableMarkdown(text, entry.name);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse Workable's public markdown feed. Exported as a named export for unit
|
||||
* tests. The feed exposes a table:
|
||||
* | Title | Department | Location | Type | Salary | Posted | Details |
|
||||
* where `Details` holds a markdown link
|
||||
* [View](https://apply.workable.com/<slug>/jobs/view/<id>.md)
|
||||
* URLs are validated against `https://apply.workable.com/` — off-domain or
|
||||
* non-HTTPS [View] links are skipped (not emitted).
|
||||
*
|
||||
* @param {string} text — markdown body
|
||||
* @param {string} companyName — value to write into job.company
|
||||
* @returns {Array<{title: string, url: string, company: string, location: string}>}
|
||||
*/
|
||||
export function parseWorkableMarkdown(text, companyName) {
|
||||
if (typeof text !== 'string') return [];
|
||||
const jobs = [];
|
||||
for (const line of text.split('\n')) {
|
||||
if (!line.startsWith('|') || !line.includes('[View]')) continue;
|
||||
const cols = line.split('|').map(c => c.trim());
|
||||
// Cols: ['', title, dept, location, type, salary, posted, '[View](url.md)', '']
|
||||
if (cols.length < 8) continue;
|
||||
const title = cols[1];
|
||||
if (!title || title === 'Title') continue;
|
||||
const location = cols[3] || '';
|
||||
const urlMatch = line.match(/\[View\]\(([^)]+)\)/);
|
||||
let url = urlMatch ? urlMatch[1] : '';
|
||||
if (url.endsWith('.md')) url = url.slice(0, -3);
|
||||
if (!url) continue; // skip rows with no resolvable URL (e.g., malformed [View] link)
|
||||
|
||||
// Validate the extracted URL — must parse as https://apply.workable.com/...
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
if (parsedUrl.protocol !== 'https:' || parsedUrl.hostname !== 'apply.workable.com') continue;
|
||||
url = parsedUrl.href;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
jobs.push({ title, url, location, company: companyName });
|
||||
}
|
||||
return jobs;
|
||||
}
|
||||
Reference in new issue
Block a user