@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.
Files changed (45) hide show
  1. package/README.md +7 -0
  2. package/dist/{jobs-DMH3Ytoq.d.mts → _spec-DRbgPIlN.d.mts} +404 -96
  3. package/dist/{jobs-DMH3Ytoq.d.ts → _spec-DRbgPIlN.d.ts} +404 -96
  4. package/dist/bin.mjs +242 -18
  5. package/dist/{board-CBry4f7H.d.mts → board-BxPUtrOl.d.ts} +1 -1
  6. package/dist/{board-tV-zcaHs.d.ts → board-DCiNpgFf.d.mts} +1 -1
  7. package/dist/doctor.js +238 -14
  8. package/dist/doctor.mjs +238 -14
  9. package/dist/filters.d.mts +18 -4
  10. package/dist/filters.d.ts +18 -4
  11. package/dist/filters.js +17 -0
  12. package/dist/filters.mjs +17 -0
  13. package/dist/format.d.mts +3 -2
  14. package/dist/format.d.ts +3 -2
  15. package/dist/index.d.mts +62 -73
  16. package/dist/index.d.ts +62 -73
  17. package/dist/index.js +71 -1
  18. package/dist/index.mjs +71 -1
  19. package/dist/jobs-DAGAVHQL.d.ts +105 -0
  20. package/dist/jobs-DhePKSRe.d.mts +105 -0
  21. package/dist/{salaries-CaGlcCC6.d.ts → salaries-D6SUVMmt.d.mts} +2 -1
  22. package/dist/{salaries-01g4wK8y.d.mts → salaries-cqb78kg0.d.ts} +2 -1
  23. package/dist/search-BalPAS0P.d.ts +81 -0
  24. package/dist/search-DYUQzCq_.d.mts +81 -0
  25. package/dist/seo.d.mts +4 -3
  26. package/dist/seo.d.ts +4 -3
  27. package/dist/server.d.mts +56 -4
  28. package/dist/server.d.ts +56 -4
  29. package/dist/server.js +41 -0
  30. package/dist/server.mjs +41 -0
  31. package/dist/sitemap.d.mts +5 -3
  32. package/dist/sitemap.d.ts +5 -3
  33. package/dist/suggest.d.mts +70 -0
  34. package/dist/suggest.d.ts +70 -0
  35. package/dist/suggest.js +165 -0
  36. package/dist/suggest.mjs +144 -0
  37. package/dist/theme.d.mts +49 -1
  38. package/dist/theme.d.ts +49 -1
  39. package/dist/theme.js +60 -26
  40. package/dist/theme.mjs +60 -26
  41. package/package.json +11 -1
  42. package/skills/cavuno-board-filters/SKILL.md +11 -4
  43. package/skills/cavuno-board-jobs/SKILL.md +18 -2
  44. package/skills/cavuno-board-suggest/SKILL.md +119 -0
  45. 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 readFileSync2 } from "fs";
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 = join(projectRoot, "src/tokens.css");
231
- if (!existsSync(tokensPath)) {
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(readFileSync2(tokensPath, "utf8"), "utf8").digest("hex");
240
- const resolvedPath = join(projectRoot, "src/theme/resolved.ts");
241
- const resolvedHash = existsSync(resolvedPath) ? readFileSync2(resolvedPath, "utf8").match(
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 existsSync2, readFileSync as readFileSync3 } from "fs";
482
- import { join as join2 } from "path";
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) => join2(projectRoot, root)).filter(
549
- (root) => existsSync2(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 = join2(root, skill.name, "SKILL.md");
563
- if (!existsSync2(copied)) continue;
780
+ const copied = join3(root, skill.name, "SKILL.md");
781
+ if (!existsSync3(copied)) continue;
564
782
  seen.add(skill.name);
565
- if (readFileSync3(copied, "utf8") !== skill.content) {
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
  );
@@ -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-DMH3Ytoq.mjs';
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-DMH3Ytoq.js';
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-DMH3Ytoq.mjs';
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-CBry4f7H.mjs';
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-DMH3Ytoq.js';
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-tV-zcaHs.js';
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,