@timurtekb/tekjobs 0.24.1

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +131 -0
  2. package/LICENSE +21 -0
  3. package/README.md +145 -0
  4. package/app/README.md +22 -0
  5. package/app/dist/assets/index-DzGgV6NF.css +1 -0
  6. package/app/dist/assets/index-QteuouKv.js +77 -0
  7. package/app/dist/favicon.svg +1 -0
  8. package/app/dist/index.html +16 -0
  9. package/app/server/cover-letter.mjs +243 -0
  10. package/app/server/index.mjs +113 -0
  11. package/app/server/mail-check.mjs +234 -0
  12. package/app/server/mcp.mjs +129 -0
  13. package/app/server/people.mjs +164 -0
  14. package/app/server/store.mjs +1131 -0
  15. package/app/server/tailored-resume.mjs +213 -0
  16. package/cli.mjs +205 -0
  17. package/package.json +74 -0
  18. package/run.cmd +10 -0
  19. package/run.mjs +201 -0
  20. package/run.sh +12 -0
  21. package/samples/vault/Jobs/Basalt Systems - Senior Design Engineer, Growth (1010).md +56 -0
  22. package/samples/vault/Jobs/Brightline Studio - Design Engineer (1005).md +51 -0
  23. package/samples/vault/Jobs/Copperleaf - Principal Design Engineer (1006).md +53 -0
  24. package/samples/vault/Jobs/Example Studio - Senior Design Engineer (1013).md +52 -0
  25. package/samples/vault/Jobs/Fjord Analytics - Staff Frontend Engineer, Platform (1003).md +58 -0
  26. package/samples/vault/Jobs/Halcyon Robotics - Senior UX Engineer (1004).md +52 -0
  27. package/samples/vault/Jobs/Lumen Health - Design Systems Engineer (1002).md +51 -0
  28. package/samples/vault/Jobs/Meridian Pay - Staff Design Engineer (1009).md +67 -0
  29. package/samples/vault/Jobs/Northwind Labs - Staff Design Engineer, Design Systems (1000).md +57 -0
  30. package/samples/vault/Jobs/Orbital Software - Senior Design Engineer (1001).md +52 -0
  31. package/samples/vault/Jobs/Quill & Co - Design Engineer, Editor (1008).md +55 -0
  32. package/samples/vault/Jobs/Signalfire Design - Design Engineer (1012).md +53 -0
  33. package/samples/vault/Jobs/Tessellate - Senior Frontend Engineer, Design Systems (1007).md +52 -0
  34. package/samples/vault/Jobs/Verdant - UX Engineer (1011).md +51 -0
  35. package/samples/vault/Logs/2026-09-16.md +13 -0
  36. package/samples/vault/Logs/2026-09-23.md +13 -0
  37. package/samples/vault/People/Lee Example (Fjord Analytics).md +22 -0
  38. package/samples/vault/People/Priya Example (Meridian Pay).md +23 -0
  39. package/samples/vault/People/Sam Example (Northwind Labs).md +20 -0
  40. package/samples/vault/Profile/Positioning.md +16 -0
  41. package/samples/vault/Profile/Profile.md +41 -0
  42. package/samples/vault/Profile/Resume.md +32 -0
  43. package/samples/vault/Profile/Voice.md +15 -0
  44. package/samples/vault/README.md +11 -0
  45. package/samples/vault/Targets/Companies.md +318 -0
  46. package/samples/vault/Targets/Search Criteria.md +234 -0
  47. package/samples/vault/_Home.md +54 -0
  48. package/scraper/config.mjs +122 -0
  49. package/scraper/health.mjs +50 -0
  50. package/scraper/import-link.mjs +281 -0
  51. package/scraper/profile.mjs +186 -0
  52. package/scraper/rescore.mjs +236 -0
  53. package/scraper/resume-sync.mjs +224 -0
  54. package/scraper/resume.mjs +65 -0
  55. package/scraper/score.mjs +114 -0
  56. package/scraper/sources-email.mjs +242 -0
  57. package/scraper/sources-extra.mjs +558 -0
  58. package/scraper/sources-sites.mjs +280 -0
  59. package/scraper/sources.mjs +264 -0
  60. package/scraper/starter/companies-table.md +306 -0
  61. package/scraper/starter/criteria.json +56 -0
  62. package/scraper/starter/profile.md +38 -0
  63. package/scraper/vault.mjs +212 -0
@@ -0,0 +1,558 @@
1
+ // Additional sources beyond Greenhouse/Lever/Ashby/Workday. Same normalized job shape as sources.mjs.
2
+ // Company-slug platforms: rippling, smartrecruiters, workable, bamboohr, breezy, personio, teamtailor, eightfold.
3
+ // Singletons (slug "-"): atlassian, github, spotify, amazon, and google + apple from sources-sites.mjs.
4
+ // Aggregators (toggled by criteria.openSources): email (a local folder), adzuna and usajobs (need keys),
5
+ // themuse, remotive, himalayas, jobicy,
6
+ // workingnomads, arbeitnow, wwr, and wellfound + builtin from sources-sites.mjs.
7
+ import { htmlToText, decodeEntities } from './sources.mjs';
8
+ import { fetchGoogle, fetchApple, fetchWellfound, fetchBuiltIn } from './sources-sites.mjs';
9
+
10
+ import { UA } from './config.mjs';
11
+ const H = { accept: 'application/json', 'user-agent': UA };
12
+ async function req(url, { method = 'GET', body, headers = {}, timeoutMs = 25000 } = {}) {
13
+ try {
14
+ const r = await fetch(url, { method, headers: { ...H, ...(body ? { 'content-type': 'application/json' } : {}), ...headers }, body: body ? JSON.stringify(body) : undefined, signal: AbortSignal.timeout(timeoutMs) });
15
+ const text = await r.text();
16
+ let data = null; try { data = JSON.parse(text); } catch { /* not json */ }
17
+ return { status: r.status, ok: r.ok, data, text };
18
+ } catch (e) { return { status: 0, ok: false, data: null, text: '', error: e.name === 'TimeoutError' ? 'timeout' : e.message }; }
19
+ }
20
+ const bad = (r) => ({ ok: false, jobs: [], error: r.error || `HTTP ${r.status}` });
21
+ const isRemoteText = (s = '') => /\bremote\b|\bdistributed\b|\banywhere\b|\bvirtual\b/i.test(s) || /^\s*(united states|usa|u\.s\.|us|north america|americas)\s*$/i.test(s);
22
+ const iso = (v) => { if (!v) return null; const d = typeof v === 'number' ? new Date(v < 1e12 ? v * 1000 : v) : new Date(v); return isNaN(d) ? null : d.toISOString(); };
23
+ const job = (o) => ({ salary: '', department: '', employmentType: '', descriptionHtml: '', location: '', remote: false, posted: null, ...o });
24
+
25
+ // ---------------- company-slug platforms ----------------
26
+
27
+ // Rippling ATS: https://ats.rippling.com/<slug>/jobs
28
+ export async function fetchRippling(company, slug, titleFilter = () => true) {
29
+ const r = await req(`https://api.rippling.com/platform/api/ats/v1/board/${encodeURIComponent(slug)}/jobs`);
30
+ if (!Array.isArray(r.data)) return bad(r);
31
+ const jobs = [];
32
+ for (const j of r.data) {
33
+ const loc = j.workLocation?.label || '';
34
+ const base = job({ id: `rp:${slug}:${j.uuid}`, source: 'rippling', company, title: j.name || '', url: j.url || `https://ats.rippling.com/${slug}/jobs/${j.uuid}`, location: loc, remote: isRemoteText(loc) || isRemoteText(j.name || ''), department: j.department?.label || '' });
35
+ if (titleFilter(base.title)) {
36
+ const d = await req(`https://api.rippling.com/platform/api/ats/v1/board/${encodeURIComponent(slug)}/jobs/${j.uuid}`);
37
+ if (d.data) {
38
+ const desc = d.data.description;
39
+ base.descriptionHtml = typeof desc === 'string' ? desc : Object.values(desc || {}).filter((v) => typeof v === 'string').join('\n');
40
+ base.posted = iso(d.data.createdOn || d.data.createdAt || d.data.publishedAt);
41
+ base.employmentType = d.data.employmentType?.id || d.data.employmentType?.label || '';
42
+ if (Array.isArray(d.data.workLocations) && d.data.workLocations.length) { base.location = d.data.workLocations.join('; '); base.remote = isRemoteText(base.location) || isRemoteText(base.title); }
43
+ }
44
+ }
45
+ jobs.push(base);
46
+ }
47
+ return { ok: true, jobs };
48
+ }
49
+
50
+ // SmartRecruiters: https://jobs.smartrecruiters.com/<Company>
51
+ export async function fetchSmartRecruiters(company, slug, titleFilter = () => true, { maxDetails = 60 } = {}) {
52
+ const all = [];
53
+ for (let offset = 0; offset < 2000; offset += 100) {
54
+ const r = await req(`https://api.smartrecruiters.com/v1/companies/${encodeURIComponent(slug)}/postings?limit=100&offset=${offset}`);
55
+ if (!r.data || !Array.isArray(r.data.content)) { if (offset === 0) return bad(r); break; }
56
+ all.push(...r.data.content);
57
+ if (all.length >= (r.data.totalFound || 0) || r.data.content.length < 100) break;
58
+ }
59
+ const jobs = []; let details = 0;
60
+ for (const p of all) {
61
+ const L = p.location || {};
62
+ const loc = L.fullLocation || [L.city, L.region, L.country].filter(Boolean).join(', ');
63
+ const base = job({ id: `sr:${slug}:${p.id}`, source: 'smartrecruiters', company, title: p.name || '', url: `https://jobs.smartrecruiters.com/${slug}/${p.id}`, location: loc, remote: !!L.remote || isRemoteText(loc), posted: iso(p.releasedDate), department: p.department?.label || '', employmentType: p.typeOfEmployment?.label || '' });
64
+ if (titleFilter(base.title) && details < maxDetails) {
65
+ details++;
66
+ const d = await req(`https://api.smartrecruiters.com/v1/companies/${encodeURIComponent(slug)}/postings/${p.id}`);
67
+ const secs = d.data?.jobAd?.sections || {};
68
+ base.descriptionHtml = Object.values(secs).map((s) => `<h3>${s.title || ''}</h3>${s.text || ''}`).join('');
69
+ if (d.data?.applyUrl) base.url = d.data.applyUrl.replace(/\?.*$/, '') || base.url;
70
+ }
71
+ jobs.push(base);
72
+ }
73
+ return { ok: true, jobs };
74
+ }
75
+
76
+ // Workable: https://apply.workable.com/<account>/
77
+ export async function fetchWorkable(company, slug) {
78
+ const r = await req(`https://apply.workable.com/api/v1/widget/accounts/${encodeURIComponent(slug)}?details=true`);
79
+ if (!r.data || !Array.isArray(r.data.jobs)) return bad(r);
80
+ const jobs = r.data.jobs.map((j) => {
81
+ const loc = [j.city, j.state, j.country].filter(Boolean).join(', ') || (j.locations || []).map((l) => [l.city, l.region, l.country].filter(Boolean).join(', ')).join('; ');
82
+ return job({ id: `wk:${slug}:${j.shortcode}`, source: 'workable', company, title: j.title || '', url: j.url || j.shortlink, location: loc, remote: !!j.telecommuting || isRemoteText(loc), posted: iso(j.published_on), descriptionHtml: `${j.description || ''}${j.requirements || ''}${j.benefits || ''}`, department: j.department || '', employmentType: j.employment_type || '' });
83
+ });
84
+ return { ok: true, jobs };
85
+ }
86
+
87
+ // BambooHR: https://<sub>.bamboohr.com/careers
88
+ export async function fetchBambooHR(company, slug, titleFilter = () => true) {
89
+ const r = await req(`https://${slug}.bamboohr.com/careers/list`);
90
+ if (!r.data || !Array.isArray(r.data.result)) return bad(r);
91
+ const jobs = [];
92
+ for (const j of r.data.result) {
93
+ const loc = [j.location?.city, j.location?.state, j.atsLocation?.country].filter(Boolean).join(', ');
94
+ const base = job({ id: `bh:${slug}:${j.id}`, source: 'bamboohr', company, title: j.jobOpeningName || '', url: `https://${slug}.bamboohr.com/careers/${j.id}`, location: loc || (j.isRemote ? 'Remote' : ''), remote: !!j.isRemote || isRemoteText(loc), department: j.departmentLabel || '', employmentType: j.employmentStatusLabel || '' });
95
+ if (titleFilter(base.title)) {
96
+ const d = await req(`https://${slug}.bamboohr.com/careers/${j.id}/detail`);
97
+ const info = d.data?.result?.jobOpening;
98
+ if (info) { base.descriptionHtml = info.description || ''; base.posted = iso(info.datePosted); }
99
+ }
100
+ jobs.push(base);
101
+ }
102
+ return { ok: true, jobs };
103
+ }
104
+
105
+ // Breezy: https://<co>.breezy.hr
106
+ export async function fetchBreezy(company, slug) {
107
+ const r = await req(`https://${slug}.breezy.hr/json`);
108
+ if (!Array.isArray(r.data)) return bad(r);
109
+ const jobs = r.data.map((j) => job({ id: `bz:${slug}:${j.id}`, source: 'breezy', company, title: j.name || '', url: j.url, location: j.location?.name || '', remote: !!j.location?.is_remote || isRemoteText(j.location?.name || ''), posted: iso(j.published_date), salary: j.salary || '', department: j.department || '', employmentType: j.type?.name || '' }));
110
+ return { ok: true, jobs };
111
+ }
112
+
113
+ // Personio: https://<co>.jobs.personio.de (XML feed)
114
+ export async function fetchPersonio(company, slug) {
115
+ const r = await req(`https://${slug}.jobs.personio.de/xml`);
116
+ if (!r.text.includes('<position')) return bad(r);
117
+ const jobs = [];
118
+ for (const m of r.text.matchAll(/<position>([\s\S]*?)<\/position>/g)) {
119
+ const x = m[1]; const tag = (t) => decodeEntities((x.match(new RegExp(`<${t}>([\\s\\S]*?)<\\/${t}>`)) || [, ''])[1].replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1').trim());
120
+ const id = tag('id'); const loc = tag('office');
121
+ const desc = [...x.matchAll(/<jobDescription>[\s\S]*?<name>([\s\S]*?)<\/name>[\s\S]*?<value>([\s\S]*?)<\/value>/g)].map((d) => `<h3>${d[1]}</h3>${d[2]}`).join('').replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1');
122
+ jobs.push(job({ id: `pe:${slug}:${id}`, source: 'personio', company, title: tag('name'), url: `https://${slug}.jobs.personio.de/job/${id}`, location: loc, remote: isRemoteText(loc) || tag('schedule').toLowerCase().includes('remote'), posted: iso(tag('createdAt')), descriptionHtml: desc, department: tag('department'), employmentType: tag('employmentType') || tag('schedule') }));
123
+ }
124
+ return { ok: true, jobs };
125
+ }
126
+
127
+ // Teamtailor: https://<co>.teamtailor.com (JSON Feed at /jobs.json)
128
+ export async function fetchTeamtailor(company, slug) {
129
+ const r = await req(`https://${slug}.teamtailor.com/jobs.json`);
130
+ if (!r.data || !Array.isArray(r.data.items)) return bad(r);
131
+ const jobs = r.data.items.map((i) => {
132
+ const loc = (i.tags || []).filter((t) => !/full|part|remote|hybrid/i.test(t)).join('; ') || i._location || '';
133
+ return job({ id: `tt:${slug}:${i.id}`, source: 'teamtailor', company, title: i.title || '', url: i.url, location: loc, remote: isRemoteText([loc, ...(i.tags || [])].join(' ')), posted: iso(i.date_published), descriptionHtml: i.content_html || i.content_text || '' });
134
+ });
135
+ return { ok: true, jobs };
136
+ }
137
+
138
+ // Eightfold: slug = host/domain e.g. explore.jobs.netflix.net/netflix.com
139
+ const EF_QUERIES = ['design engineer', 'design systems', 'design technologist', 'ux engineer', 'frontend engineer', 'front end engineer', 'product designer', 'prototyping'];
140
+ export async function fetchEightfold(company, slug, titleFilter = () => true, { maxDetails = 60 } = {}) {
141
+ const [host, domain] = slug.split('/');
142
+ if (!host || !domain) return { ok: false, jobs: [], error: 'slug must be host/domain' };
143
+ const found = new Map();
144
+ for (const q of EF_QUERIES) {
145
+ for (let start = 0; start < 100; start += 50) {
146
+ const r = await req(`https://${host}/api/apply/v2/jobs?domain=${encodeURIComponent(domain)}&start=${start}&num=50&query=${encodeURIComponent(q)}`);
147
+ if (!r.data || !Array.isArray(r.data.positions)) { if (found.size === 0 && start === 0 && q === EF_QUERIES[0]) return bad(r); break; }
148
+ for (const p of r.data.positions) found.set(p.id, p);
149
+ if (r.data.positions.length < 50) break;
150
+ }
151
+ }
152
+ const jobs = []; let details = 0;
153
+ for (const p of found.values()) {
154
+ const loc = (p.locations || [p.location]).filter(Boolean).join('; ');
155
+ const base = job({ id: `ef:${domain}:${p.id}`, source: 'eightfold', company, title: p.name || p.posting_name || '', url: p.canonicalPositionUrl || `https://${host}/careers?pid=${p.id}&domain=${domain}`, location: loc, remote: isRemoteText(loc), posted: iso(p.t_create || p.t_update), department: [p.business_unit, p.department].filter(Boolean).join(' / ') });
156
+ if (titleFilter(base.title) && details < maxDetails) {
157
+ details++;
158
+ const d = await req(`https://${host}/api/apply/v2/jobs/${p.id}?domain=${encodeURIComponent(domain)}`);
159
+ if (d.data) { base.descriptionHtml = d.data.job_description || d.data.description || ''; if (d.data.canonicalPositionUrl) base.url = d.data.canonicalPositionUrl; }
160
+ }
161
+ jobs.push(base);
162
+ }
163
+ return { ok: true, jobs, scanned: found.size };
164
+ }
165
+
166
+ // ---------------- singletons ----------------
167
+
168
+ export async function fetchAtlassian() {
169
+ const r = await req('https://www.atlassian.com/endpoint/careers/listings');
170
+ if (!Array.isArray(r.data)) return bad(r);
171
+ const jobs = r.data.map((j) => {
172
+ const loc = (j.locations || []).join('; ');
173
+ return job({ id: `atl:${j.id}`, source: 'atlassian', company: 'Atlassian', title: j.title || '', url: j.applyUrl?.replace(/\?mode=apply$/, '') || j.portalJobPost?.portalUrl, location: loc, remote: isRemoteText(loc), posted: iso(j.portalJobPost?.updatedDate), descriptionHtml: `${j.overview || ''}${j.responsibilities || ''}${j.qualifications || ''}${j.compensation || ''}`, department: j.category || '' });
174
+ });
175
+ return { ok: true, jobs };
176
+ }
177
+
178
+ export async function fetchGitHub() {
179
+ const jobs = [];
180
+ for (let page = 1; page <= 40; page++) {
181
+ const r = await req(`https://www.github.careers/api/jobs?page=${page}`);
182
+ if (!r.data || !Array.isArray(r.data.jobs)) { if (page === 1) return bad(r); break; }
183
+ for (const w of r.data.jobs) {
184
+ const d = w.data || w;
185
+ const tags = [...(d.tags6 || []), ...(d.tags2 || [])].join(' ');
186
+ const loc = [d.location_name, ...(d.tags6 || [])].filter(Boolean).join('; ');
187
+ jobs.push(job({ id: `gh-careers:${d.req_id || d.slug}`, source: 'github', company: 'GitHub', title: d.title || '', url: `https://www.github.careers/careers-home/jobs/${d.req_id || d.slug}`, location: loc, remote: /remote/i.test(tags) || d.location_type === 'ANY', posted: iso(d.posted_date), descriptionHtml: `${d.description || ''}<h3>Responsibilities</h3>${d.responsibilities || ''}<h3>Qualifications</h3>${d.qualifications || ''}`, department: (d.categories || []).map((c) => c.name).join(', '), employmentType: (d.tags4 || []).join(', ') }));
188
+ }
189
+ if (r.data.jobs.length === 0 || jobs.length >= (r.data.totalCount || Infinity)) break;
190
+ }
191
+ return { ok: true, jobs };
192
+ }
193
+
194
+ export async function fetchSpotify() {
195
+ const seen = new Map();
196
+ for (const q of ['design', 'frontend', 'front-end', 'web engineer', 'prototyp', 'ux']) {
197
+ const r = await req(`https://api-dot-new-spotifyjobs-com.nw.r.appspot.com/wp-json/animal/v1/job/search?q=${encodeURIComponent(q)}`);
198
+ if (!r.data || !Array.isArray(r.data.result)) { if (seen.size === 0 && q === 'design') return bad(r); continue; }
199
+ for (const j of r.data.result) seen.set(j.id, j);
200
+ }
201
+ const jobs = [...seen.values()].map((j) => {
202
+ const loc = (j.locations || []).map((l) => l.location).join('; ');
203
+ return job({ id: `sp:${j.id}`, source: 'spotify', company: 'Spotify', title: j.text || '', url: `https://www.lifeatspotify.com/jobs/${j.id}`, location: loc, remote: isRemoteText(loc) || /remote|anywhere/i.test(j.job_type?.name || ''), descriptionHtml: `<p>${j.main_category?.name || ''} · ${j.sub_category?.name || ''}</p>`, department: [j.main_category?.name, j.sub_category?.name].filter(Boolean).join(' / '), employmentType: j.job_type?.name || '' });
204
+ });
205
+ return { ok: true, jobs };
206
+ }
207
+
208
+ const AMZ_QUERIES = ['design engineer', 'design technologist', 'design systems', 'ux engineer', 'front end engineer', 'frontend engineer', 'prototyper'];
209
+ export async function fetchAmazon(titleFilter = () => true) {
210
+ const found = new Map();
211
+ for (const q of AMZ_QUERIES) {
212
+ const r = await req(`https://www.amazon.jobs/en/search.json?base_query=${encodeURIComponent(q)}&result_limit=100&offset=0&country=USA`);
213
+ if (!r.data || !Array.isArray(r.data.jobs)) { if (found.size === 0 && q === AMZ_QUERIES[0]) return bad(r); continue; }
214
+ for (const j of r.data.jobs) found.set(j.id, j);
215
+ }
216
+ const jobs = [...found.values()].filter((j) => titleFilter(j.title || '')).map((j) => job({ id: `amz:${j.id_icims || j.id}`, source: 'amazon', company: j.company_name || 'Amazon', title: j.title || '', url: `https://www.amazon.jobs${j.job_path}`, location: j.normalized_location || j.location || '', remote: isRemoteText(j.normalized_location || j.location || '') || /virtual/i.test(j.location || ''), posted: iso(j.posted_date), descriptionHtml: `${j.description || ''}<h3>Basic qualifications</h3>${j.basic_qualifications || ''}<h3>Preferred qualifications</h3>${j.preferred_qualifications || ''}`, department: j.job_category || '', employmentType: j.job_schedule_type || '' }));
217
+ return { ok: true, jobs, scanned: found.size };
218
+ }
219
+
220
+ // ---------------- aggregators ----------------
221
+
222
+ export async function fetchTheMuse({ pages = 12 } = {}) {
223
+ const jobs = []; const seen = new Set();
224
+ for (const cat of ['Design and UX', 'Software Engineering', 'UX']) {
225
+ for (let page = 1; page <= pages; page++) {
226
+ const r = await req(`https://www.themuse.com/api/public/jobs?category=${encodeURIComponent(cat)}&location=${encodeURIComponent('Flexible / Remote')}&page=${page}`);
227
+ if (!r.data || !Array.isArray(r.data.results)) { if (jobs.length === 0 && page === 1 && cat === 'Design and UX') return bad(r); break; }
228
+ for (const j of r.data.results) {
229
+ if (seen.has(j.id)) continue; seen.add(j.id);
230
+ const loc = (j.locations || []).map((l) => l.name).join('; ');
231
+ jobs.push(job({ id: `muse:${j.id}`, source: 'themuse', company: j.company?.name || 'Unknown', title: j.name || '', url: j.refs?.landing_page, location: loc, remote: isRemoteText(loc) || /flexible/i.test(loc), posted: iso(j.publication_date), descriptionHtml: j.contents || '', department: (j.categories || []).map((c) => c.name).join(', '), employmentType: (j.levels || []).map((l) => l.name).join(', ') }));
232
+ }
233
+ if (page >= (r.data.page_count || 0)) break;
234
+ }
235
+ }
236
+ return { ok: true, jobs };
237
+ }
238
+
239
+ // TekJobs' own postings: the feed the site serves from what employers paid to post. On by default, because
240
+ // it is the one source that exists for these readers; `openSources.tekjobs: false` turns it off.
241
+ const TEKJOBS_FEED = process.env.TEKJOBS_FEED || 'https://tekjobs.timurtek.com/api/feed/jobs';
242
+ export async function fetchTekJobs() {
243
+ const r = await req(TEKJOBS_FEED);
244
+ if (!r.ok || !Array.isArray(r.data?.jobs)) return bad(r);
245
+ const jobs = r.data.jobs.map((j) => job({
246
+ id: `tj:${j.id}`, source: 'tekjobs', company: j.company || 'Unknown', title: j.title || '', url: j.url, location: j.location || '', remote: !!j.remote,
247
+ posted: iso(j.posted), descriptionHtml: `<p>${String(j.description || '').replace(/\n\n/g, '</p><p>')}</p>${(j.tags || []).length ? `<p>Keywords: ${j.tags.join(', ')}</p>` : ''}${j.applyUrl ? `<p>Apply: ${j.applyUrl}</p>` : ''}${j.applyEmail ? `<p>Apply by email: ${j.applyEmail}</p>` : ''}`,
248
+ salary: j.salary || '', salaryMin: j.salaryMin || 0, salaryMax: j.salaryMax || 0, department: j.department || '', employmentType: j.employmentType || '',
249
+ }));
250
+ return { ok: true, jobs, scanned: jobs.length };
251
+ }
252
+
253
+ export async function fetchRemotive() {
254
+ const jobs = []; const seen = new Set();
255
+ for (const cat of ['software-dev', 'design', 'product']) {
256
+ const r = await req(`https://remotive.com/api/remote-jobs?category=${cat}&limit=300`);
257
+ if (!r.data || !Array.isArray(r.data.jobs)) { if (jobs.length === 0 && cat === 'software-dev') return bad(r); continue; }
258
+ for (const j of r.data.jobs) {
259
+ if (seen.has(j.id)) continue; seen.add(j.id);
260
+ const loc = j.candidate_required_location || 'Worldwide';
261
+ jobs.push(job({ id: `rmv:${j.id}`, source: 'remotive', company: j.company_name || 'Unknown', title: j.title || '', url: j.url, location: `Remote · ${loc}`, remote: true, posted: iso(j.publication_date), descriptionHtml: `${j.description || ''}<p>Tags: ${(j.tags || []).join(', ')}</p>`, salary: j.salary || '', department: j.category || '', employmentType: j.job_type || '' }));
262
+ }
263
+ }
264
+ return { ok: true, jobs };
265
+ }
266
+
267
+ export async function fetchHimalayas({ max = 400 } = {}) {
268
+ const jobs = [];
269
+ for (let offset = 0; offset < max; offset += 20) { // API caps limit at 20
270
+ const r = await req(`https://himalayas.app/jobs/api?limit=20&offset=${offset}`);
271
+ if (!r.data || !Array.isArray(r.data.jobs)) { if (offset === 0) return bad(r); break; }
272
+ for (const j of r.data.jobs) {
273
+ const restr = (j.locationRestrictions || []).join('; ');
274
+ const usOk = !restr || /united states|usa|north america|americas|worldwide|anywhere/i.test(restr);
275
+ const sal = j.minSalary && j.salaryPeriod === 'yearly' ? `$${Math.round(j.minSalary / 1000)}k–$${Math.round((j.maxSalary || j.minSalary) / 1000)}k` : '';
276
+ jobs.push(job({ id: `him:${j.guid || j.applicationLink || j.title + j.companyName}`, source: 'himalayas', company: j.companyName || 'Unknown', title: j.title || '', url: j.applicationLink || j.guid, location: `Remote · ${restr || 'Worldwide'}`, remote: usOk, posted: iso(j.pubDate), descriptionHtml: j.description || j.excerpt || '', salary: sal, employmentType: j.employmentType || '', department: (j.categories || []).join(', ') }));
277
+ }
278
+ if (r.data.jobs.length < 20) break;
279
+ }
280
+ return { ok: true, jobs };
281
+ }
282
+
283
+ export async function fetchJobicy() {
284
+ const jobs = []; const seen = new Set();
285
+ for (const tag of ['design', 'frontend', 'react', 'ux', 'ai']) {
286
+ const r = await req(`https://jobicy.com/api/v2/remote-jobs?count=100&tag=${tag}&geo=usa`);
287
+ if (!r.data || !Array.isArray(r.data.jobs)) continue;
288
+ for (const j of r.data.jobs) {
289
+ if (seen.has(j.id)) continue; seen.add(j.id);
290
+ const sal = j.salaryMin && j.salaryPeriod === 'yearly' ? `$${Math.round(j.salaryMin / 1000)}k–$${Math.round(j.salaryMax / 1000)}k` : '';
291
+ jobs.push(job({ id: `jby:${j.id}`, source: 'jobicy', company: j.companyName || 'Unknown', title: j.jobTitle || '', url: j.url, location: `Remote · ${j.jobGeo || 'Anywhere'}`, remote: true, posted: iso(j.pubDate), descriptionHtml: j.jobDescription || j.jobExcerpt || '', salary: sal, department: (j.jobIndustry || []).join(', '), employmentType: (j.jobType || []).join(', ') }));
292
+ }
293
+ }
294
+ return jobs.length ? { ok: true, jobs } : { ok: false, jobs: [], error: 'no results' };
295
+ }
296
+
297
+ export async function fetchWorkingNomads() {
298
+ const r = await req('https://www.workingnomads.com/api/exposed_jobs/');
299
+ if (!Array.isArray(r.data)) return bad(r);
300
+ const jobs = r.data.map((j) => job({ id: `wn:${(j.url || '').replace(/\D/g, '') || j.title}`, source: 'workingnomads', company: j.company_name || 'Unknown', title: j.title || '', url: j.url, location: `Remote · ${j.location || 'Anywhere'}`, remote: !j.location || /usa|united states|americas|north america|anywhere|worldwide/i.test(j.location), posted: iso(j.pub_date), descriptionHtml: `${j.description || ''}<p>Tags: ${j.tags || ''}</p>`, department: j.category_name || '' }));
301
+ return { ok: true, jobs };
302
+ }
303
+
304
+ export async function fetchArbeitnow({ pages = 3 } = {}) {
305
+ const jobs = [];
306
+ for (let page = 1; page <= pages; page++) {
307
+ const r = await req(`https://www.arbeitnow.com/api/job-board-api?remote=true&page=${page}`);
308
+ if (!r.data || !Array.isArray(r.data.data)) { if (page === 1) return bad(r); break; }
309
+ for (const j of r.data.data) jobs.push(job({ id: `arb:${j.slug}`, source: 'arbeitnow', company: j.company_name || 'Unknown', title: j.title || '', url: j.url, location: j.location || '', remote: !!j.remote, posted: iso(j.created_at), descriptionHtml: j.description || '', employmentType: (j.job_types || []).join(', ') }));
310
+ if (!r.data.links?.next) break;
311
+ }
312
+ return { ok: true, jobs };
313
+ }
314
+
315
+ export async function fetchWWR() {
316
+ const jobs = [];
317
+ for (const cat of ['remote-design-jobs', 'remote-programming-jobs', 'remote-full-stack-programming-jobs', 'remote-front-end-programming-jobs', 'remote-product-jobs']) {
318
+ const r = await req(`https://weworkremotely.com/categories/${cat}.rss`, { headers: { accept: 'application/rss+xml, application/xml, text/xml' } });
319
+ if (!r.text.includes('<item>')) continue;
320
+ for (const m of r.text.matchAll(/<item>([\s\S]*?)<\/item>/g)) {
321
+ const x = m[1]; const tag = (t) => decodeEntities((x.match(new RegExp(`<${t}>([\\s\\S]*?)<\\/${t}>`)) || [, ''])[1].replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1').trim());
322
+ const full = tag('title'); const [company, ...rest] = full.split(':'); const title = rest.join(':').trim() || full;
323
+ const region = tag('region'); const link = tag('link') || tag('guid');
324
+ if (jobs.some((j) => j.url === link)) continue;
325
+ jobs.push(job({ id: `wwr:${link.split('/').filter(Boolean).pop()}`, source: 'wwr', company: company.trim(), title, url: link, location: `Remote · ${region || 'Anywhere'}`, remote: !region || /usa|united states|americas|north america|anywhere/i.test(region), posted: iso(tag('pubDate')), descriptionHtml: tag('description'), department: cat.replace(/remote-|-jobs/g, '') }));
326
+ }
327
+ }
328
+ return jobs.length ? { ok: true, jobs } : { ok: false, jobs: [], error: 'no items' };
329
+ }
330
+
331
+ // ---------------- Adzuna ----------------
332
+ // https://developer.adzuna.com — free app id and key, read from ADZUNA_APP_ID and ADZUNA_APP_KEY.
333
+ //
334
+ // The only source here that needs a key, and the only one that is an aggregator of aggregators: it carries
335
+ // inventory from job sites with no usable public API of their own, including postings that never reach a
336
+ // company ATS board. That is the reason to have it, and it is the sanctioned way to see that inventory.
337
+ //
338
+ // Three limits to know before trusting what it returns:
339
+ // 1. The API returns a *snippet* of each description, not the full text. `descTerms` therefore see a
340
+ // paragraph where an ATS source gives them a page, so an Adzuna row usually scores lower than the same
341
+ // job fetched from its ATS. When both appear, run.mjs keeps the ATS copy.
342
+ // 2. Results are per country, and salary figures are in that country's currency. Only `us` rows get a
343
+ // `$` salary string, because parseSalary reads dollars; other countries get a currency-tagged string
344
+ // that is shown to the reader and deliberately not parsed as USD.
345
+ // 3. `salary_is_predicted` marks a figure Adzuna inferred rather than one the employer published. Those
346
+ // are never emitted as a salary: a guessed number would move the pay band as if it were stated.
347
+ //
348
+ // `url` is Adzuna's redirect, not the employer's page — that is what the API gives, and it resolves.
349
+ const ADZUNA_COUNTRIES = ['us', 'gb', 'ca', 'au', 'at', 'be', 'br', 'ch', 'de', 'es', 'fr', 'in', 'it', 'mx', 'nl', 'nz', 'pl', 'sg', 'za'];
350
+ const ADZUNA_CURRENCY = { us: 'USD', gb: 'GBP', ca: 'CAD', au: 'AUD', at: 'EUR', be: 'EUR', br: 'BRL', ch: 'CHF', de: 'EUR', es: 'EUR', fr: 'EUR', in: 'INR', it: 'EUR', mx: 'MXN', nl: 'EUR', nz: 'NZD', pl: 'PLN', sg: 'SGD', za: 'ZAR' };
351
+
352
+ const adzunaSalary = (j, country) => {
353
+ const lo = Number(j.salary_min) || 0, hi = Number(j.salary_max) || 0;
354
+ if (!hi || String(j.salary_is_predicted) === '1') return '';
355
+ const k = (n) => `${Math.round(n / 1000)}k`;
356
+ if (country === 'us') return lo && lo !== hi ? `$${k(lo)} - $${k(hi)}` : `$${k(hi)}`;
357
+ const cur = ADZUNA_CURRENCY[country] || country.toUpperCase();
358
+ return lo && lo !== hi ? `${cur} ${k(lo)}-${k(hi)}` : `${cur} ${k(hi)}`;
359
+ };
360
+
361
+ /**
362
+ * One query per search term per country, paginated. Terms come from the profile's own `titleTerms` unless
363
+ * `adzuna.queries` overrides them, so the feed follows the profile rather than being a second thing to tune.
364
+ */
365
+ export async function fetchAdzuna(criteria = {}) {
366
+ const appId = process.env.ADZUNA_APP_ID, appKey = process.env.ADZUNA_APP_KEY;
367
+ if (!appId || !appKey) return { ok: false, jobs: [], error: 'needs ADZUNA_APP_ID and ADZUNA_APP_KEY (free at developer.adzuna.com)' };
368
+
369
+ const cfg = criteria.adzuna || {};
370
+ const countries = (cfg.countries?.length ? cfg.countries : ['us']).filter((c) => ADZUNA_COUNTRIES.includes(c));
371
+ if (!countries.length) return { ok: false, jobs: [], error: `no supported country in adzuna.countries (have: ${ADZUNA_COUNTRIES.join(', ')})` };
372
+ const queries = cfg.queries?.length
373
+ ? cfg.queries
374
+ : Object.entries(criteria.titleTerms || {}).sort((a, b) => b[1] - a[1]).slice(0, cfg.maxQueries ?? 6).map(([t]) => t);
375
+ if (!queries.length) return { ok: false, jobs: [], error: 'no titleTerms and no adzuna.queries to search for' };
376
+
377
+ const pages = Math.max(1, cfg.pages ?? 2);
378
+ const perPage = Math.min(50, cfg.resultsPerPage ?? 50);
379
+ const maxDaysOld = cfg.maxDaysOld ?? 30;
380
+ const cutoff = Date.now() - maxDaysOld * 864e5;
381
+
382
+ const byId = new Map(); // the same posting comes back under several terms
383
+ let calls = 0, failed = 0, lastError = '';
384
+ for (const country of countries) {
385
+ for (const what of queries) {
386
+ for (let page = 1; page <= pages; page++) {
387
+ const qs = new URLSearchParams({ app_id: appId, app_key: appKey, 'content-type': 'application/json', results_per_page: String(perPage), what, max_days_old: String(maxDaysOld) });
388
+ if (cfg.where) qs.set('where', cfg.where);
389
+ const r = await req(`https://api.adzuna.com/v1/api/jobs/${country}/search/${page}?${qs}`);
390
+ calls++;
391
+ if (!r.data || !Array.isArray(r.data.results)) { failed++; lastError = r.error || `HTTP ${r.status}`; break; }
392
+ for (const j of r.data.results) {
393
+ const id = `adz:${country}:${j.id}`;
394
+ if (byId.has(id)) continue;
395
+ const location = j.location?.display_name || (j.location?.area || []).slice(1).reverse().join(', ') || '';
396
+ const title = decodeEntities(j.title || '').replace(/<[^>]+>/g, '').trim();
397
+ // max_days_old is sent, but filtered here too so recency holds even if the parameter is ignored.
398
+ const posted = iso(j.created);
399
+ if (posted && Date.parse(posted) < cutoff) continue;
400
+ byId.set(id, job({
401
+ id,
402
+ source: 'adzuna',
403
+ company: decodeEntities(j.company?.display_name || 'Unknown').trim(),
404
+ title,
405
+ url: j.redirect_url || '',
406
+ location,
407
+ remote: isRemoteText(location) || isRemoteText(title),
408
+ posted,
409
+ descriptionHtml: j.description || '',
410
+ salary: adzunaSalary(j, country),
411
+ department: j.category?.label || '',
412
+ employmentType: [j.contract_time, j.contract_type].filter(Boolean).join(', '),
413
+ }));
414
+ }
415
+ if (r.data.results.length < perPage) break; // last page for this term
416
+ }
417
+ }
418
+ }
419
+ if (!byId.size) return { ok: false, jobs: [], error: failed ? `${failed}/${calls} requests failed (${lastError})` : 'no results' };
420
+ return { ok: true, jobs: [...byId.values()] };
421
+ }
422
+
423
+ // ---------------- USAJOBS ----------------
424
+ // https://developer.usajobs.gov — free key, plus the email you registered it with, which the API expects as
425
+ // the User-Agent. Read from USAJOBS_API_KEY and USAJOBS_EMAIL.
426
+ //
427
+ // Every federal job in the United States, from one endpoint. Unlike Adzuna this returns the *whole*
428
+ // description under Fields=Full, so descTerms see as much as they do from an ATS, and these rows score on
429
+ // equal footing. It also carries an ApplicationCloseDate, which almost nothing else does — federal postings
430
+ // have hard deadlines.
431
+ //
432
+ // Two shapes to respect:
433
+ // 1. PositionRemuneration carries a RateIntervalCode. Only "PA" (per annum) becomes a `$` salary;
434
+ // an hourly or biweekly figure written as an annual one would put the pay band somewhere invented.
435
+ // 2. Host is not set as a header. The docs list it, but it is derived from the URL and Node forbids
436
+ // setting it; the request is to data.usajobs.gov either way.
437
+ const USAJOBS_DESC_PARTS = ['JobSummary', 'MajorDuties', 'Qualifications', 'Requirements', 'Education'];
438
+
439
+ const usajobsSalary = (rem = []) => {
440
+ const annual = rem.find((r) => r.RateIntervalCode === 'PA');
441
+ if (!annual) return '';
442
+ const lo = Number(annual.MinimumRange) || 0, hi = Number(annual.MaximumRange) || 0;
443
+ if (!hi) return '';
444
+ const k = (n) => `${Math.round(n / 1000)}k`;
445
+ return lo && lo !== hi ? `$${k(lo)} - $${k(hi)}` : `$${k(hi)}`;
446
+ };
447
+
448
+ /**
449
+ * One query per search term, paginated. Terms come from the profile's `titleTerms` unless
450
+ * `usajobs.queries` overrides them, matching how Adzuna works.
451
+ */
452
+ export async function fetchUSAJobs(criteria = {}) {
453
+ const key = process.env.USAJOBS_API_KEY, email = process.env.USAJOBS_EMAIL;
454
+ if (!key || !email) return { ok: false, jobs: [], error: 'needs USAJOBS_API_KEY and USAJOBS_EMAIL (free at developer.usajobs.gov; the email is the one the key is registered to)' };
455
+
456
+ const cfg = criteria.usajobs || {};
457
+ const queries = cfg.queries?.length
458
+ ? cfg.queries
459
+ : Object.entries(criteria.titleTerms || {}).sort((a, b) => b[1] - a[1]).slice(0, cfg.maxQueries ?? 6).map(([t]) => t);
460
+ if (!queries.length) return { ok: false, jobs: [], error: 'no titleTerms and no usajobs.queries to search for' };
461
+
462
+ const pages = Math.max(1, cfg.pages ?? 2);
463
+ const perPage = Math.min(500, cfg.resultsPerPage ?? 250);
464
+ const datePosted = Math.min(60, Math.max(0, cfg.datePosted ?? 30)); // the API caps this at 60 days
465
+ const remoteOnly = cfg.remoteOnly ?? criteria.location?.requireRemote ?? false;
466
+ const headers = { 'Authorization-Key': key, 'user-agent': email };
467
+
468
+ const byId = new Map();
469
+ let calls = 0, failed = 0, lastError = '';
470
+ for (const term of queries) {
471
+ for (let page = 1; page <= pages; page++) {
472
+ const qs = new URLSearchParams({ [cfg.searchBy === 'keyword' ? 'Keyword' : 'PositionTitle']: term, ResultsPerPage: String(perPage), Page: String(page), DatePosted: String(datePosted), Fields: 'Full' });
473
+ if (remoteOnly) qs.set('RemoteIndicator', 'True');
474
+ if (cfg.locationName) qs.set('LocationName', cfg.locationName);
475
+ const r = await req(`https://data.usajobs.gov/api/Search?${qs}`, { headers });
476
+ calls++;
477
+ const result = r.data?.SearchResult;
478
+ if (!result || !Array.isArray(result.SearchResultItems)) { failed++; lastError = r.error || `HTTP ${r.status}`; break; }
479
+ for (const item of result.SearchResultItems) {
480
+ const d = item.MatchedObjectDescriptor || {};
481
+ const id = `usa:${item.MatchedObjectId || d.PositionID}`;
482
+ if (byId.has(id)) continue;
483
+ const details = d.UserArea?.Details || {};
484
+ const location = d.PositionLocationDisplay || (d.PositionLocation || []).map((l) => l.LocationName).join('; ') || '';
485
+ const closes = iso(d.ApplicationCloseDate);
486
+ byId.set(id, job({
487
+ id,
488
+ source: 'usajobs',
489
+ // The agency is the employer; the department is the parent, and is the more recognisable name.
490
+ company: d.OrganizationName || d.DepartmentName || 'US Federal Government',
491
+ title: decodeEntities(d.PositionTitle || '').trim(),
492
+ url: d.PositionURI || (Array.isArray(d.ApplyURI) ? d.ApplyURI[0] : '') || '',
493
+ location,
494
+ remote: isRemoteText(location) || /\bremote\b/i.test(d.PositionTitle || ''),
495
+ posted: iso(d.PublicationStartDate || d.PositionStartDate),
496
+ descriptionHtml: [
497
+ ...USAJOBS_DESC_PARTS.map((k) => (details[k] ? `<h3>${k.replace(/([a-z])([A-Z])/g, '$1 $2')}</h3>${details[k]}` : '')),
498
+ d.QualificationSummary ? `<h3>Qualification summary</h3>${d.QualificationSummary}` : '',
499
+ closes ? `<p>Applications close ${closes.slice(0, 10)}.</p>` : '',
500
+ ].filter(Boolean).join(''),
501
+ salary: usajobsSalary(d.PositionRemuneration),
502
+ department: (d.JobCategory || []).map((c) => c.Name).filter(Boolean).join(', ') || d.DepartmentName || '',
503
+ employmentType: [(d.PositionSchedule || [])[0]?.Name, (d.PositionOfferingType || [])[0]?.Name].filter(Boolean).join(', '),
504
+ }));
505
+ }
506
+ const totalPages = Number(result.UserArea?.NumberOfPages) || 1;
507
+ if (page >= totalPages || result.SearchResultItems.length < perPage) break;
508
+ }
509
+ }
510
+ if (!byId.size) return { ok: false, jobs: [], error: failed ? `${failed}/${calls} requests failed (${lastError})` : 'no results' };
511
+ return { ok: true, jobs: [...byId.values()] };
512
+ }
513
+
514
+ export const OPEN_SOURCES = {
515
+ email: { label: 'Alert emails (Inbox folder)', fn: async (c) => (await import('./sources-email.mjs')).fetchEmailInbox(c) },
516
+ adzuna: { label: 'Adzuna', fn: fetchAdzuna },
517
+ usajobs: { label: 'USAJOBS (US federal)', fn: fetchUSAJobs },
518
+ themuse: { label: 'The Muse (remote, design + engineering)', fn: fetchTheMuse },
519
+ tekjobs: { label: 'TekJobs postings', fn: fetchTekJobs, defaultOn: true },
520
+ remotive: { label: 'Remotive', fn: fetchRemotive },
521
+ himalayas: { label: 'Himalayas', fn: fetchHimalayas },
522
+ jobicy: { label: 'Jobicy', fn: fetchJobicy },
523
+ workingnomads: { label: 'Working Nomads', fn: fetchWorkingNomads },
524
+ arbeitnow: { label: 'Arbeitnow', fn: fetchArbeitnow },
525
+ wwr: { label: 'We Work Remotely', fn: fetchWWR },
526
+ wellfound: { label: 'Wellfound (remote design-engineering roles)', fn: fetchWellfound },
527
+ builtin: { label: 'Built In (remote)', fn: fetchBuiltIn },
528
+ };
529
+
530
+ export const SINGLETONS = {
531
+ atlassian: fetchAtlassian,
532
+ github: fetchGitHub,
533
+ spotify: fetchSpotify,
534
+ amazon: fetchAmazon,
535
+ google: fetchGoogle,
536
+ apple: fetchApple,
537
+ };
538
+
539
+ export function fetchExtraCompany(c, titleFilter) {
540
+ switch (c.ats) {
541
+ case 'rippling': return fetchRippling(c.name, c.slug, titleFilter);
542
+ case 'smartrecruiters': return fetchSmartRecruiters(c.name, c.slug, titleFilter);
543
+ case 'workable': return fetchWorkable(c.name, c.slug);
544
+ case 'bamboohr': return fetchBambooHR(c.name, c.slug, titleFilter);
545
+ case 'breezy': return fetchBreezy(c.name, c.slug);
546
+ case 'personio': return fetchPersonio(c.name, c.slug);
547
+ case 'teamtailor': return fetchTeamtailor(c.name, c.slug);
548
+ case 'eightfold': return fetchEightfold(c.name, c.slug, titleFilter);
549
+ case 'atlassian': return fetchAtlassian();
550
+ case 'github': return fetchGitHub();
551
+ case 'spotify': return fetchSpotify();
552
+ case 'amazon': return fetchAmazon(titleFilter);
553
+ case 'google': return fetchGoogle(titleFilter);
554
+ case 'apple': return fetchApple(titleFilter);
555
+ default: return Promise.resolve({ ok: false, jobs: [], error: `unknown ats ${c.ats}` });
556
+ }
557
+ }
558
+ export const EXTRA_ATS = ['rippling', 'smartrecruiters', 'workable', 'bamboohr', 'breezy', 'personio', 'teamtailor', 'eightfold', 'atlassian', 'github', 'spotify', 'amazon', 'google', 'apple'];