@cavuno/board 1.29.0 → 1.30.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) {
@@ -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-lzeoVGY6.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-lzeoVGY6.js';
2
2
 
3
3
  declare const REMOTE_OPTIONS: readonly RemoteOption[];
4
4
  /**
package/dist/format.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { P as PublicJob, a as PublicJobCard } from './jobs-DK5mPBgq.mjs';
1
+ import { P as PublicJob, a as PublicJobCard } from './jobs-lzeoVGY6.mjs';
2
2
 
3
3
  /**
4
4
  * Date display helpers in the board language (required leading parameter,
package/dist/format.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { P as PublicJob, a as PublicJobCard } from './jobs-DK5mPBgq.js';
1
+ import { P as PublicJob, a as PublicJobCard } from './jobs-lzeoVGY6.js';
2
2
 
3
3
  /**
4
4
  * Date display helpers in the board language (required leading parameter,
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
- import { b as Schemas, R as RemoteOption, E as EmploymentType, S as Seniority, L as ListEnvelope, c as components, d as JobsListQuery, e as JobCardListEnvelope, f as JobsSearchBody, g as JobCardSearchEnvelope, h as JobsSimilarQuery, i as SearchEnvelope } from './jobs-DK5mPBgq.mjs';
2
- export { C as CustomFieldValue, j as CustomFieldValues, k as EducationRequirement, l as JobCompany, J as JobSort, O as OfficeLocation, P as PublicJob, a as PublicJobCard, m as RelatedSearch, n as RemotePermit, o as RemoteTimezone, p as StorefrontPagination } from './jobs-DK5mPBgq.mjs';
3
- import { c as CompaniesListQuery, d as CompanyListEnvelope, e as CompaniesSearchBody, f as CompanyJobsListQuery, g as CompanySimilarQuery, h as CompanyMarketsListQuery, i as SalaryDetailQuery, j as BlogPostsListQuery, k as BlogSimilarQuery, l as BlogSearchBody } from './salaries-CrJsaZe6.mjs';
4
- export { B as BlogAuthorEmbed, m as BlogTagEmbed, C as CompanyCategorySalary, n as CompanyMarket, o as CompanyMarketRef, b as CompanySalary, p as CustomFieldDefinition, q as CustomFieldOption, r as CustomFieldType, L as LocationSalaryDetail, s as LocationSkillsIndex, t as LocationTitlesIndex, u as PublicBlogAdjacentPosts, v as PublicBlogAuthor, w as PublicBlogPost, a as PublicBlogPostSummary, x as PublicBlogTag, P as PublicBoard, y as PublicBoardAnalytics, z as PublicBoardFeatures, A as PublicBoardTheme, D as PublicCompany, E as PublicCompanyDetail, F as SalaryCompany, G as SalaryLocation, H as SalarySkill, I as SalaryTitle, J as SkillLocationSalary, K as SkillLocationsIndex, S as SkillSalaryDetail, M as TitleLocationSalary, N as TitleLocationsIndex, T as TitleSalaryDetail } from './salaries-CrJsaZe6.mjs';
1
+ import { b as Schemas, R as RemoteOption, E as EmploymentType, S as Seniority, L as ListEnvelope, c as components, d as JobsListQuery, e as JobCardListEnvelope, f as JobsSearchBody, g as JobCardSearchEnvelope, h as JobsSimilarQuery, i as SearchEnvelope } from './jobs-lzeoVGY6.mjs';
2
+ export { C as CustomFieldValue, j as CustomFieldValues, k as EducationRequirement, l as JobCompany, J as JobSort, O as OfficeLocation, P as PublicJob, a as PublicJobCard, m as RelatedSearch, n as RemotePermit, o as RemoteTimezone, p as StorefrontPagination } from './jobs-lzeoVGY6.mjs';
3
+ import { c as CompaniesListQuery, d as CompanyListEnvelope, e as CompaniesSearchBody, f as CompanyJobsListQuery, g as CompanySimilarQuery, h as CompanyMarketsListQuery, i as SalaryDetailQuery, j as BlogPostsListQuery, k as BlogSimilarQuery, l as BlogSearchBody } from './salaries-DpH5CxdM.mjs';
4
+ export { B as BlogAuthorEmbed, m as BlogTagEmbed, C as CompanyCategorySalary, n as CompanyMarket, o as CompanyMarketRef, b as CompanySalary, p as CustomFieldDefinition, q as CustomFieldOption, r as CustomFieldType, L as LocationSalaryDetail, s as LocationSkillsIndex, t as LocationTitlesIndex, u as PublicBlogAdjacentPosts, v as PublicBlogAuthor, w as PublicBlogPost, a as PublicBlogPostSummary, x as PublicBlogTag, P as PublicBoard, y as PublicBoardAnalytics, z as PublicBoardFeatures, A as PublicBoardTheme, D as PublicCompany, E as PublicCompanyDetail, F as SalaryCompany, G as SalaryLocation, H as SalarySkill, I as SalaryTitle, J as SkillLocationSalary, K as SkillLocationsIndex, S as SkillSalaryDetail, M as TitleLocationSalary, N as TitleLocationsIndex, T as TitleSalaryDetail } from './salaries-DpH5CxdM.mjs';
5
5
 
6
6
  type Awaitable<T> = T | Promise<T>;
7
7
  /**
@@ -203,7 +203,7 @@ declare function paginate<Q extends Record<string, unknown>, P extends PageShape
203
203
  * constant because the package is platform-neutral and cannot read
204
204
  * package.json at runtime.
205
205
  */
206
- declare const SDK_VERSION = "1.29.0";
206
+ declare const SDK_VERSION = "1.30.0";
207
207
 
208
208
  type SavedJob = Schemas['SavedJob'];
209
209
  type SavedJobsListQuery = {
@@ -654,6 +654,7 @@ declare function createBoardClient(options: CreateBoardClientOptions): {
654
654
  logoUrl: string | null;
655
655
  primaryDomain: string | null;
656
656
  showCavunoBranding: boolean;
657
+ sandbox: boolean;
657
658
  features: {
658
659
  jobAlerts: boolean;
659
660
  candidates: boolean;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { b as Schemas, R as RemoteOption, E as EmploymentType, S as Seniority, L as ListEnvelope, c as components, d as JobsListQuery, e as JobCardListEnvelope, f as JobsSearchBody, g as JobCardSearchEnvelope, h as JobsSimilarQuery, i as SearchEnvelope } from './jobs-DK5mPBgq.js';
2
- export { C as CustomFieldValue, j as CustomFieldValues, k as EducationRequirement, l as JobCompany, J as JobSort, O as OfficeLocation, P as PublicJob, a as PublicJobCard, m as RelatedSearch, n as RemotePermit, o as RemoteTimezone, p as StorefrontPagination } from './jobs-DK5mPBgq.js';
3
- import { c as CompaniesListQuery, d as CompanyListEnvelope, e as CompaniesSearchBody, f as CompanyJobsListQuery, g as CompanySimilarQuery, h as CompanyMarketsListQuery, i as SalaryDetailQuery, j as BlogPostsListQuery, k as BlogSimilarQuery, l as BlogSearchBody } from './salaries-CXt6Vkrp.js';
4
- export { B as BlogAuthorEmbed, m as BlogTagEmbed, C as CompanyCategorySalary, n as CompanyMarket, o as CompanyMarketRef, b as CompanySalary, p as CustomFieldDefinition, q as CustomFieldOption, r as CustomFieldType, L as LocationSalaryDetail, s as LocationSkillsIndex, t as LocationTitlesIndex, u as PublicBlogAdjacentPosts, v as PublicBlogAuthor, w as PublicBlogPost, a as PublicBlogPostSummary, x as PublicBlogTag, P as PublicBoard, y as PublicBoardAnalytics, z as PublicBoardFeatures, A as PublicBoardTheme, D as PublicCompany, E as PublicCompanyDetail, F as SalaryCompany, G as SalaryLocation, H as SalarySkill, I as SalaryTitle, J as SkillLocationSalary, K as SkillLocationsIndex, S as SkillSalaryDetail, M as TitleLocationSalary, N as TitleLocationsIndex, T as TitleSalaryDetail } from './salaries-CXt6Vkrp.js';
1
+ import { b as Schemas, R as RemoteOption, E as EmploymentType, S as Seniority, L as ListEnvelope, c as components, d as JobsListQuery, e as JobCardListEnvelope, f as JobsSearchBody, g as JobCardSearchEnvelope, h as JobsSimilarQuery, i as SearchEnvelope } from './jobs-lzeoVGY6.js';
2
+ export { C as CustomFieldValue, j as CustomFieldValues, k as EducationRequirement, l as JobCompany, J as JobSort, O as OfficeLocation, P as PublicJob, a as PublicJobCard, m as RelatedSearch, n as RemotePermit, o as RemoteTimezone, p as StorefrontPagination } from './jobs-lzeoVGY6.js';
3
+ import { c as CompaniesListQuery, d as CompanyListEnvelope, e as CompaniesSearchBody, f as CompanyJobsListQuery, g as CompanySimilarQuery, h as CompanyMarketsListQuery, i as SalaryDetailQuery, j as BlogPostsListQuery, k as BlogSimilarQuery, l as BlogSearchBody } from './salaries-Cy3MhZov.js';
4
+ export { B as BlogAuthorEmbed, m as BlogTagEmbed, C as CompanyCategorySalary, n as CompanyMarket, o as CompanyMarketRef, b as CompanySalary, p as CustomFieldDefinition, q as CustomFieldOption, r as CustomFieldType, L as LocationSalaryDetail, s as LocationSkillsIndex, t as LocationTitlesIndex, u as PublicBlogAdjacentPosts, v as PublicBlogAuthor, w as PublicBlogPost, a as PublicBlogPostSummary, x as PublicBlogTag, P as PublicBoard, y as PublicBoardAnalytics, z as PublicBoardFeatures, A as PublicBoardTheme, D as PublicCompany, E as PublicCompanyDetail, F as SalaryCompany, G as SalaryLocation, H as SalarySkill, I as SalaryTitle, J as SkillLocationSalary, K as SkillLocationsIndex, S as SkillSalaryDetail, M as TitleLocationSalary, N as TitleLocationsIndex, T as TitleSalaryDetail } from './salaries-Cy3MhZov.js';
5
5
 
6
6
  type Awaitable<T> = T | Promise<T>;
7
7
  /**
@@ -203,7 +203,7 @@ declare function paginate<Q extends Record<string, unknown>, P extends PageShape
203
203
  * constant because the package is platform-neutral and cannot read
204
204
  * package.json at runtime.
205
205
  */
206
- declare const SDK_VERSION = "1.29.0";
206
+ declare const SDK_VERSION = "1.30.0";
207
207
 
208
208
  type SavedJob = Schemas['SavedJob'];
209
209
  type SavedJobsListQuery = {
@@ -654,6 +654,7 @@ declare function createBoardClient(options: CreateBoardClientOptions): {
654
654
  logoUrl: string | null;
655
655
  primaryDomain: string | null;
656
656
  showCavunoBranding: boolean;
657
+ sandbox: boolean;
657
658
  features: {
658
659
  jobAlerts: boolean;
659
660
  candidates: boolean;
package/dist/index.js CHANGED
@@ -310,7 +310,7 @@ async function clearSession(storage) {
310
310
  }
311
311
 
312
312
  // src/version.ts
313
- var SDK_VERSION = "1.29.0";
313
+ var SDK_VERSION = "1.30.0";
314
314
 
315
315
  // src/client.ts
316
316
  function isRawBody(body) {
package/dist/index.mjs CHANGED
@@ -265,7 +265,7 @@ async function clearSession(storage) {
265
265
  }
266
266
 
267
267
  // src/version.ts
268
- var SDK_VERSION = "1.29.0";
268
+ var SDK_VERSION = "1.30.0";
269
269
 
270
270
  // src/client.ts
271
271
  function isRawBody(body) {
@@ -2382,6 +2382,8 @@ interface components {
2382
2382
  primaryDomain: string | null;
2383
2383
  /** @description Whitelabel toggle (default `true`). Render the "Powered by Cavuno" badge unless `false`. */
2384
2384
  showCavunoBranding: boolean;
2385
+ /** @description True ONLY for the platform sandbox fixture tenant — the safe target for write probes (doctor tier 3); data is reset nightly. Every real tenant board is `false`. */
2386
+ sandbox: boolean;
2385
2387
  features: {
2386
2388
  jobAlerts: boolean;
2387
2389
  candidates: boolean;
@@ -2382,6 +2382,8 @@ interface components {
2382
2382
  primaryDomain: string | null;
2383
2383
  /** @description Whitelabel toggle (default `true`). Render the "Powered by Cavuno" badge unless `false`. */
2384
2384
  showCavunoBranding: boolean;
2385
+ /** @description True ONLY for the platform sandbox fixture tenant — the safe target for write probes (doctor tier 3); data is reset nightly. Every real tenant board is `false`. */
2386
+ sandbox: boolean;
2385
2387
  features: {
2386
2388
  jobAlerts: boolean;
2387
2389
  candidates: boolean;
@@ -1,4 +1,4 @@
1
- import { b as Schemas, L as ListEnvelope, m as RelatedSearch } from './jobs-DK5mPBgq.js';
1
+ import { b as Schemas, L as ListEnvelope, m as RelatedSearch } from './jobs-lzeoVGY6.js';
2
2
 
3
3
  type PublicBoard = Schemas['PublicBoardContext'];
4
4
  type PublicBoardFeatures = PublicBoard['features'];
@@ -1,4 +1,4 @@
1
- import { b as Schemas, L as ListEnvelope, m as RelatedSearch } from './jobs-DK5mPBgq.mjs';
1
+ import { b as Schemas, L as ListEnvelope, m as RelatedSearch } from './jobs-lzeoVGY6.mjs';
2
2
 
3
3
  type PublicBoard = Schemas['PublicBoardContext'];
4
4
  type PublicBoardFeatures = PublicBoard['features'];
package/dist/seo.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { P as PublicBoard, a as PublicBlogPostSummary, B as BlogAuthorEmbed, C as CompanyCategorySalary, b as CompanySalary, L as LocationSalaryDetail, S as SkillSalaryDetail, T as TitleSalaryDetail } from './salaries-CrJsaZe6.mjs';
2
- import { P as PublicJob } from './jobs-DK5mPBgq.mjs';
1
+ import { P as PublicBoard, a as PublicBlogPostSummary, B as BlogAuthorEmbed, C as CompanyCategorySalary, b as CompanySalary, L as LocationSalaryDetail, S as SkillSalaryDetail, T as TitleSalaryDetail } from './salaries-DpH5CxdM.mjs';
2
+ import { P as PublicJob } from './jobs-lzeoVGY6.mjs';
3
3
 
4
4
  /**
5
5
  * Google for Jobs `JobPosting` structured data on the `@cavuno/board` wire
package/dist/seo.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { P as PublicBoard, a as PublicBlogPostSummary, B as BlogAuthorEmbed, C as CompanyCategorySalary, b as CompanySalary, L as LocationSalaryDetail, S as SkillSalaryDetail, T as TitleSalaryDetail } from './salaries-CXt6Vkrp.js';
2
- import { P as PublicJob } from './jobs-DK5mPBgq.js';
1
+ import { P as PublicBoard, a as PublicBlogPostSummary, B as BlogAuthorEmbed, C as CompanyCategorySalary, b as CompanySalary, L as LocationSalaryDetail, S as SkillSalaryDetail, T as TitleSalaryDetail } from './salaries-Cy3MhZov.js';
2
+ import { P as PublicJob } from './jobs-lzeoVGY6.js';
3
3
 
4
4
  /**
5
5
  * Google for Jobs `JobPosting` structured data on the `@cavuno/board` wire
package/dist/server.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { BoardSdk } from './index.mjs';
2
- import './jobs-DK5mPBgq.mjs';
3
- import './salaries-CrJsaZe6.mjs';
2
+ import './jobs-lzeoVGY6.mjs';
3
+ import './salaries-DpH5CxdM.mjs';
4
4
 
5
5
  /**
6
6
  * Session cookie codec — pure (no framework imports, no node imports) so it
package/dist/server.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { BoardSdk } from './index.js';
2
- import './jobs-DK5mPBgq.js';
3
- import './salaries-CXt6Vkrp.js';
2
+ import './jobs-lzeoVGY6.js';
3
+ import './salaries-Cy3MhZov.js';
4
4
 
5
5
  /**
6
6
  * Session cookie codec — pure (no framework imports, no node imports) so it
@@ -1,6 +1,6 @@
1
1
  import { BoardSdk } from './index.mjs';
2
- import './jobs-DK5mPBgq.mjs';
3
- import './salaries-CrJsaZe6.mjs';
2
+ import './jobs-lzeoVGY6.mjs';
3
+ import './salaries-DpH5CxdM.mjs';
4
4
 
5
5
  /**
6
6
  * Sitemap primitives — the pure XML + bucket-filename logic behind a board
package/dist/sitemap.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { BoardSdk } from './index.js';
2
- import './jobs-DK5mPBgq.js';
3
- import './salaries-CXt6Vkrp.js';
2
+ import './jobs-lzeoVGY6.js';
3
+ import './salaries-Cy3MhZov.js';
4
4
 
5
5
  /**
6
6
  * Sitemap primitives — the pure XML + bucket-filename logic behind a board
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cavuno/board",
3
- "version": "1.29.0",
3
+ "version": "1.30.0",
4
4
  "description": "Typed isomorphic client for the Cavuno Board API",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -29,6 +29,20 @@ named in its summary and still needs the manual checks below. Use the
29
29
  rest of this skill for the behavioral checks doctor does not automate
30
30
  (auth flows, gating semantics, production build).
31
31
 
32
+ Add `--sandbox` to also run the write probes (register → login → apply →
33
+ alert-signup). They are refused unless the board the `pk_` resolves is
34
+ THE platform sandbox (`sandbox: true` in the board context — set only by
35
+ the platform's sandbox seed and stripped from any client settings write)
36
+ — never point them at a real tenant. The
37
+ email-delivery check additionally needs `RESEND_API_KEY` (a platform
38
+ operator credential) and SKIPs loudly without it.
39
+
40
+ Rate limits are real on the sandbox: the auth endpoints share a
41
+ 10-requests/min bucket and register allows 5 signups per 15 minutes per
42
+ IP, and one probe run consumes 4 requests + 1 signup. Rapid reruns will
43
+ start failing with HTTP 429 — that's the limiter working, not a broken
44
+ board; wait a few minutes.
45
+
32
46
  ## 1 — Probe the API directly (before blaming app code)
33
47
 
34
48
  Use the real env values the app reads (`PUBLIC_CAVUNO_API_URL`,
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.29.0",
2
+ "version": "1.30.0",
3
3
  "skills": [
4
4
  {
5
5
  "name": "cavuno-board-account",