@cavuno/board 4.8.0 → 4.10.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/CHANGELOG.md +48 -0
- package/dist/{_spec-DePC7a05.d.mts → _spec-BwUP7Wni.d.mts} +100 -1
- package/dist/{_spec-DePC7a05.d.ts → _spec-BwUP7Wni.d.ts} +100 -1
- package/dist/bin.mjs +174 -19
- package/dist/{blog-DbFr4VPW.d.mts → blog-C_xXYOxc.d.mts} +1 -1
- package/dist/{blog-ummlt6Uq.d.ts → blog-DC4BfQqF.d.ts} +1 -1
- package/dist/{board-zWyHbyi4.d.ts → board-CG8rDGTY.d.mts} +5 -3
- package/dist/{board-DxTMBgIf.d.mts → board-zdQ875ck.d.ts} +5 -3
- package/dist/{breadcrumbs-P4qh0zML.d.mts → breadcrumbs-BK7AqNp2.d.mts} +2 -2
- package/dist/{breadcrumbs-kbmspmfk.d.ts → breadcrumbs-qs9DC0yh.d.ts} +2 -2
- package/dist/browser/cavuno-board.global.min.js +1 -1
- package/dist/{chunk-IWWPHFK2.mjs → chunk-XEJWXRLK.mjs} +10 -0
- package/dist/doctor.d.mts +3 -1
- package/dist/doctor.d.ts +3 -1
- package/dist/doctor.js +169 -17
- package/dist/doctor.mjs +169 -17
- package/dist/filters.d.mts +2 -2
- package/dist/filters.d.ts +2 -2
- package/dist/format.d.mts +4 -4
- package/dist/format.d.ts +4 -4
- package/dist/index.d.mts +118 -24
- package/dist/index.d.ts +118 -24
- package/dist/index.js +127 -9
- package/dist/index.mjs +118 -10
- package/dist/{jobs-twt5TO9l.d.ts → jobs-B-uO7ffG.d.ts} +1 -1
- package/dist/{jobs-DTLDt57G.d.mts → jobs-BToqhRuU.d.mts} +1 -1
- package/dist/{search-f2UNlTfv.d.mts → search-CvwKD0Va.d.mts} +1 -1
- package/dist/{search-CHD6peIl.d.ts → search-Sl9-xi1z.d.ts} +1 -1
- package/dist/seo-B9b2AkSE.d.mts +12 -0
- package/dist/seo-D1Vc_0HF.d.ts +12 -0
- package/dist/seo-files.d.mts +36 -0
- package/dist/seo-files.d.ts +36 -0
- package/dist/seo-files.js +102 -0
- package/dist/seo-files.mjs +81 -0
- package/dist/seo.d.mts +6 -6
- package/dist/seo.d.ts +6 -6
- package/dist/server.d.mts +6 -5
- package/dist/server.d.ts +6 -5
- package/dist/server.mjs +1 -1
- package/dist/sitemap.d.mts +6 -5
- package/dist/sitemap.d.ts +6 -5
- package/dist/suggest.d.mts +2 -2
- package/dist/suggest.d.ts +2 -2
- package/package.json +11 -1
- package/skills/cavuno-board-account/SKILL.md +3 -1
- package/skills/cavuno-board-filters/SKILL.md +17 -3
- package/skills/cavuno-board-setup/SKILL.md +1 -0
- package/skills/cavuno-board-talent-access/SKILL.md +149 -0
- package/skills/manifest.json +8 -1
package/dist/doctor.js
CHANGED
|
@@ -137,7 +137,14 @@ function summarize(results) {
|
|
|
137
137
|
const passed = results.filter((r) => r.status === "pass").map((r) => r.id);
|
|
138
138
|
const failed = results.filter((r) => r.status === "fail").map((r) => r.id);
|
|
139
139
|
const skipped = results.filter((r) => r.status === "skip").map((r) => r.id);
|
|
140
|
-
|
|
140
|
+
const warned = results.filter((r) => r.status === "warn").map((r) => r.id);
|
|
141
|
+
return {
|
|
142
|
+
exitCode: failed.length > 0 ? 1 : 0,
|
|
143
|
+
passed,
|
|
144
|
+
failed,
|
|
145
|
+
skipped,
|
|
146
|
+
warned
|
|
147
|
+
};
|
|
141
148
|
}
|
|
142
149
|
|
|
143
150
|
// src/doctor/cookie-conformance.ts
|
|
@@ -312,7 +319,7 @@ function checkCookieCodecConformance(projectRoot) {
|
|
|
312
319
|
}
|
|
313
320
|
|
|
314
321
|
// src/doctor/probe.ts
|
|
315
|
-
var FETCH_TIMEOUT_MS =
|
|
322
|
+
var FETCH_TIMEOUT_MS = 6e4;
|
|
316
323
|
async function probe(fetchImpl, url) {
|
|
317
324
|
try {
|
|
318
325
|
const response = await fetchImpl(url, {
|
|
@@ -338,7 +345,11 @@ var READ = {
|
|
|
338
345
|
jobs: record("read.jobs", 2),
|
|
339
346
|
jsonld: record("read.jsonld", 2),
|
|
340
347
|
sitemap: record("read.sitemap", 2),
|
|
341
|
-
robots: record("read.robots", 2)
|
|
348
|
+
robots: record("read.robots", 2),
|
|
349
|
+
adsTxt: record("read.adsTxt", 2),
|
|
350
|
+
indexNow: record("read.indexNow", 2),
|
|
351
|
+
googleVerification: record("read.googleVerification", 2),
|
|
352
|
+
oauthCallback: record("read.oauthCallback", 2)
|
|
342
353
|
};
|
|
343
354
|
function skipReadProbes(reason) {
|
|
344
355
|
return Object.values(READ).map((make) => make("skip", reason));
|
|
@@ -394,7 +405,11 @@ async function probeSitemap(fetchImpl, base) {
|
|
|
394
405
|
}
|
|
395
406
|
let urls = doc.urls;
|
|
396
407
|
if (doc.kind === "index") {
|
|
397
|
-
const
|
|
408
|
+
const childUrl = onFrontend(base, urls[0]);
|
|
409
|
+
let child = await probe(fetchImpl, childUrl);
|
|
410
|
+
if (!child.ok && (child.status === 503 || child.status === 0)) {
|
|
411
|
+
child = await probe(fetchImpl, childUrl);
|
|
412
|
+
}
|
|
398
413
|
const childDoc = child.ok ? parseSitemap(child.body) : null;
|
|
399
414
|
if (!childDoc || childDoc.urls.length === 0) {
|
|
400
415
|
return READ.sitemap(
|
|
@@ -408,14 +423,109 @@ async function probeSitemap(fetchImpl, base) {
|
|
|
408
423
|
const sample = await probe(fetchImpl, sampleUrl);
|
|
409
424
|
return sample.ok ? READ.sitemap("pass", `${urls.length} entries; sample page resolves`) : READ.sitemap("fail", `sitemap page ${sampleUrl} \u2192 HTTP ${sample.status}`);
|
|
410
425
|
}
|
|
411
|
-
|
|
426
|
+
function scoreRobots(robots, seo) {
|
|
427
|
+
if (!robots.ok) {
|
|
428
|
+
return READ.robots("fail", `robots.txt HTTP ${robots.status}`);
|
|
429
|
+
}
|
|
430
|
+
const missing = [];
|
|
431
|
+
if (!/user-agent\s*:/i.test(robots.body)) missing.push("User-agent");
|
|
432
|
+
if (!robots.body.includes("Disallow: /go/")) missing.push("Disallow: /go/");
|
|
433
|
+
if (seo) {
|
|
434
|
+
const canonicalBase = seo.canonicalBase.replace(/\/+$/, "");
|
|
435
|
+
const sitemapLine = `Sitemap: ${canonicalBase}/sitemap.xml`;
|
|
436
|
+
if (!robots.body.includes(sitemapLine)) missing.push(sitemapLine);
|
|
437
|
+
}
|
|
438
|
+
if (missing.includes("User-agent")) {
|
|
439
|
+
return READ.robots(
|
|
440
|
+
"fail",
|
|
441
|
+
"robots.txt returned 200 but has no User-agent directive"
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
return missing.length === 0 ? READ.robots("pass", "present") : READ.robots(
|
|
445
|
+
"warn",
|
|
446
|
+
`robots.txt is missing ${missing.join(", ")} (hosted boards emit it)`
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
function configuredText(value) {
|
|
450
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
451
|
+
}
|
|
452
|
+
function scoreOptionalTextFile(options) {
|
|
453
|
+
const { make, label, path, seo, probed } = options;
|
|
454
|
+
if (seo == null) {
|
|
455
|
+
return make("skip", "board seo not fetched");
|
|
456
|
+
}
|
|
457
|
+
const expected = configuredText(options.configured);
|
|
458
|
+
if (expected !== null) {
|
|
459
|
+
if (!probed.ok) {
|
|
460
|
+
return make("fail", `${path} HTTP ${probed.status}`);
|
|
461
|
+
}
|
|
462
|
+
if (probed.body.trimEnd() === expected.trimEnd()) {
|
|
463
|
+
return make("pass", `${label} matches dashboard`);
|
|
464
|
+
}
|
|
465
|
+
return make("fail", `${path} body does not match dashboard ${label}`);
|
|
466
|
+
}
|
|
467
|
+
if (probed.status === 404) {
|
|
468
|
+
return make("pass", "unconfigured; 404 as expected");
|
|
469
|
+
}
|
|
470
|
+
if (probed.status === 200) {
|
|
471
|
+
return make(
|
|
472
|
+
"warn",
|
|
473
|
+
`${label} served but not configured in dashboard \u2014 clone leftover?`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
return make("fail", `${path} HTTP ${probed.status}`);
|
|
477
|
+
}
|
|
478
|
+
function escapeRegExp(value) {
|
|
479
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
480
|
+
}
|
|
481
|
+
function hasGoogleSiteVerificationMeta(html, token) {
|
|
482
|
+
const escaped = escapeRegExp(token);
|
|
483
|
+
const quoted = `["']${escaped}["']`;
|
|
484
|
+
const name = `name\\s*=\\s*["']google-site-verification["']`;
|
|
485
|
+
const content = `content\\s*=\\s*${quoted}`;
|
|
486
|
+
return new RegExp(`<meta\\b[^>]*\\b${name}[^>]*\\b${content}`, "i").test(html) || new RegExp(`<meta\\b[^>]*\\b${content}[^>]*\\b${name}`, "i").test(html);
|
|
487
|
+
}
|
|
488
|
+
function scoreGoogleVerification(homeHtml, seo) {
|
|
489
|
+
if (seo == null) {
|
|
490
|
+
return READ.googleVerification("skip", "board seo not fetched");
|
|
491
|
+
}
|
|
492
|
+
const token = configuredText(seo.googleSiteVerification);
|
|
493
|
+
if (token === null) {
|
|
494
|
+
return READ.googleVerification("skip", "no token configured");
|
|
495
|
+
}
|
|
496
|
+
return hasGoogleSiteVerificationMeta(homeHtml, token) ? READ.googleVerification("pass", "google-site-verification meta present") : READ.googleVerification(
|
|
497
|
+
"fail",
|
|
498
|
+
"home HTML is missing the google-site-verification meta tag"
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
var OAUTH_PROVIDERS = ["google", "linkedin"];
|
|
502
|
+
function scoreOAuthCallback(probed) {
|
|
503
|
+
const broken = OAUTH_PROVIDERS.filter(
|
|
504
|
+
(provider) => !(probed[provider].status === 400 && /missing code or state/i.test(probed[provider].body))
|
|
505
|
+
);
|
|
506
|
+
return broken.length === 0 ? READ.oauthCallback("pass", "callback paths reach the hosted app") : READ.oauthCallback(
|
|
507
|
+
"warn",
|
|
508
|
+
`${broken.map((p) => `/api/board-auth/oauth/${p}/callback \u2192 HTTP ${probed[p].status}`).join(", ")} \u2014 only matters if you registered this domain's callback with the provider: forward the path to cavuno.com, or register https://cavuno.com/api/board-auth/oauth/{provider}/callback instead`
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
async function runReadProbes(fetchImpl, frontendUrl, seo) {
|
|
412
512
|
const base = frontendUrl.replace(/\/$/, "");
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
513
|
+
const home = await probe(fetchImpl, base);
|
|
514
|
+
const jobsAndJsonLd = await probeJobsAndJsonLd(fetchImpl, base);
|
|
515
|
+
const sitemap = await probeSitemap(fetchImpl, base);
|
|
516
|
+
const robots = await probe(fetchImpl, `${base}/robots.txt`);
|
|
517
|
+
const adsTxt = await probe(fetchImpl, `${base}/ads.txt`);
|
|
518
|
+
const indexNow = await probe(fetchImpl, `${base}/indexnow-key.txt`);
|
|
519
|
+
const oauthCallback = {
|
|
520
|
+
google: await probe(
|
|
521
|
+
fetchImpl,
|
|
522
|
+
`${base}/api/board-auth/oauth/google/callback`
|
|
523
|
+
),
|
|
524
|
+
linkedin: await probe(
|
|
525
|
+
fetchImpl,
|
|
526
|
+
`${base}/api/board-auth/oauth/linkedin/callback`
|
|
527
|
+
)
|
|
528
|
+
};
|
|
419
529
|
return [
|
|
420
530
|
home.ok && /<(html|body|div|main)[\s>]/i.test(home.body) ? READ.home("pass", "home renders") : READ.home(
|
|
421
531
|
"fail",
|
|
@@ -423,10 +533,25 @@ async function runReadProbes(fetchImpl, frontendUrl) {
|
|
|
423
533
|
),
|
|
424
534
|
...jobsAndJsonLd,
|
|
425
535
|
sitemap,
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
536
|
+
scoreRobots(robots, seo),
|
|
537
|
+
scoreOptionalTextFile({
|
|
538
|
+
make: READ.adsTxt,
|
|
539
|
+
label: "ads.txt",
|
|
540
|
+
path: "/ads.txt",
|
|
541
|
+
seo,
|
|
542
|
+
configured: seo?.adsTxt,
|
|
543
|
+
probed: adsTxt
|
|
544
|
+
}),
|
|
545
|
+
scoreOptionalTextFile({
|
|
546
|
+
make: READ.indexNow,
|
|
547
|
+
label: "indexnow-key.txt",
|
|
548
|
+
path: "/indexnow-key.txt",
|
|
549
|
+
seo,
|
|
550
|
+
configured: seo?.indexNowKey,
|
|
551
|
+
probed: indexNow
|
|
552
|
+
}),
|
|
553
|
+
scoreGoogleVerification(home.body, seo),
|
|
554
|
+
scoreOAuthCallback(oauthCallback)
|
|
430
555
|
];
|
|
431
556
|
}
|
|
432
557
|
|
|
@@ -482,6 +607,28 @@ async function checkBoardResolves(fetchImpl, env) {
|
|
|
482
607
|
}
|
|
483
608
|
return STATIC_BOARD("pass", "publishable key resolves the board");
|
|
484
609
|
}
|
|
610
|
+
async function fetchBoardSeo(fetchImpl, env) {
|
|
611
|
+
const result = await probe(
|
|
612
|
+
fetchImpl,
|
|
613
|
+
`${apiBase(env.apiUrl)}/v1/boards/${encodeURIComponent(env.boardKey)}/seo`
|
|
614
|
+
);
|
|
615
|
+
if (!result.ok) return null;
|
|
616
|
+
try {
|
|
617
|
+
const parsed = JSON.parse(result.body);
|
|
618
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
619
|
+
const rec = parsed;
|
|
620
|
+
if (typeof rec.canonicalBase !== "string") return null;
|
|
621
|
+
const asNullableString = (value) => value === null || typeof value === "string" ? value : null;
|
|
622
|
+
return {
|
|
623
|
+
canonicalBase: rec.canonicalBase,
|
|
624
|
+
adsTxt: asNullableString(rec.adsTxt),
|
|
625
|
+
indexNowKey: asNullableString(rec.indexNowKey),
|
|
626
|
+
googleSiteVerification: asNullableString(rec.googleSiteVerification)
|
|
627
|
+
};
|
|
628
|
+
} catch {
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
485
632
|
var SKILL_ROOTS = [
|
|
486
633
|
".claude/skills",
|
|
487
634
|
".agents/skills",
|
|
@@ -543,8 +690,13 @@ async function runDoctor(options) {
|
|
|
543
690
|
} else {
|
|
544
691
|
results.push(STATIC_API("skip", "PUBLIC_CAVUNO_API_URL not set"));
|
|
545
692
|
}
|
|
693
|
+
let seo = null;
|
|
546
694
|
if (envOk) {
|
|
547
|
-
|
|
695
|
+
const boardResult = await checkBoardResolves(fetchImpl, env);
|
|
696
|
+
results.push(boardResult);
|
|
697
|
+
if (boardResult.status === "pass") {
|
|
698
|
+
seo = await fetchBoardSeo(fetchImpl, env);
|
|
699
|
+
}
|
|
548
700
|
} else {
|
|
549
701
|
results.push(STATIC_BOARD("skip", "env checks failed \u2014 fix them first"));
|
|
550
702
|
}
|
|
@@ -553,7 +705,7 @@ async function runDoctor(options) {
|
|
|
553
705
|
...checkCookieCodecConformance(options.projectRoot ?? process.cwd())
|
|
554
706
|
);
|
|
555
707
|
results.push(
|
|
556
|
-
...options.frontendUrl ? await runReadProbes(fetchImpl, options.frontendUrl) : skipReadProbes("no --frontend <url> given")
|
|
708
|
+
...options.frontendUrl ? await runReadProbes(fetchImpl, options.frontendUrl, seo) : skipReadProbes("no --frontend <url> given")
|
|
557
709
|
);
|
|
558
710
|
return { results, summary: summarize(results) };
|
|
559
711
|
}
|
package/dist/doctor.mjs
CHANGED
|
@@ -107,7 +107,14 @@ function summarize(results) {
|
|
|
107
107
|
const passed = results.filter((r) => r.status === "pass").map((r) => r.id);
|
|
108
108
|
const failed = results.filter((r) => r.status === "fail").map((r) => r.id);
|
|
109
109
|
const skipped = results.filter((r) => r.status === "skip").map((r) => r.id);
|
|
110
|
-
|
|
110
|
+
const warned = results.filter((r) => r.status === "warn").map((r) => r.id);
|
|
111
|
+
return {
|
|
112
|
+
exitCode: failed.length > 0 ? 1 : 0,
|
|
113
|
+
passed,
|
|
114
|
+
failed,
|
|
115
|
+
skipped,
|
|
116
|
+
warned
|
|
117
|
+
};
|
|
111
118
|
}
|
|
112
119
|
|
|
113
120
|
// src/doctor/cookie-conformance.ts
|
|
@@ -282,7 +289,7 @@ function checkCookieCodecConformance(projectRoot) {
|
|
|
282
289
|
}
|
|
283
290
|
|
|
284
291
|
// src/doctor/probe.ts
|
|
285
|
-
var FETCH_TIMEOUT_MS =
|
|
292
|
+
var FETCH_TIMEOUT_MS = 6e4;
|
|
286
293
|
async function probe(fetchImpl, url) {
|
|
287
294
|
try {
|
|
288
295
|
const response = await fetchImpl(url, {
|
|
@@ -308,7 +315,11 @@ var READ = {
|
|
|
308
315
|
jobs: record("read.jobs", 2),
|
|
309
316
|
jsonld: record("read.jsonld", 2),
|
|
310
317
|
sitemap: record("read.sitemap", 2),
|
|
311
|
-
robots: record("read.robots", 2)
|
|
318
|
+
robots: record("read.robots", 2),
|
|
319
|
+
adsTxt: record("read.adsTxt", 2),
|
|
320
|
+
indexNow: record("read.indexNow", 2),
|
|
321
|
+
googleVerification: record("read.googleVerification", 2),
|
|
322
|
+
oauthCallback: record("read.oauthCallback", 2)
|
|
312
323
|
};
|
|
313
324
|
function skipReadProbes(reason) {
|
|
314
325
|
return Object.values(READ).map((make) => make("skip", reason));
|
|
@@ -364,7 +375,11 @@ async function probeSitemap(fetchImpl, base) {
|
|
|
364
375
|
}
|
|
365
376
|
let urls = doc.urls;
|
|
366
377
|
if (doc.kind === "index") {
|
|
367
|
-
const
|
|
378
|
+
const childUrl = onFrontend(base, urls[0]);
|
|
379
|
+
let child = await probe(fetchImpl, childUrl);
|
|
380
|
+
if (!child.ok && (child.status === 503 || child.status === 0)) {
|
|
381
|
+
child = await probe(fetchImpl, childUrl);
|
|
382
|
+
}
|
|
368
383
|
const childDoc = child.ok ? parseSitemap(child.body) : null;
|
|
369
384
|
if (!childDoc || childDoc.urls.length === 0) {
|
|
370
385
|
return READ.sitemap(
|
|
@@ -378,14 +393,109 @@ async function probeSitemap(fetchImpl, base) {
|
|
|
378
393
|
const sample = await probe(fetchImpl, sampleUrl);
|
|
379
394
|
return sample.ok ? READ.sitemap("pass", `${urls.length} entries; sample page resolves`) : READ.sitemap("fail", `sitemap page ${sampleUrl} \u2192 HTTP ${sample.status}`);
|
|
380
395
|
}
|
|
381
|
-
|
|
396
|
+
function scoreRobots(robots, seo) {
|
|
397
|
+
if (!robots.ok) {
|
|
398
|
+
return READ.robots("fail", `robots.txt HTTP ${robots.status}`);
|
|
399
|
+
}
|
|
400
|
+
const missing = [];
|
|
401
|
+
if (!/user-agent\s*:/i.test(robots.body)) missing.push("User-agent");
|
|
402
|
+
if (!robots.body.includes("Disallow: /go/")) missing.push("Disallow: /go/");
|
|
403
|
+
if (seo) {
|
|
404
|
+
const canonicalBase = seo.canonicalBase.replace(/\/+$/, "");
|
|
405
|
+
const sitemapLine = `Sitemap: ${canonicalBase}/sitemap.xml`;
|
|
406
|
+
if (!robots.body.includes(sitemapLine)) missing.push(sitemapLine);
|
|
407
|
+
}
|
|
408
|
+
if (missing.includes("User-agent")) {
|
|
409
|
+
return READ.robots(
|
|
410
|
+
"fail",
|
|
411
|
+
"robots.txt returned 200 but has no User-agent directive"
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
return missing.length === 0 ? READ.robots("pass", "present") : READ.robots(
|
|
415
|
+
"warn",
|
|
416
|
+
`robots.txt is missing ${missing.join(", ")} (hosted boards emit it)`
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
function configuredText(value) {
|
|
420
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
421
|
+
}
|
|
422
|
+
function scoreOptionalTextFile(options) {
|
|
423
|
+
const { make, label, path, seo, probed } = options;
|
|
424
|
+
if (seo == null) {
|
|
425
|
+
return make("skip", "board seo not fetched");
|
|
426
|
+
}
|
|
427
|
+
const expected = configuredText(options.configured);
|
|
428
|
+
if (expected !== null) {
|
|
429
|
+
if (!probed.ok) {
|
|
430
|
+
return make("fail", `${path} HTTP ${probed.status}`);
|
|
431
|
+
}
|
|
432
|
+
if (probed.body.trimEnd() === expected.trimEnd()) {
|
|
433
|
+
return make("pass", `${label} matches dashboard`);
|
|
434
|
+
}
|
|
435
|
+
return make("fail", `${path} body does not match dashboard ${label}`);
|
|
436
|
+
}
|
|
437
|
+
if (probed.status === 404) {
|
|
438
|
+
return make("pass", "unconfigured; 404 as expected");
|
|
439
|
+
}
|
|
440
|
+
if (probed.status === 200) {
|
|
441
|
+
return make(
|
|
442
|
+
"warn",
|
|
443
|
+
`${label} served but not configured in dashboard \u2014 clone leftover?`
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
return make("fail", `${path} HTTP ${probed.status}`);
|
|
447
|
+
}
|
|
448
|
+
function escapeRegExp(value) {
|
|
449
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
450
|
+
}
|
|
451
|
+
function hasGoogleSiteVerificationMeta(html, token) {
|
|
452
|
+
const escaped = escapeRegExp(token);
|
|
453
|
+
const quoted = `["']${escaped}["']`;
|
|
454
|
+
const name = `name\\s*=\\s*["']google-site-verification["']`;
|
|
455
|
+
const content = `content\\s*=\\s*${quoted}`;
|
|
456
|
+
return new RegExp(`<meta\\b[^>]*\\b${name}[^>]*\\b${content}`, "i").test(html) || new RegExp(`<meta\\b[^>]*\\b${content}[^>]*\\b${name}`, "i").test(html);
|
|
457
|
+
}
|
|
458
|
+
function scoreGoogleVerification(homeHtml, seo) {
|
|
459
|
+
if (seo == null) {
|
|
460
|
+
return READ.googleVerification("skip", "board seo not fetched");
|
|
461
|
+
}
|
|
462
|
+
const token = configuredText(seo.googleSiteVerification);
|
|
463
|
+
if (token === null) {
|
|
464
|
+
return READ.googleVerification("skip", "no token configured");
|
|
465
|
+
}
|
|
466
|
+
return hasGoogleSiteVerificationMeta(homeHtml, token) ? READ.googleVerification("pass", "google-site-verification meta present") : READ.googleVerification(
|
|
467
|
+
"fail",
|
|
468
|
+
"home HTML is missing the google-site-verification meta tag"
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
var OAUTH_PROVIDERS = ["google", "linkedin"];
|
|
472
|
+
function scoreOAuthCallback(probed) {
|
|
473
|
+
const broken = OAUTH_PROVIDERS.filter(
|
|
474
|
+
(provider) => !(probed[provider].status === 400 && /missing code or state/i.test(probed[provider].body))
|
|
475
|
+
);
|
|
476
|
+
return broken.length === 0 ? READ.oauthCallback("pass", "callback paths reach the hosted app") : READ.oauthCallback(
|
|
477
|
+
"warn",
|
|
478
|
+
`${broken.map((p) => `/api/board-auth/oauth/${p}/callback \u2192 HTTP ${probed[p].status}`).join(", ")} \u2014 only matters if you registered this domain's callback with the provider: forward the path to cavuno.com, or register https://cavuno.com/api/board-auth/oauth/{provider}/callback instead`
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
async function runReadProbes(fetchImpl, frontendUrl, seo) {
|
|
382
482
|
const base = frontendUrl.replace(/\/$/, "");
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
483
|
+
const home = await probe(fetchImpl, base);
|
|
484
|
+
const jobsAndJsonLd = await probeJobsAndJsonLd(fetchImpl, base);
|
|
485
|
+
const sitemap = await probeSitemap(fetchImpl, base);
|
|
486
|
+
const robots = await probe(fetchImpl, `${base}/robots.txt`);
|
|
487
|
+
const adsTxt = await probe(fetchImpl, `${base}/ads.txt`);
|
|
488
|
+
const indexNow = await probe(fetchImpl, `${base}/indexnow-key.txt`);
|
|
489
|
+
const oauthCallback = {
|
|
490
|
+
google: await probe(
|
|
491
|
+
fetchImpl,
|
|
492
|
+
`${base}/api/board-auth/oauth/google/callback`
|
|
493
|
+
),
|
|
494
|
+
linkedin: await probe(
|
|
495
|
+
fetchImpl,
|
|
496
|
+
`${base}/api/board-auth/oauth/linkedin/callback`
|
|
497
|
+
)
|
|
498
|
+
};
|
|
389
499
|
return [
|
|
390
500
|
home.ok && /<(html|body|div|main)[\s>]/i.test(home.body) ? READ.home("pass", "home renders") : READ.home(
|
|
391
501
|
"fail",
|
|
@@ -393,10 +503,25 @@ async function runReadProbes(fetchImpl, frontendUrl) {
|
|
|
393
503
|
),
|
|
394
504
|
...jobsAndJsonLd,
|
|
395
505
|
sitemap,
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
506
|
+
scoreRobots(robots, seo),
|
|
507
|
+
scoreOptionalTextFile({
|
|
508
|
+
make: READ.adsTxt,
|
|
509
|
+
label: "ads.txt",
|
|
510
|
+
path: "/ads.txt",
|
|
511
|
+
seo,
|
|
512
|
+
configured: seo?.adsTxt,
|
|
513
|
+
probed: adsTxt
|
|
514
|
+
}),
|
|
515
|
+
scoreOptionalTextFile({
|
|
516
|
+
make: READ.indexNow,
|
|
517
|
+
label: "indexnow-key.txt",
|
|
518
|
+
path: "/indexnow-key.txt",
|
|
519
|
+
seo,
|
|
520
|
+
configured: seo?.indexNowKey,
|
|
521
|
+
probed: indexNow
|
|
522
|
+
}),
|
|
523
|
+
scoreGoogleVerification(home.body, seo),
|
|
524
|
+
scoreOAuthCallback(oauthCallback)
|
|
400
525
|
];
|
|
401
526
|
}
|
|
402
527
|
|
|
@@ -452,6 +577,28 @@ async function checkBoardResolves(fetchImpl, env) {
|
|
|
452
577
|
}
|
|
453
578
|
return STATIC_BOARD("pass", "publishable key resolves the board");
|
|
454
579
|
}
|
|
580
|
+
async function fetchBoardSeo(fetchImpl, env) {
|
|
581
|
+
const result = await probe(
|
|
582
|
+
fetchImpl,
|
|
583
|
+
`${apiBase(env.apiUrl)}/v1/boards/${encodeURIComponent(env.boardKey)}/seo`
|
|
584
|
+
);
|
|
585
|
+
if (!result.ok) return null;
|
|
586
|
+
try {
|
|
587
|
+
const parsed = JSON.parse(result.body);
|
|
588
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
589
|
+
const rec = parsed;
|
|
590
|
+
if (typeof rec.canonicalBase !== "string") return null;
|
|
591
|
+
const asNullableString = (value) => value === null || typeof value === "string" ? value : null;
|
|
592
|
+
return {
|
|
593
|
+
canonicalBase: rec.canonicalBase,
|
|
594
|
+
adsTxt: asNullableString(rec.adsTxt),
|
|
595
|
+
indexNowKey: asNullableString(rec.indexNowKey),
|
|
596
|
+
googleSiteVerification: asNullableString(rec.googleSiteVerification)
|
|
597
|
+
};
|
|
598
|
+
} catch {
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
455
602
|
var SKILL_ROOTS = [
|
|
456
603
|
".claude/skills",
|
|
457
604
|
".agents/skills",
|
|
@@ -513,8 +660,13 @@ async function runDoctor(options) {
|
|
|
513
660
|
} else {
|
|
514
661
|
results.push(STATIC_API("skip", "PUBLIC_CAVUNO_API_URL not set"));
|
|
515
662
|
}
|
|
663
|
+
let seo = null;
|
|
516
664
|
if (envOk) {
|
|
517
|
-
|
|
665
|
+
const boardResult = await checkBoardResolves(fetchImpl, env);
|
|
666
|
+
results.push(boardResult);
|
|
667
|
+
if (boardResult.status === "pass") {
|
|
668
|
+
seo = await fetchBoardSeo(fetchImpl, env);
|
|
669
|
+
}
|
|
518
670
|
} else {
|
|
519
671
|
results.push(STATIC_BOARD("skip", "env checks failed \u2014 fix them first"));
|
|
520
672
|
}
|
|
@@ -523,7 +675,7 @@ async function runDoctor(options) {
|
|
|
523
675
|
...checkCookieCodecConformance(options.projectRoot ?? process.cwd())
|
|
524
676
|
);
|
|
525
677
|
results.push(
|
|
526
|
-
...options.frontendUrl ? await runReadProbes(fetchImpl, options.frontendUrl) : skipReadProbes("no --frontend <url> given")
|
|
678
|
+
...options.frontendUrl ? await runReadProbes(fetchImpl, options.frontendUrl, seo) : skipReadProbes("no --frontend <url> given")
|
|
527
679
|
);
|
|
528
680
|
return { results, summary: summarize(results) };
|
|
529
681
|
}
|
package/dist/filters.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { J as JobSort, E as EmploymentType, R as RemoteOption, S as Seniority } from './jobs-
|
|
2
|
-
import './_spec-
|
|
1
|
+
import { J as JobSort, E as EmploymentType, R as RemoteOption, S as Seniority } from './jobs-BToqhRuU.mjs';
|
|
2
|
+
import './_spec-BwUP7Wni.mjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* `@cavuno/board/filters` — the canonical listing-filter vocabulary and URL
|
package/dist/filters.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { J as JobSort, E as EmploymentType, R as RemoteOption, S as Seniority } from './jobs-
|
|
2
|
-
import './_spec-
|
|
1
|
+
import { J as JobSort, E as EmploymentType, R as RemoteOption, S as Seniority } from './jobs-B-uO7ffG.js';
|
|
2
|
+
import './_spec-BwUP7Wni.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* `@cavuno/board/filters` — the canonical listing-filter vocabulary and URL
|
package/dist/format.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export { C as CurrencyDisplay, F as FormattedSalaryRange, J as JobBreadcrumb, N as NumberNotation, S as SalaryTimeframeInput, a as SalaryTimeframeValue, b as buildJobBreadcrumbs, f as formatSalaryRange, c as formatSalaryStat, d as formatSalaryStatRange, n as normalizeWebsiteUrl } from './breadcrumbs-
|
|
2
|
-
import { C as CustomFieldDefinition } from './board-
|
|
3
|
-
import { C as CustomFieldValues } from './jobs-
|
|
4
|
-
import './_spec-
|
|
1
|
+
export { C as CurrencyDisplay, F as FormattedSalaryRange, J as JobBreadcrumb, N as NumberNotation, S as SalaryTimeframeInput, a as SalaryTimeframeValue, b as buildJobBreadcrumbs, f as formatSalaryRange, c as formatSalaryStat, d as formatSalaryStatRange, n as normalizeWebsiteUrl } from './breadcrumbs-BK7AqNp2.mjs';
|
|
2
|
+
import { C as CustomFieldDefinition } from './board-CG8rDGTY.mjs';
|
|
3
|
+
import { C as CustomFieldValues } from './jobs-BToqhRuU.mjs';
|
|
4
|
+
import './_spec-BwUP7Wni.mjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Date display helpers in the board language (required leading parameter,
|
package/dist/format.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export { C as CurrencyDisplay, F as FormattedSalaryRange, J as JobBreadcrumb, N as NumberNotation, S as SalaryTimeframeInput, a as SalaryTimeframeValue, b as buildJobBreadcrumbs, f as formatSalaryRange, c as formatSalaryStat, d as formatSalaryStatRange, n as normalizeWebsiteUrl } from './breadcrumbs-
|
|
2
|
-
import { C as CustomFieldDefinition } from './board-
|
|
3
|
-
import { C as CustomFieldValues } from './jobs-
|
|
4
|
-
import './_spec-
|
|
1
|
+
export { C as CurrencyDisplay, F as FormattedSalaryRange, J as JobBreadcrumb, N as NumberNotation, S as SalaryTimeframeInput, a as SalaryTimeframeValue, b as buildJobBreadcrumbs, f as formatSalaryRange, c as formatSalaryStat, d as formatSalaryStatRange, n as normalizeWebsiteUrl } from './breadcrumbs-qs9DC0yh.js';
|
|
2
|
+
import { C as CustomFieldDefinition } from './board-zdQ875ck.js';
|
|
3
|
+
import { C as CustomFieldValues } from './jobs-B-uO7ffG.js';
|
|
4
|
+
import './_spec-BwUP7Wni.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Date display helpers in the board language (required leading parameter,
|