@cavuno/board 1.38.0 → 1.40.0
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.
- package/README.md +7 -0
- package/dist/{jobs-DMH3Ytoq.d.mts → _spec-DRbgPIlN.d.mts} +404 -96
- package/dist/{jobs-DMH3Ytoq.d.ts → _spec-DRbgPIlN.d.ts} +404 -96
- package/dist/bin.mjs +242 -18
- package/dist/{board-CBry4f7H.d.mts → board-BxPUtrOl.d.ts} +1 -1
- package/dist/{board-tV-zcaHs.d.ts → board-DCiNpgFf.d.mts} +1 -1
- package/dist/doctor.js +238 -14
- package/dist/doctor.mjs +238 -14
- package/dist/filters.d.mts +18 -4
- package/dist/filters.d.ts +18 -4
- package/dist/filters.js +17 -0
- package/dist/filters.mjs +17 -0
- package/dist/format.d.mts +3 -2
- package/dist/format.d.ts +3 -2
- package/dist/index.d.mts +62 -73
- package/dist/index.d.ts +62 -73
- package/dist/index.js +71 -1
- package/dist/index.mjs +71 -1
- package/dist/jobs-DAGAVHQL.d.ts +105 -0
- package/dist/jobs-DhePKSRe.d.mts +105 -0
- package/dist/{salaries-CaGlcCC6.d.ts → salaries-D6SUVMmt.d.mts} +2 -1
- package/dist/{salaries-01g4wK8y.d.mts → salaries-cqb78kg0.d.ts} +2 -1
- package/dist/search-BalPAS0P.d.ts +81 -0
- package/dist/search-DYUQzCq_.d.mts +81 -0
- package/dist/seo.d.mts +4 -3
- package/dist/seo.d.ts +4 -3
- package/dist/server.d.mts +56 -4
- package/dist/server.d.ts +56 -4
- package/dist/server.js +41 -0
- package/dist/server.mjs +41 -0
- package/dist/sitemap.d.mts +5 -3
- package/dist/sitemap.d.ts +5 -3
- package/dist/suggest.d.mts +70 -0
- package/dist/suggest.d.ts +70 -0
- package/dist/suggest.js +165 -0
- package/dist/suggest.mjs +144 -0
- package/dist/theme.d.mts +49 -1
- package/dist/theme.d.ts +49 -1
- package/dist/theme.js +60 -26
- package/dist/theme.mjs +60 -26
- package/package.json +11 -1
- package/skills/cavuno-board-filters/SKILL.md +11 -4
- package/skills/cavuno-board-jobs/SKILL.md +18 -2
- package/skills/cavuno-board-suggest/SKILL.md +119 -0
- package/skills/manifest.json +9 -2
package/dist/doctor.mjs
CHANGED
|
@@ -102,6 +102,156 @@ function summarize(results) {
|
|
|
102
102
|
return { exitCode: failed.length > 0 ? 1 : 0, passed, failed, skipped };
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
// src/doctor/cookie-conformance.ts
|
|
106
|
+
import { existsSync, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
|
|
107
|
+
import { join, relative } from "path";
|
|
108
|
+
var COOKIE = record("static.cookie-codec", 1);
|
|
109
|
+
var SOURCE_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|astro|svelte|vue)$/;
|
|
110
|
+
var REMEDIATION = "set cookies only through the SDK server cookie codec (buildCookie/buildClearCookie from @cavuno/board/server)";
|
|
111
|
+
var SET_COOKIE_SINK = /set-cookie/i;
|
|
112
|
+
var DOCUMENT_COOKIE_ASSIGN = /document\.cookie\s*\+?=(?!=)/i;
|
|
113
|
+
var DOMAIN_SCOPE = /domain\s*=/i;
|
|
114
|
+
var LOOKAHEAD_LINES = 3;
|
|
115
|
+
function stripComments(line) {
|
|
116
|
+
let out = "";
|
|
117
|
+
let i = 0;
|
|
118
|
+
let quote = null;
|
|
119
|
+
while (i < line.length) {
|
|
120
|
+
const c = line[i];
|
|
121
|
+
if (quote !== null) {
|
|
122
|
+
out += c;
|
|
123
|
+
if (c === "\\" && i + 1 < line.length) {
|
|
124
|
+
out += line[i + 1];
|
|
125
|
+
i += 2;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (c === quote) quote = null;
|
|
129
|
+
i += 1;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (c === "'" || c === '"' || c === "`") {
|
|
133
|
+
quote = c;
|
|
134
|
+
out += c;
|
|
135
|
+
i += 1;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (c === "/" && i + 1 < line.length && line[i - 1] !== "\\") {
|
|
139
|
+
const next = line[i + 1];
|
|
140
|
+
if (next === "/") {
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
if (next === "*") {
|
|
144
|
+
i += 2;
|
|
145
|
+
while (i < line.length) {
|
|
146
|
+
if (line[i] === "*" && i + 1 < line.length && line[i + 1] === "/") {
|
|
147
|
+
i += 2;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
i += 1;
|
|
151
|
+
}
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
out += c;
|
|
156
|
+
i += 1;
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
function isCommentLine(line) {
|
|
161
|
+
const trimmed = line.trim();
|
|
162
|
+
if (trimmed.startsWith("*")) return true;
|
|
163
|
+
return stripComments(line).trim() === "";
|
|
164
|
+
}
|
|
165
|
+
function hasCookieWriteSink(line) {
|
|
166
|
+
return SET_COOKIE_SINK.test(line) || DOCUMENT_COOKIE_ASSIGN.test(line);
|
|
167
|
+
}
|
|
168
|
+
function statementWindow(lines, sinkIdx) {
|
|
169
|
+
const sinkStripped = stripComments(lines[sinkIdx]);
|
|
170
|
+
const parts = [sinkStripped];
|
|
171
|
+
if (sinkStripped.includes(";")) return sinkStripped;
|
|
172
|
+
let taken = 0;
|
|
173
|
+
for (let j = sinkIdx + 1; j < lines.length && taken < LOOKAHEAD_LINES; j += 1) {
|
|
174
|
+
const next = lines[j];
|
|
175
|
+
if (isCommentLine(next)) continue;
|
|
176
|
+
const stripped = stripComments(next);
|
|
177
|
+
parts.push(stripped);
|
|
178
|
+
taken += 1;
|
|
179
|
+
if (stripped.includes(";")) break;
|
|
180
|
+
}
|
|
181
|
+
return parts.join("\n");
|
|
182
|
+
}
|
|
183
|
+
function isOffendingSink(lines, index) {
|
|
184
|
+
const line = lines[index];
|
|
185
|
+
if (isCommentLine(line)) return false;
|
|
186
|
+
const stripped = stripComments(line);
|
|
187
|
+
if (!hasCookieWriteSink(stripped)) return false;
|
|
188
|
+
return DOMAIN_SCOPE.test(statementWindow(lines, index));
|
|
189
|
+
}
|
|
190
|
+
function walkSourceFiles(dir, out) {
|
|
191
|
+
let entries;
|
|
192
|
+
try {
|
|
193
|
+
entries = readdirSync(dir);
|
|
194
|
+
} catch {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
for (const name of entries) {
|
|
198
|
+
if (name === "node_modules" || name.startsWith(".")) continue;
|
|
199
|
+
const full = join(dir, name);
|
|
200
|
+
let st;
|
|
201
|
+
try {
|
|
202
|
+
st = statSync(full);
|
|
203
|
+
} catch {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (st.isDirectory()) {
|
|
207
|
+
walkSourceFiles(full, out);
|
|
208
|
+
} else if (st.isFile() && SOURCE_EXT.test(name)) {
|
|
209
|
+
out.push(full);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function truncate(line, max = 120) {
|
|
214
|
+
const t = line.trim();
|
|
215
|
+
return t.length > max ? `${t.slice(0, max)}\u2026` : t;
|
|
216
|
+
}
|
|
217
|
+
function checkCookieCodecConformance(projectRoot) {
|
|
218
|
+
const srcDir = join(projectRoot, "src");
|
|
219
|
+
if (!existsSync(srcDir)) {
|
|
220
|
+
return [
|
|
221
|
+
COOKIE(
|
|
222
|
+
"skip",
|
|
223
|
+
"no src/ directory \u2014 cookie-codec conformance scan skipped"
|
|
224
|
+
)
|
|
225
|
+
];
|
|
226
|
+
}
|
|
227
|
+
const files = [];
|
|
228
|
+
walkSourceFiles(srcDir, files);
|
|
229
|
+
const findings = [];
|
|
230
|
+
for (const file of files) {
|
|
231
|
+
const rel = relative(projectRoot, file).split("\\").join("/");
|
|
232
|
+
let text;
|
|
233
|
+
try {
|
|
234
|
+
text = readFileSync2(file, "utf8");
|
|
235
|
+
} catch {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const lines = text.split(/\r?\n/);
|
|
239
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
240
|
+
if (!isOffendingSink(lines, i)) continue;
|
|
241
|
+
findings.push(`${rel}:${i + 1} \u2014 ${truncate(lines[i])}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (findings.length === 0) {
|
|
245
|
+
return [
|
|
246
|
+
COOKIE(
|
|
247
|
+
"pass",
|
|
248
|
+
"no domain-scoped Set-Cookie or document.cookie writes in src/"
|
|
249
|
+
)
|
|
250
|
+
];
|
|
251
|
+
}
|
|
252
|
+
return [COOKIE("fail", `${findings.join("; ")}; ${REMEDIATION}`)];
|
|
253
|
+
}
|
|
254
|
+
|
|
105
255
|
// src/doctor/probe.ts
|
|
106
256
|
var FETCH_TIMEOUT_MS = 1e4;
|
|
107
257
|
async function probe(fetchImpl, url) {
|
|
@@ -221,14 +371,82 @@ async function runReadProbes(fetchImpl, frontendUrl) {
|
|
|
221
371
|
];
|
|
222
372
|
}
|
|
223
373
|
|
|
374
|
+
// src/doctor/sandbox.ts
|
|
375
|
+
var SANDBOX_GATE = record("sandbox.persona-gate", 1);
|
|
376
|
+
var EXPECTED_PERSONA_COUNT = 8;
|
|
377
|
+
function skipSandboxPersonaGate(reason) {
|
|
378
|
+
return SANDBOX_GATE("skip", reason);
|
|
379
|
+
}
|
|
380
|
+
async function checkSandboxPersonaGate(fetchImpl, env, boardContext) {
|
|
381
|
+
if (boardContext === null) {
|
|
382
|
+
return SANDBOX_GATE(
|
|
383
|
+
"skip",
|
|
384
|
+
"board did not resolve \u2014 fix static.board first"
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
const url = `${apiBase(env.apiUrl)}/v1/boards/${encodeURIComponent(env.boardKey)}/sandbox/personas`;
|
|
388
|
+
const result = await probe(fetchImpl, url);
|
|
389
|
+
if (boardContext.sandbox !== true) {
|
|
390
|
+
return result.status === 404 ? SANDBOX_GATE(
|
|
391
|
+
"pass",
|
|
392
|
+
"sandbox persona endpoint refuses (404) on a non-sandbox board"
|
|
393
|
+
) : SANDBOX_GATE(
|
|
394
|
+
"fail",
|
|
395
|
+
`sandbox persona endpoint is reachable on a NON-sandbox board (HTTP ${result.status}) \u2014 the capability gate is not holding`
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
if (!result.ok) {
|
|
399
|
+
return SANDBOX_GATE(
|
|
400
|
+
"fail",
|
|
401
|
+
`sandbox persona roster did not resolve (HTTP ${result.status}) \u2014 the persona seed may have failed`
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
let personas;
|
|
405
|
+
let password;
|
|
406
|
+
try {
|
|
407
|
+
const parsed = JSON.parse(result.body);
|
|
408
|
+
personas = parsed.personas;
|
|
409
|
+
password = parsed.password;
|
|
410
|
+
} catch {
|
|
411
|
+
return SANDBOX_GATE(
|
|
412
|
+
"fail",
|
|
413
|
+
"sandbox persona endpoint returned 200 but not JSON \u2014 proxy or captive portal in the way?"
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
if (!Array.isArray(personas) || personas.length !== EXPECTED_PERSONA_COUNT) {
|
|
417
|
+
return SANDBOX_GATE(
|
|
418
|
+
"fail",
|
|
419
|
+
`expected ${EXPECTED_PERSONA_COUNT} personas, got ${Array.isArray(personas) ? personas.length : "a non-array"} \u2014 the persona seed is incomplete`
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
const unseeded = personas.filter((persona) => persona.seeded !== true);
|
|
423
|
+
if (unseeded.length > 0) {
|
|
424
|
+
const ids = unseeded.map((persona) => typeof persona.id === "string" ? persona.id : "?").join(", ");
|
|
425
|
+
return SANDBOX_GATE(
|
|
426
|
+
"fail",
|
|
427
|
+
`${unseeded.length} of ${EXPECTED_PERSONA_COUNT} personas are not seeded on the board (${ids}) \u2014 the persona seed failed or was purged; run POST /v1/boards/{key}/sandbox/reseed to restore the roster`
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
if (typeof password !== "string" || password.length === 0) {
|
|
431
|
+
return SANDBOX_GATE(
|
|
432
|
+
"fail",
|
|
433
|
+
"roster present but the published password is missing"
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
return SANDBOX_GATE(
|
|
437
|
+
"pass",
|
|
438
|
+
`sandbox roster complete and seeded (${EXPECTED_PERSONA_COUNT} personas)`
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
224
442
|
// src/doctor/theme.ts
|
|
225
443
|
import { createHash } from "crypto";
|
|
226
|
-
import { existsSync, readFileSync as
|
|
227
|
-
import { join } from "path";
|
|
444
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
445
|
+
import { join as join2 } from "path";
|
|
228
446
|
var THEME = record("static.theme", 1);
|
|
229
447
|
function checkThemeFreshness(projectRoot, context) {
|
|
230
|
-
const tokensPath =
|
|
231
|
-
if (!
|
|
448
|
+
const tokensPath = join2(projectRoot, "src/tokens.css");
|
|
449
|
+
if (!existsSync2(tokensPath)) {
|
|
232
450
|
return [
|
|
233
451
|
THEME(
|
|
234
452
|
"skip",
|
|
@@ -236,9 +454,9 @@ function checkThemeFreshness(projectRoot, context) {
|
|
|
236
454
|
)
|
|
237
455
|
];
|
|
238
456
|
}
|
|
239
|
-
const tokensHash = createHash("sha256").update(
|
|
240
|
-
const resolvedPath =
|
|
241
|
-
const resolvedHash =
|
|
457
|
+
const tokensHash = createHash("sha256").update(readFileSync3(tokensPath, "utf8"), "utf8").digest("hex");
|
|
458
|
+
const resolvedPath = join2(projectRoot, "src/theme/resolved.ts");
|
|
459
|
+
const resolvedHash = existsSync2(resolvedPath) ? readFileSync3(resolvedPath, "utf8").match(
|
|
242
460
|
/tokensHash = '([0-9a-f]{64})'/
|
|
243
461
|
)?.[1] ?? null : null;
|
|
244
462
|
if (resolvedHash !== tokensHash) {
|
|
@@ -478,8 +696,8 @@ async function runWriteProbes(options) {
|
|
|
478
696
|
}
|
|
479
697
|
|
|
480
698
|
// src/doctor/run.ts
|
|
481
|
-
import { existsSync as
|
|
482
|
-
import { join as
|
|
699
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
|
|
700
|
+
import { join as join3 } from "path";
|
|
483
701
|
var STATIC_API = record("static.api", 1);
|
|
484
702
|
var STATIC_BOARD = record("static.board", 1);
|
|
485
703
|
var STATIC_SKILLS = record("static.skills", 1);
|
|
@@ -545,8 +763,8 @@ var SKILL_ROOTS = [
|
|
|
545
763
|
".cursor/skills"
|
|
546
764
|
];
|
|
547
765
|
function checkSkillsFreshness(projectRoot) {
|
|
548
|
-
const roots = SKILL_ROOTS.map((root) =>
|
|
549
|
-
(root) =>
|
|
766
|
+
const roots = SKILL_ROOTS.map((root) => join3(projectRoot, root)).filter(
|
|
767
|
+
(root) => existsSync3(root)
|
|
550
768
|
);
|
|
551
769
|
if (roots.length === 0) {
|
|
552
770
|
return STATIC_SKILLS(
|
|
@@ -559,10 +777,10 @@ function checkSkillsFreshness(projectRoot) {
|
|
|
559
777
|
const seen = /* @__PURE__ */ new Set();
|
|
560
778
|
for (const root of roots) {
|
|
561
779
|
for (const skill of corpus.skills) {
|
|
562
|
-
const copied =
|
|
563
|
-
if (!
|
|
780
|
+
const copied = join3(root, skill.name, "SKILL.md");
|
|
781
|
+
if (!existsSync3(copied)) continue;
|
|
564
782
|
seen.add(skill.name);
|
|
565
|
-
if (
|
|
783
|
+
if (readFileSync4(copied, "utf8") !== skill.content) {
|
|
566
784
|
stale.add(skill.name);
|
|
567
785
|
}
|
|
568
786
|
}
|
|
@@ -612,6 +830,12 @@ async function runDoctor(options) {
|
|
|
612
830
|
results.push(
|
|
613
831
|
...checkThemeFreshness(options.projectRoot ?? process.cwd(), boardContext)
|
|
614
832
|
);
|
|
833
|
+
results.push(
|
|
834
|
+
...checkCookieCodecConformance(options.projectRoot ?? process.cwd())
|
|
835
|
+
);
|
|
836
|
+
results.push(
|
|
837
|
+
envOk ? await checkSandboxPersonaGate(fetchImpl, env, boardContext) : skipSandboxPersonaGate("env checks failed \u2014 fix them first")
|
|
838
|
+
);
|
|
615
839
|
results.push(
|
|
616
840
|
...options.frontendUrl ? await runReadProbes(fetchImpl, options.frontendUrl) : skipReadProbes("no --frontend <url> given")
|
|
617
841
|
);
|
package/dist/filters.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { B as BoardLabelOverrides } from './ui-copy-CKfFTtLk.mjs';
|
|
2
|
-
import { J as JobSort, E as EmploymentType, R as RemoteOption, S as Seniority } from './jobs-
|
|
2
|
+
import { J as JobSort, E as EmploymentType, R as RemoteOption, S as Seniority } from './jobs-DhePKSRe.mjs';
|
|
3
|
+
import './_spec-DRbgPIlN.mjs';
|
|
3
4
|
|
|
4
5
|
declare const REMOTE_OPTIONS: readonly RemoteOption[];
|
|
5
6
|
/**
|
|
@@ -33,6 +34,12 @@ interface ListingFilters {
|
|
|
33
34
|
employmentType?: EmploymentType;
|
|
34
35
|
/** Multi-select (parity with the hosted board's seniority checkboxes). */
|
|
35
36
|
seniority?: Seniority[];
|
|
37
|
+
/**
|
|
38
|
+
* Company filter as PUBLIC SLUGS (the URL identity). Multi-select tolerated
|
|
39
|
+
* in URLs (comma-joined); the hosted UI applies single-replace. Map to the
|
|
40
|
+
* wire as `companySlug` (`jobs.list` query / `jobs.search` filters).
|
|
41
|
+
*/
|
|
42
|
+
company?: string[];
|
|
36
43
|
/** Result ordering (ADR-0048); absent ⇒ `relevance`. */
|
|
37
44
|
sort?: JobSort;
|
|
38
45
|
}
|
|
@@ -42,14 +49,21 @@ interface ListingFilters {
|
|
|
42
49
|
* semantics: trim, lowercase, drop unknowns, dedupe preserving order.
|
|
43
50
|
*/
|
|
44
51
|
declare function parseSeniority(raw: unknown): Seniority[] | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* Normalise raw company-slug input — an array (repeated params) or a
|
|
54
|
+
* comma-string (hand-typed URL) — with the hosted semantics: trim,
|
|
55
|
+
* lowercase, drop empties, dedupe preserving order, then cap at 10
|
|
56
|
+
* (the wire max; keep FIRST 10). Open value set (no vocabulary).
|
|
57
|
+
*/
|
|
58
|
+
declare function parseCompany(raw: unknown): string[] | undefined;
|
|
45
59
|
/**
|
|
46
60
|
* Validate raw URL search params into the supported listing filters,
|
|
47
61
|
* dropping unknown values (never throwing — listing URLs are public input).
|
|
48
62
|
*
|
|
49
63
|
* @example
|
|
50
|
-
* parseListingFilters({ seniority: 'senior,lead', sort: 'newest' });
|
|
51
|
-
* // { seniority: ['senior', 'lead'], sort: 'newest' }
|
|
64
|
+
* parseListingFilters({ seniority: 'senior,lead', company: 'acme', sort: 'newest' });
|
|
65
|
+
* // { seniority: ['senior', 'lead'], company: ['acme'], sort: 'newest' }
|
|
52
66
|
*/
|
|
53
67
|
declare function parseListingFilters(search: Record<string, unknown>): ListingFilters;
|
|
54
68
|
|
|
55
|
-
export { DEFAULT_SORT, EMPLOYMENT_TYPES, JOB_SORTS, type ListingFilters, REMOTE_OPTIONS, SENIORITIES, parseListingFilters, parseSeniority, seniorityLabels, sortLabels };
|
|
69
|
+
export { DEFAULT_SORT, EMPLOYMENT_TYPES, JOB_SORTS, type ListingFilters, REMOTE_OPTIONS, SENIORITIES, parseCompany, parseListingFilters, parseSeniority, seniorityLabels, sortLabels };
|
package/dist/filters.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { B as BoardLabelOverrides } from './ui-copy-CKfFTtLk.js';
|
|
2
|
-
import { J as JobSort, E as EmploymentType, R as RemoteOption, S as Seniority } from './jobs-
|
|
2
|
+
import { J as JobSort, E as EmploymentType, R as RemoteOption, S as Seniority } from './jobs-DAGAVHQL.js';
|
|
3
|
+
import './_spec-DRbgPIlN.js';
|
|
3
4
|
|
|
4
5
|
declare const REMOTE_OPTIONS: readonly RemoteOption[];
|
|
5
6
|
/**
|
|
@@ -33,6 +34,12 @@ interface ListingFilters {
|
|
|
33
34
|
employmentType?: EmploymentType;
|
|
34
35
|
/** Multi-select (parity with the hosted board's seniority checkboxes). */
|
|
35
36
|
seniority?: Seniority[];
|
|
37
|
+
/**
|
|
38
|
+
* Company filter as PUBLIC SLUGS (the URL identity). Multi-select tolerated
|
|
39
|
+
* in URLs (comma-joined); the hosted UI applies single-replace. Map to the
|
|
40
|
+
* wire as `companySlug` (`jobs.list` query / `jobs.search` filters).
|
|
41
|
+
*/
|
|
42
|
+
company?: string[];
|
|
36
43
|
/** Result ordering (ADR-0048); absent ⇒ `relevance`. */
|
|
37
44
|
sort?: JobSort;
|
|
38
45
|
}
|
|
@@ -42,14 +49,21 @@ interface ListingFilters {
|
|
|
42
49
|
* semantics: trim, lowercase, drop unknowns, dedupe preserving order.
|
|
43
50
|
*/
|
|
44
51
|
declare function parseSeniority(raw: unknown): Seniority[] | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* Normalise raw company-slug input — an array (repeated params) or a
|
|
54
|
+
* comma-string (hand-typed URL) — with the hosted semantics: trim,
|
|
55
|
+
* lowercase, drop empties, dedupe preserving order, then cap at 10
|
|
56
|
+
* (the wire max; keep FIRST 10). Open value set (no vocabulary).
|
|
57
|
+
*/
|
|
58
|
+
declare function parseCompany(raw: unknown): string[] | undefined;
|
|
45
59
|
/**
|
|
46
60
|
* Validate raw URL search params into the supported listing filters,
|
|
47
61
|
* dropping unknown values (never throwing — listing URLs are public input).
|
|
48
62
|
*
|
|
49
63
|
* @example
|
|
50
|
-
* parseListingFilters({ seniority: 'senior,lead', sort: 'newest' });
|
|
51
|
-
* // { seniority: ['senior', 'lead'], sort: 'newest' }
|
|
64
|
+
* parseListingFilters({ seniority: 'senior,lead', company: 'acme', sort: 'newest' });
|
|
65
|
+
* // { seniority: ['senior', 'lead'], company: ['acme'], sort: 'newest' }
|
|
52
66
|
*/
|
|
53
67
|
declare function parseListingFilters(search: Record<string, unknown>): ListingFilters;
|
|
54
68
|
|
|
55
|
-
export { DEFAULT_SORT, EMPLOYMENT_TYPES, JOB_SORTS, type ListingFilters, REMOTE_OPTIONS, SENIORITIES, parseListingFilters, parseSeniority, seniorityLabels, sortLabels };
|
|
69
|
+
export { DEFAULT_SORT, EMPLOYMENT_TYPES, JOB_SORTS, type ListingFilters, REMOTE_OPTIONS, SENIORITIES, parseCompany, parseListingFilters, parseSeniority, seniorityLabels, sortLabels };
|
package/dist/filters.js
CHANGED
|
@@ -25,6 +25,7 @@ __export(filters_exports, {
|
|
|
25
25
|
JOB_SORTS: () => JOB_SORTS,
|
|
26
26
|
REMOTE_OPTIONS: () => REMOTE_OPTIONS,
|
|
27
27
|
SENIORITIES: () => SENIORITIES,
|
|
28
|
+
parseCompany: () => parseCompany,
|
|
28
29
|
parseListingFilters: () => parseListingFilters,
|
|
29
30
|
parseSeniority: () => parseSeniority,
|
|
30
31
|
seniorityLabels: () => seniorityLabels,
|
|
@@ -746,6 +747,7 @@ function sortLabels(locale, labels) {
|
|
|
746
747
|
salary_high: copy.sortSalaryHighLabel
|
|
747
748
|
};
|
|
748
749
|
}
|
|
750
|
+
var COMPANY_SLUG_MAX = 10;
|
|
749
751
|
var SENIORITY_LOOKUP = new Set(SENIORITIES);
|
|
750
752
|
function parseSeniority(raw) {
|
|
751
753
|
const values = Array.isArray(raw) ? raw : typeof raw === "string" && raw ? raw.split(",") : [];
|
|
@@ -761,6 +763,20 @@ function parseSeniority(raw) {
|
|
|
761
763
|
}
|
|
762
764
|
return result.length > 0 ? result : void 0;
|
|
763
765
|
}
|
|
766
|
+
function parseCompany(raw) {
|
|
767
|
+
const values = Array.isArray(raw) ? raw : typeof raw === "string" && raw ? raw.split(",") : [];
|
|
768
|
+
const result = [];
|
|
769
|
+
const seen = /* @__PURE__ */ new Set();
|
|
770
|
+
for (const value of values) {
|
|
771
|
+
if (typeof value !== "string") continue;
|
|
772
|
+
const normalized = value.trim().toLowerCase();
|
|
773
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
774
|
+
seen.add(normalized);
|
|
775
|
+
result.push(normalized);
|
|
776
|
+
if (result.length >= COMPANY_SLUG_MAX) break;
|
|
777
|
+
}
|
|
778
|
+
return result.length > 0 ? result : void 0;
|
|
779
|
+
}
|
|
764
780
|
function parseListingFilters(search) {
|
|
765
781
|
return {
|
|
766
782
|
q: typeof search.q === "string" && search.q ? search.q : void 0,
|
|
@@ -769,6 +785,7 @@ function parseListingFilters(search) {
|
|
|
769
785
|
search.employmentType
|
|
770
786
|
) ? search.employmentType : void 0,
|
|
771
787
|
seniority: parseSeniority(search.seniority),
|
|
788
|
+
company: parseCompany(search.company),
|
|
772
789
|
sort: JOB_SORTS.includes(search.sort) ? search.sort : void 0
|
|
773
790
|
};
|
|
774
791
|
}
|
package/dist/filters.mjs
CHANGED
|
@@ -712,6 +712,7 @@ function sortLabels(locale, labels) {
|
|
|
712
712
|
salary_high: copy.sortSalaryHighLabel
|
|
713
713
|
};
|
|
714
714
|
}
|
|
715
|
+
var COMPANY_SLUG_MAX = 10;
|
|
715
716
|
var SENIORITY_LOOKUP = new Set(SENIORITIES);
|
|
716
717
|
function parseSeniority(raw) {
|
|
717
718
|
const values = Array.isArray(raw) ? raw : typeof raw === "string" && raw ? raw.split(",") : [];
|
|
@@ -727,6 +728,20 @@ function parseSeniority(raw) {
|
|
|
727
728
|
}
|
|
728
729
|
return result.length > 0 ? result : void 0;
|
|
729
730
|
}
|
|
731
|
+
function parseCompany(raw) {
|
|
732
|
+
const values = Array.isArray(raw) ? raw : typeof raw === "string" && raw ? raw.split(",") : [];
|
|
733
|
+
const result = [];
|
|
734
|
+
const seen = /* @__PURE__ */ new Set();
|
|
735
|
+
for (const value of values) {
|
|
736
|
+
if (typeof value !== "string") continue;
|
|
737
|
+
const normalized = value.trim().toLowerCase();
|
|
738
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
739
|
+
seen.add(normalized);
|
|
740
|
+
result.push(normalized);
|
|
741
|
+
if (result.length >= COMPANY_SLUG_MAX) break;
|
|
742
|
+
}
|
|
743
|
+
return result.length > 0 ? result : void 0;
|
|
744
|
+
}
|
|
730
745
|
function parseListingFilters(search) {
|
|
731
746
|
return {
|
|
732
747
|
q: typeof search.q === "string" && search.q ? search.q : void 0,
|
|
@@ -735,6 +750,7 @@ function parseListingFilters(search) {
|
|
|
735
750
|
search.employmentType
|
|
736
751
|
) ? search.employmentType : void 0,
|
|
737
752
|
seniority: parseSeniority(search.seniority),
|
|
753
|
+
company: parseCompany(search.company),
|
|
738
754
|
sort: JOB_SORTS.includes(search.sort) ? search.sort : void 0
|
|
739
755
|
};
|
|
740
756
|
}
|
|
@@ -744,6 +760,7 @@ export {
|
|
|
744
760
|
JOB_SORTS,
|
|
745
761
|
REMOTE_OPTIONS,
|
|
746
762
|
SENIORITIES,
|
|
763
|
+
parseCompany,
|
|
747
764
|
parseListingFilters,
|
|
748
765
|
parseSeniority,
|
|
749
766
|
seniorityLabels,
|
package/dist/format.d.mts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { P as PublicJob, a as PublicJobCard, C as CustomFieldValues } from './jobs-
|
|
1
|
+
import { P as PublicJob, a as PublicJobCard, C as CustomFieldValues } from './jobs-DhePKSRe.mjs';
|
|
2
2
|
import { B as BoardLabelOverrides } from './ui-copy-CKfFTtLk.mjs';
|
|
3
3
|
export { A as AlertsCopy, a as ApplyCopy, b as BlogCopy, c as BreadcrumbsCopy, C as CopyLinkCopy, E as EntityCopy, F as FooterCopy, J as JobCardCopy, d as JobDetailCopy, e as JobSearchCopy, N as NavCopy, P as PUBLIC_LABEL_GROUPS, f as PaginationCopy, S as SalaryCopy, U as UiCopy, u as uiCopy } from './ui-copy-CKfFTtLk.mjs';
|
|
4
|
-
import { C as CustomFieldDefinition } from './board-
|
|
4
|
+
import { C as CustomFieldDefinition } from './board-DCiNpgFf.mjs';
|
|
5
|
+
import './_spec-DRbgPIlN.mjs';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Date display helpers in the board language (required leading parameter,
|
package/dist/format.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { P as PublicJob, a as PublicJobCard, C as CustomFieldValues } from './jobs-
|
|
1
|
+
import { P as PublicJob, a as PublicJobCard, C as CustomFieldValues } from './jobs-DAGAVHQL.js';
|
|
2
2
|
import { B as BoardLabelOverrides } from './ui-copy-CKfFTtLk.js';
|
|
3
3
|
export { A as AlertsCopy, a as ApplyCopy, b as BlogCopy, c as BreadcrumbsCopy, C as CopyLinkCopy, E as EntityCopy, F as FooterCopy, J as JobCardCopy, d as JobDetailCopy, e as JobSearchCopy, N as NavCopy, P as PUBLIC_LABEL_GROUPS, f as PaginationCopy, S as SalaryCopy, U as UiCopy, u as uiCopy } from './ui-copy-CKfFTtLk.js';
|
|
4
|
-
import { C as CustomFieldDefinition } from './board-
|
|
4
|
+
import { C as CustomFieldDefinition } from './board-BxPUtrOl.js';
|
|
5
|
+
import './_spec-DRbgPIlN.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Date display helpers in the board language (required leading parameter,
|