@cavuno/board 1.29.0 → 1.31.0-preview.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/dist/bin.mjs CHANGED
@@ -98,20 +98,8 @@ function summarize(results) {
98
98
  return { exitCode: failed.length > 0 ? 1 : 0, passed, failed, skipped };
99
99
  }
100
100
 
101
- // src/doctor/run.ts
102
- import { existsSync, readFileSync as readFileSync2 } from "fs";
103
- import { join } from "path";
101
+ // src/doctor/probe.ts
104
102
  var FETCH_TIMEOUT_MS = 1e4;
105
- var READ = {
106
- home: record("read.home", 2),
107
- jobs: record("read.jobs", 2),
108
- jsonld: record("read.jsonld", 2),
109
- sitemap: record("read.sitemap", 2),
110
- robots: record("read.robots", 2)
111
- };
112
- var STATIC_API = record("static.api", 1);
113
- var STATIC_BOARD = record("static.board", 1);
114
- var STATIC_SKILLS = record("static.skills", 1);
115
103
  async function probe(fetchImpl, url) {
116
104
  try {
117
105
  const response = await fetchImpl(url, {
@@ -130,6 +118,316 @@ async function probe(fetchImpl, url) {
130
118
  };
131
119
  }
132
120
  }
121
+
122
+ // src/doctor/read.ts
123
+ var READ = {
124
+ home: record("read.home", 2),
125
+ jobs: record("read.jobs", 2),
126
+ jsonld: record("read.jsonld", 2),
127
+ sitemap: record("read.sitemap", 2),
128
+ robots: record("read.robots", 2)
129
+ };
130
+ function skipReadProbes(reason) {
131
+ return Object.values(READ).map((make) => make("skip", reason));
132
+ }
133
+ async function probeJobsAndJsonLd(fetchImpl, base) {
134
+ const jobs = await probe(fetchImpl, `${base}/jobs`);
135
+ const jobLink = jobs.ok ? extractJobDetailLink(jobs.body) : null;
136
+ const jobsResult = jobs.ok && jobLink ? READ.jobs("pass", `listing renders with job detail links (${jobLink})`) : READ.jobs(
137
+ "fail",
138
+ jobs.ok ? "listing renders but contains no /companies/{c}/jobs/{s} detail links" : `listing HTTP ${jobs.status}`
139
+ );
140
+ if (!jobLink) {
141
+ return [
142
+ jobsResult,
143
+ READ.jsonld(
144
+ "skip",
145
+ jobs.ok ? "not probed \u2014 the listing rendered no job detail link (see read.jobs failure)" : "not probed \u2014 the /jobs listing itself failed (see read.jobs failure)"
146
+ )
147
+ ];
148
+ }
149
+ const detail = await probe(fetchImpl, `${base}${jobLink}`);
150
+ const posting = detail.ok ? extractJobPostingJsonLd(detail.body) : null;
151
+ return [
152
+ jobsResult,
153
+ posting ? READ.jsonld(
154
+ "pass",
155
+ `JobPosting JSON-LD present (${String(posting.title ?? "")})`
156
+ ) : READ.jsonld(
157
+ "fail",
158
+ detail.ok ? "job detail page has no parseable JobPosting JSON-LD" : `job detail HTTP ${detail.status}`
159
+ )
160
+ ];
161
+ }
162
+ function onFrontend(base, locUrl) {
163
+ try {
164
+ const parsed = new URL(locUrl);
165
+ return `${base}${parsed.pathname}${parsed.search}`;
166
+ } catch {
167
+ return locUrl;
168
+ }
169
+ }
170
+ async function probeSitemap(fetchImpl, base) {
171
+ const sitemap = await probe(fetchImpl, `${base}/sitemap.xml`);
172
+ if (!sitemap.ok) {
173
+ return READ.sitemap("fail", `sitemap.xml HTTP ${sitemap.status}`);
174
+ }
175
+ const doc = parseSitemap(sitemap.body);
176
+ if (!doc || doc.urls.length === 0) {
177
+ return READ.sitemap(
178
+ "fail",
179
+ doc ? "sitemap.xml has no <loc> entries" : "sitemap.xml is not a sitemap"
180
+ );
181
+ }
182
+ let urls = doc.urls;
183
+ if (doc.kind === "index") {
184
+ const child = await probe(fetchImpl, onFrontend(base, urls[0]));
185
+ const childDoc = child.ok ? parseSitemap(child.body) : null;
186
+ if (!childDoc || childDoc.urls.length === 0) {
187
+ return READ.sitemap(
188
+ "fail",
189
+ `child sitemap ${urls[0]} ${child.ok ? "has no <loc> entries" : `HTTP ${child.status}`}`
190
+ );
191
+ }
192
+ urls = childDoc.urls;
193
+ }
194
+ const sampleUrl = onFrontend(base, urls[0]);
195
+ const sample = await probe(fetchImpl, sampleUrl);
196
+ return sample.ok ? READ.sitemap("pass", `${urls.length} entries; sample page resolves`) : READ.sitemap("fail", `sitemap page ${sampleUrl} \u2192 HTTP ${sample.status}`);
197
+ }
198
+ async function runReadProbes(fetchImpl, frontendUrl) {
199
+ const base = frontendUrl.replace(/\/$/, "");
200
+ const [home, jobsAndJsonLd, sitemap, robots] = await Promise.all([
201
+ probe(fetchImpl, base),
202
+ probeJobsAndJsonLd(fetchImpl, base),
203
+ probeSitemap(fetchImpl, base),
204
+ probe(fetchImpl, `${base}/robots.txt`)
205
+ ]);
206
+ return [
207
+ home.ok && /<(html|body|div|main)[\s>]/i.test(home.body) ? READ.home("pass", "home renders") : READ.home(
208
+ "fail",
209
+ home.ok ? "home returned 200 but no HTML document \u2014 captive portal or empty shell?" : `home HTTP ${home.status}`
210
+ ),
211
+ ...jobsAndJsonLd,
212
+ sitemap,
213
+ robots.ok && /user-agent\s*:/i.test(robots.body) ? READ.robots("pass", "present") : READ.robots(
214
+ "fail",
215
+ robots.ok ? "robots.txt returned 200 but has no User-agent directive" : `robots.txt HTTP ${robots.status}`
216
+ )
217
+ ];
218
+ }
219
+
220
+ // src/doctor/writes.ts
221
+ var WRITE = {
222
+ board: record("write.board", 3),
223
+ register: record("write.register", 3),
224
+ login: record("write.login", 3),
225
+ email: record("write.email", 3),
226
+ apply: record("write.apply", 3),
227
+ alert: record("write.alert", 3)
228
+ };
229
+ var DEPENDENT_CHECKS = [
230
+ WRITE.register,
231
+ WRITE.login,
232
+ WRITE.email,
233
+ WRITE.apply,
234
+ WRITE.alert
235
+ ];
236
+ function skipWriteProbes(reason) {
237
+ return Object.values(WRITE).map((make) => make("skip", reason));
238
+ }
239
+ var FETCH_TIMEOUT_MS2 = 1e4;
240
+ var RESEND_API_BASE = "https://api.resend.com";
241
+ async function request(fetchImpl, url, init) {
242
+ try {
243
+ const response = await fetchImpl(url, {
244
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS2),
245
+ ...init
246
+ });
247
+ let json = null;
248
+ try {
249
+ json = await response.json();
250
+ } catch {
251
+ json = null;
252
+ }
253
+ return { ok: response.ok, status: response.status, json };
254
+ } catch (error) {
255
+ return {
256
+ ok: false,
257
+ status: 0,
258
+ json: { error: error instanceof Error ? error.message : String(error) }
259
+ };
260
+ }
261
+ }
262
+ function post(fetchImpl, url, body, bearer) {
263
+ return request(fetchImpl, url, {
264
+ method: "POST",
265
+ headers: {
266
+ "content-type": "application/json",
267
+ accept: "application/json",
268
+ ...bearer ? { authorization: `Bearer ${bearer}` } : {}
269
+ },
270
+ body: JSON.stringify(body)
271
+ });
272
+ }
273
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
274
+ async function observeVerificationEmail(fetchImpl, apiKey, recipient, pollMs, timeoutMs) {
275
+ const deadline = Date.now() + timeoutMs;
276
+ const subjectMarker = `[To: ${recipient}]`;
277
+ do {
278
+ const list = await request(
279
+ fetchImpl,
280
+ `${RESEND_API_BASE}/emails?limit=20`,
281
+ {
282
+ headers: { authorization: `Bearer ${apiKey}` }
283
+ }
284
+ );
285
+ if (!list.ok) {
286
+ return WRITE.email(
287
+ "fail",
288
+ `Resend list returned HTTP ${list.status} \u2014 wrong RESEND_API_KEY?`
289
+ );
290
+ }
291
+ const items = list.json?.data ?? [];
292
+ const match = items.find(
293
+ (item) => item.subject?.includes(subjectMarker) || item.to?.includes(recipient)
294
+ );
295
+ if (match) {
296
+ return WRITE.email(
297
+ "pass",
298
+ `verification send observed in Resend for ${recipient}`
299
+ );
300
+ }
301
+ await sleep(pollMs);
302
+ } while (Date.now() < deadline);
303
+ return WRITE.email(
304
+ "fail",
305
+ `no verification send for ${recipient} appeared in Resend within ${timeoutMs}ms`
306
+ );
307
+ }
308
+ async function runWriteProbes(options) {
309
+ const { fetchImpl } = options;
310
+ const origin = new URL(options.env.apiUrl).origin;
311
+ const base = `${origin}/v1/boards/${encodeURIComponent(options.env.boardKey)}`;
312
+ const nonce = options.nonce ?? `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
313
+ const email = `doctor+${nonce}@cavuno.com`;
314
+ const password = `doctor-probe-${nonce}-A1!`;
315
+ let context = options.boardContext ?? null;
316
+ if (context === null) {
317
+ const r = await request(fetchImpl, base);
318
+ if (!r.ok) {
319
+ return [
320
+ WRITE.board("fail", `board context did not resolve (HTTP ${r.status})`),
321
+ ...DEPENDENT_CHECKS.map(
322
+ (make) => make("skip", "not run \u2014 the write gate failed (see write.board)")
323
+ )
324
+ ];
325
+ }
326
+ context = r.json ?? {};
327
+ }
328
+ if (context.sandbox !== true) {
329
+ return [
330
+ WRITE.board(
331
+ "fail",
332
+ "refusing write probes: this board is not the platform sandbox (`sandbox` is not true) \u2014 point PUBLIC_CAVUNO_BOARD at the sandbox pk_"
333
+ ),
334
+ ...DEPENDENT_CHECKS.map(
335
+ (make) => make("skip", "not run \u2014 the write gate failed (see write.board)")
336
+ )
337
+ ];
338
+ }
339
+ const results = [
340
+ WRITE.board("pass", "board is the platform sandbox")
341
+ ];
342
+ const registered = await post(fetchImpl, `${base}/auth/register`, {
343
+ role: "candidate",
344
+ method: "emailpass",
345
+ email,
346
+ password,
347
+ displayName: "Doctor Probe"
348
+ });
349
+ const sessionOk = (r) => r.ok && typeof r.json?.accessToken === "string";
350
+ results.push(
351
+ sessionOk(registered) ? WRITE.register("pass", `candidate registered (${email})`) : WRITE.register(
352
+ "fail",
353
+ `register returned HTTP ${registered.status}` + (registered.status === 429 ? " \u2014 rate-limited: register allows 5 signups / 15 min per IP; wait before rerunning" : "")
354
+ )
355
+ );
356
+ let accessToken = null;
357
+ if (sessionOk(registered)) {
358
+ const login = await post(fetchImpl, `${base}/auth/login`, {
359
+ email,
360
+ password
361
+ });
362
+ if (sessionOk(login)) {
363
+ accessToken = login.json.accessToken;
364
+ results.push(WRITE.login("pass", "emailpass login returns a session"));
365
+ } else {
366
+ results.push(WRITE.login("fail", `login returned HTTP ${login.status}`));
367
+ }
368
+ } else {
369
+ results.push(WRITE.login("skip", "not run \u2014 register failed"));
370
+ }
371
+ if (!sessionOk(registered)) {
372
+ results.push(WRITE.email("skip", "not run \u2014 register failed"));
373
+ } else if (!options.resendApiKey) {
374
+ results.push(
375
+ WRITE.email(
376
+ "skip",
377
+ "RESEND_API_KEY not set \u2014 the verification send was NOT verified (platform-operator check)"
378
+ )
379
+ );
380
+ } else {
381
+ results.push(
382
+ await observeVerificationEmail(
383
+ fetchImpl,
384
+ options.resendApiKey,
385
+ email,
386
+ options.emailPollMs ?? 2e3,
387
+ options.emailTimeoutMs ?? 3e4
388
+ )
389
+ );
390
+ }
391
+ if (!accessToken) {
392
+ results.push(WRITE.apply("skip", "not run \u2014 no session (see above)"));
393
+ } else {
394
+ const listing = await request(fetchImpl, `${base}/jobs?limit=1`);
395
+ const card = listing.json?.data?.[0];
396
+ if (!listing.ok || !card?.slug) {
397
+ results.push(
398
+ WRITE.apply(
399
+ "fail",
400
+ listing.ok ? "the board has no jobs to apply to \u2014 is the sandbox seeded?" : `jobs listing returned HTTP ${listing.status}`
401
+ )
402
+ );
403
+ } else {
404
+ const applied = await post(
405
+ fetchImpl,
406
+ `${base}/jobs/${encodeURIComponent(card.slug)}/apply`,
407
+ {},
408
+ accessToken
409
+ );
410
+ results.push(
411
+ applied.ok && typeof applied.json?.id === "string" ? WRITE.apply("pass", `applied to ${card.slug}`) : WRITE.apply("fail", `apply returned HTTP ${applied.status}`)
412
+ );
413
+ }
414
+ }
415
+ const alert = await post(fetchImpl, `${base}/job-alerts`, {
416
+ email,
417
+ consent: true
418
+ });
419
+ results.push(
420
+ alert.ok ? WRITE.alert("pass", "job-alert subscription accepted") : WRITE.alert("fail", `job-alerts returned HTTP ${alert.status}`)
421
+ );
422
+ return results;
423
+ }
424
+
425
+ // src/doctor/run.ts
426
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
427
+ import { join } from "path";
428
+ var STATIC_API = record("static.api", 1);
429
+ var STATIC_BOARD = record("static.board", 1);
430
+ var STATIC_SKILLS = record("static.skills", 1);
133
431
  async function checkApiReachable(fetchImpl, apiUrl) {
134
432
  let origin;
135
433
  try {
@@ -163,21 +461,31 @@ async function checkBoardResolves(fetchImpl, env) {
163
461
  `${origin}/v1/boards/${encodeURIComponent(env.boardKey)}`
164
462
  );
165
463
  if (!result.ok) {
166
- return STATIC_BOARD(
167
- "fail",
168
- `board did not resolve for this key (HTTP ${result.status}) \u2014 revoked or wrong key?`
169
- );
464
+ return {
465
+ result: STATIC_BOARD(
466
+ "fail",
467
+ `board did not resolve for this key (HTTP ${result.status}) \u2014 revoked or wrong key?`
468
+ ),
469
+ context: null
470
+ };
170
471
  }
472
+ let parsed = null;
171
473
  try {
172
- const parsed = JSON.parse(result.body);
474
+ parsed = JSON.parse(result.body);
173
475
  if (typeof parsed !== "object" || parsed === null) throw new Error("shape");
174
476
  } catch {
175
- return STATIC_BOARD(
176
- "fail",
177
- "board endpoint returned 200 but not JSON \u2014 proxy or captive portal in the way?"
178
- );
477
+ return {
478
+ result: STATIC_BOARD(
479
+ "fail",
480
+ "board endpoint returned 200 but not JSON \u2014 proxy or captive portal in the way?"
481
+ ),
482
+ context: null
483
+ };
179
484
  }
180
- return STATIC_BOARD("pass", "publishable key resolves the board");
485
+ return {
486
+ result: STATIC_BOARD("pass", "publishable key resolves the board"),
487
+ context: parsed
488
+ };
181
489
  }
182
490
  var SKILL_ROOTS = [".claude/skills", ".agents/skills"];
183
491
  function checkSkillsFreshness(projectRoot) {
@@ -218,71 +526,6 @@ function checkSkillsFreshness(projectRoot) {
218
526
  `stale skills (re-run \`npx @cavuno/board setup\`): ${[...stale].join(", ")}`
219
527
  );
220
528
  }
221
- async function probeJobsAndJsonLd(fetchImpl, base) {
222
- const jobs = await probe(fetchImpl, `${base}/jobs`);
223
- const jobLink = jobs.ok ? extractJobDetailLink(jobs.body) : null;
224
- const jobsResult = jobs.ok && jobLink ? READ.jobs("pass", `listing renders with job detail links (${jobLink})`) : READ.jobs(
225
- "fail",
226
- jobs.ok ? "listing renders but contains no /companies/{c}/jobs/{s} detail links" : `listing HTTP ${jobs.status}`
227
- );
228
- if (!jobLink) {
229
- return [
230
- jobsResult,
231
- READ.jsonld(
232
- "skip",
233
- jobs.ok ? "not probed \u2014 the listing rendered no job detail link (see read.jobs failure)" : "not probed \u2014 the /jobs listing itself failed (see read.jobs failure)"
234
- )
235
- ];
236
- }
237
- const detail = await probe(fetchImpl, `${base}${jobLink}`);
238
- const posting = detail.ok ? extractJobPostingJsonLd(detail.body) : null;
239
- return [
240
- jobsResult,
241
- posting ? READ.jsonld(
242
- "pass",
243
- `JobPosting JSON-LD present (${String(posting.title ?? "")})`
244
- ) : READ.jsonld(
245
- "fail",
246
- detail.ok ? "job detail page has no parseable JobPosting JSON-LD" : `job detail HTTP ${detail.status}`
247
- )
248
- ];
249
- }
250
- function onFrontend(base, locUrl) {
251
- try {
252
- const parsed = new URL(locUrl);
253
- return `${base}${parsed.pathname}${parsed.search}`;
254
- } catch {
255
- return locUrl;
256
- }
257
- }
258
- async function probeSitemap(fetchImpl, base) {
259
- const sitemap = await probe(fetchImpl, `${base}/sitemap.xml`);
260
- if (!sitemap.ok) {
261
- return READ.sitemap("fail", `sitemap.xml HTTP ${sitemap.status}`);
262
- }
263
- const doc = parseSitemap(sitemap.body);
264
- if (!doc || doc.urls.length === 0) {
265
- return READ.sitemap(
266
- "fail",
267
- doc ? "sitemap.xml has no <loc> entries" : "sitemap.xml is not a sitemap"
268
- );
269
- }
270
- let urls = doc.urls;
271
- if (doc.kind === "index") {
272
- const child = await probe(fetchImpl, onFrontend(base, urls[0]));
273
- const childDoc = child.ok ? parseSitemap(child.body) : null;
274
- if (!childDoc || childDoc.urls.length === 0) {
275
- return READ.sitemap(
276
- "fail",
277
- `child sitemap ${urls[0]} ${child.ok ? "has no <loc> entries" : `HTTP ${child.status}`}`
278
- );
279
- }
280
- urls = childDoc.urls;
281
- }
282
- const sampleUrl = onFrontend(base, urls[0]);
283
- const sample = await probe(fetchImpl, sampleUrl);
284
- return sample.ok ? READ.sitemap("pass", `${urls.length} entries; sample page resolves`) : READ.sitemap("fail", `sitemap page ${sampleUrl} \u2192 HTTP ${sample.status}`);
285
- }
286
529
  async function runDoctor(options) {
287
530
  const fetchImpl = options.fetchImpl ?? fetch;
288
531
  const results = [];
@@ -297,35 +540,37 @@ async function runDoctor(options) {
297
540
  } else {
298
541
  results.push(STATIC_API("skip", "PUBLIC_CAVUNO_API_URL not set"));
299
542
  }
300
- results.push(
301
- envOk ? await checkBoardResolves(fetchImpl, options.env) : STATIC_BOARD("skip", "env checks failed \u2014 fix them first")
302
- );
303
- results.push(checkSkillsFreshness(options.projectRoot ?? process.cwd()));
304
- if (!options.frontendUrl) {
305
- for (const make of Object.values(READ)) {
306
- results.push(make("skip", "no --frontend <url> given"));
307
- }
308
- return { results, summary: summarize(results) };
543
+ let boardContext = null;
544
+ if (envOk) {
545
+ const resolved = await checkBoardResolves(fetchImpl, options.env);
546
+ boardContext = resolved.context;
547
+ results.push(resolved.result);
548
+ } else {
549
+ results.push(STATIC_BOARD("skip", "env checks failed \u2014 fix them first"));
309
550
  }
310
- const base = options.frontendUrl.replace(/\/$/, "");
311
- const [home, jobsAndJsonLd, sitemap, robots] = await Promise.all([
312
- probe(fetchImpl, base),
313
- probeJobsAndJsonLd(fetchImpl, base),
314
- probeSitemap(fetchImpl, base),
315
- probe(fetchImpl, `${base}/robots.txt`)
316
- ]);
551
+ results.push(checkSkillsFreshness(options.projectRoot ?? process.cwd()));
317
552
  results.push(
318
- home.ok && /<(html|body|div|main)[\s>]/i.test(home.body) ? READ.home("pass", "home renders") : READ.home(
319
- "fail",
320
- home.ok ? "home returned 200 but no HTML document \u2014 captive portal or empty shell?" : `home HTTP ${home.status}`
321
- ),
322
- ...jobsAndJsonLd,
323
- sitemap,
324
- robots.ok && /user-agent\s*:/i.test(robots.body) ? READ.robots("pass", "present") : READ.robots(
325
- "fail",
326
- robots.ok ? "robots.txt returned 200 but has no User-agent directive" : `robots.txt HTTP ${robots.status}`
327
- )
553
+ ...options.frontendUrl ? await runReadProbes(fetchImpl, options.frontendUrl) : skipReadProbes("no --frontend <url> given")
328
554
  );
555
+ if (!options.sandbox) {
556
+ results.push(
557
+ ...skipWriteProbes(
558
+ "write probes not enabled \u2014 run with --sandbox against the platform sandbox"
559
+ )
560
+ );
561
+ } else if (!envOk) {
562
+ results.push(...skipWriteProbes("env checks failed \u2014 fix them first"));
563
+ } else {
564
+ results.push(
565
+ ...await runWriteProbes({
566
+ env: options.env,
567
+ fetchImpl,
568
+ resendApiKey: options.resendApiKey,
569
+ nonce: options.writeProbeNonce,
570
+ boardContext
571
+ })
572
+ );
573
+ }
329
574
  return { results, summary: summarize(results) };
330
575
  }
331
576
 
@@ -375,7 +620,11 @@ async function doctor(argv) {
375
620
  apiUrl: process.env.PUBLIC_CAVUNO_API_URL,
376
621
  boardKey: process.env.PUBLIC_CAVUNO_BOARD
377
622
  },
378
- frontendUrl
623
+ frontendUrl,
624
+ // Tier-3 write probes: opt-in via --sandbox, and refused server-side
625
+ // unless the resolved board is platform-marked test (writes.ts gate).
626
+ sandbox: argv.includes("--sandbox"),
627
+ resendApiKey: process.env.RESEND_API_KEY
379
628
  });
380
629
  console.log("\n@cavuno/board doctor");
381
630
  for (const result of results) {
@@ -0,0 +1,17 @@
1
+ import { b as Schemas } from './jobs-Dmz8uexp.js';
2
+
3
+ type PublicBoard = Schemas['PublicBoardContext'];
4
+ type PublicBoardFeatures = PublicBoard['features'];
5
+ type PublicBoardAnalytics = PublicBoard['analytics'];
6
+ type PublicBoardTheme = NonNullable<PublicBoard['theme']>;
7
+ /**
8
+ * An operator-defined custom job-field definition (CAV-294). Board-wide;
9
+ * use it to render and localize a job's opaque `customFieldValues` (resolve
10
+ * option `key`s → labels, honour field `type` and display order). Shared with
11
+ * the Operator API's custom-field surface (one canonical schema).
12
+ */
13
+ type CustomFieldDefinition = Schemas['CustomFieldDefinition'];
14
+ type CustomFieldType = CustomFieldDefinition['type'];
15
+ type CustomFieldOption = Schemas['CustomFieldOption'];
16
+
17
+ export type { CustomFieldDefinition as C, PublicBoard as P, CustomFieldOption as a, CustomFieldType as b, PublicBoardAnalytics as c, PublicBoardFeatures as d, PublicBoardTheme as e };
@@ -0,0 +1,17 @@
1
+ import { b as Schemas } from './jobs-Dmz8uexp.mjs';
2
+
3
+ type PublicBoard = Schemas['PublicBoardContext'];
4
+ type PublicBoardFeatures = PublicBoard['features'];
5
+ type PublicBoardAnalytics = PublicBoard['analytics'];
6
+ type PublicBoardTheme = NonNullable<PublicBoard['theme']>;
7
+ /**
8
+ * An operator-defined custom job-field definition (CAV-294). Board-wide;
9
+ * use it to render and localize a job's opaque `customFieldValues` (resolve
10
+ * option `key`s → labels, honour field `type` and display order). Shared with
11
+ * the Operator API's custom-field surface (one canonical schema).
12
+ */
13
+ type CustomFieldDefinition = Schemas['CustomFieldDefinition'];
14
+ type CustomFieldType = CustomFieldDefinition['type'];
15
+ type CustomFieldOption = Schemas['CustomFieldOption'];
16
+
17
+ export type { CustomFieldDefinition as C, PublicBoard as P, CustomFieldOption as a, CustomFieldType as b, PublicBoardAnalytics as c, PublicBoardFeatures as d, PublicBoardTheme as e };
@@ -1,4 +1,4 @@
1
- import { J as JobSort, E as EmploymentType, R as RemoteOption, S as Seniority } from './jobs-DK5mPBgq.mjs';
1
+ import { J as JobSort, E as EmploymentType, R as RemoteOption, S as Seniority } from './jobs-Dmz8uexp.mjs';
2
2
 
3
3
  declare const REMOTE_OPTIONS: readonly RemoteOption[];
4
4
  /**
package/dist/filters.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { J as JobSort, E as EmploymentType, R as RemoteOption, S as Seniority } from './jobs-DK5mPBgq.js';
1
+ import { J as JobSort, E as EmploymentType, R as RemoteOption, S as Seniority } from './jobs-Dmz8uexp.js';
2
2
 
3
3
  declare const REMOTE_OPTIONS: readonly RemoteOption[];
4
4
  /**
package/dist/format.d.mts CHANGED
@@ -1,4 +1,5 @@
1
- import { P as PublicJob, a as PublicJobCard } from './jobs-DK5mPBgq.mjs';
1
+ import { P as PublicJob, a as PublicJobCard, C as CustomFieldValues } from './jobs-Dmz8uexp.mjs';
2
+ import { C as CustomFieldDefinition } from './board-BS8Ax7hz.mjs';
2
3
 
3
4
  /**
4
5
  * Date display helpers in the board language (required leading parameter,
@@ -237,4 +238,31 @@ interface SalaryTimeframeOverrides {
237
238
  */
238
239
  declare function formatSalaryRange(language: string | undefined, min: number | null, max: number | null, timeframe: SalaryTimeframeInput, currency?: string | null, timeframeOverrides?: SalaryTimeframeOverrides): string | null;
239
240
 
240
- export { type SalaryFrames, type SalaryLexicon, type SalaryTimeframeInput, type SalaryTimeframeOverrides, type SalaryTimeframeValue, type SeniorityKey, cardLocationLabel, fieldLabel, formatDate, formatMonthYear, formatPublishedRelativeDate, formatSalaryRange, fullJobToCard, getSalaryLexicon, locationLabel };
241
+ declare function companyIntro(summary: string | null, description: string | null): string | null;
242
+
243
+ /**
244
+ * Resolve a job's opaque `customFieldValues` into ordered, display-ready
245
+ * entries for the "Additional details" section — the client-side twin of the
246
+ * hosted `resolveCustomFieldDisplay` (apps/web/lib/jobs/job-form-config/
247
+ * custom-field-display.ts). Pure. Iterates the *definitions* (from board
248
+ * context) in config order, so display order is operator-controlled, orphan
249
+ * values (deleted definitions) are never inspected, and empty values drop per
250
+ * field. Select values resolve their stored option KEY to the current label
251
+ * (rename-safe); an option with no current match is dropped. Booleans stay raw
252
+ * (a discriminated `kind`) — the render layer authors the "Yes"/"No" chrome
253
+ * (English; localized copy is the use-intl follow-up).
254
+ */
255
+ type CustomFieldDisplayEntry = {
256
+ key: string;
257
+ label: string;
258
+ kind: 'text';
259
+ value: string;
260
+ } | {
261
+ key: string;
262
+ label: string;
263
+ kind: 'boolean';
264
+ value: boolean;
265
+ };
266
+ declare function resolveCustomFieldDisplay(definitions: CustomFieldDefinition[] | undefined, values: CustomFieldValues | undefined): CustomFieldDisplayEntry[];
267
+
268
+ export { type CustomFieldDisplayEntry, type SalaryFrames, type SalaryLexicon, type SalaryTimeframeInput, type SalaryTimeframeOverrides, type SalaryTimeframeValue, type SeniorityKey, cardLocationLabel, companyIntro, fieldLabel, formatDate, formatMonthYear, formatPublishedRelativeDate, formatSalaryRange, fullJobToCard, getSalaryLexicon, locationLabel, resolveCustomFieldDisplay };
package/dist/format.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { P as PublicJob, a as PublicJobCard } from './jobs-DK5mPBgq.js';
1
+ import { P as PublicJob, a as PublicJobCard, C as CustomFieldValues } from './jobs-Dmz8uexp.js';
2
+ import { C as CustomFieldDefinition } from './board--nLjpneU.js';
2
3
 
3
4
  /**
4
5
  * Date display helpers in the board language (required leading parameter,
@@ -237,4 +238,31 @@ interface SalaryTimeframeOverrides {
237
238
  */
238
239
  declare function formatSalaryRange(language: string | undefined, min: number | null, max: number | null, timeframe: SalaryTimeframeInput, currency?: string | null, timeframeOverrides?: SalaryTimeframeOverrides): string | null;
239
240
 
240
- export { type SalaryFrames, type SalaryLexicon, type SalaryTimeframeInput, type SalaryTimeframeOverrides, type SalaryTimeframeValue, type SeniorityKey, cardLocationLabel, fieldLabel, formatDate, formatMonthYear, formatPublishedRelativeDate, formatSalaryRange, fullJobToCard, getSalaryLexicon, locationLabel };
241
+ declare function companyIntro(summary: string | null, description: string | null): string | null;
242
+
243
+ /**
244
+ * Resolve a job's opaque `customFieldValues` into ordered, display-ready
245
+ * entries for the "Additional details" section — the client-side twin of the
246
+ * hosted `resolveCustomFieldDisplay` (apps/web/lib/jobs/job-form-config/
247
+ * custom-field-display.ts). Pure. Iterates the *definitions* (from board
248
+ * context) in config order, so display order is operator-controlled, orphan
249
+ * values (deleted definitions) are never inspected, and empty values drop per
250
+ * field. Select values resolve their stored option KEY to the current label
251
+ * (rename-safe); an option with no current match is dropped. Booleans stay raw
252
+ * (a discriminated `kind`) — the render layer authors the "Yes"/"No" chrome
253
+ * (English; localized copy is the use-intl follow-up).
254
+ */
255
+ type CustomFieldDisplayEntry = {
256
+ key: string;
257
+ label: string;
258
+ kind: 'text';
259
+ value: string;
260
+ } | {
261
+ key: string;
262
+ label: string;
263
+ kind: 'boolean';
264
+ value: boolean;
265
+ };
266
+ declare function resolveCustomFieldDisplay(definitions: CustomFieldDefinition[] | undefined, values: CustomFieldValues | undefined): CustomFieldDisplayEntry[];
267
+
268
+ export { type CustomFieldDisplayEntry, type SalaryFrames, type SalaryLexicon, type SalaryTimeframeInput, type SalaryTimeframeOverrides, type SalaryTimeframeValue, type SeniorityKey, cardLocationLabel, companyIntro, fieldLabel, formatDate, formatMonthYear, formatPublishedRelativeDate, formatSalaryRange, fullJobToCard, getSalaryLexicon, locationLabel, resolveCustomFieldDisplay };