@cavuno/board 4.7.0 → 4.9.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 +35 -0
- package/dist/{_spec-B2ZrXuea.d.mts → _spec-B9Ah8dNs.d.mts} +78 -3
- package/dist/{_spec-B2ZrXuea.d.ts → _spec-B9Ah8dNs.d.ts} +78 -3
- package/dist/bin.mjs +174 -19
- package/dist/{blog-B125P3dS.d.mts → blog-BmqWfLZi.d.mts} +1 -1
- package/dist/{blog-BmEG_fhe.d.ts → blog-bh07fZ8Q.d.ts} +1 -1
- package/dist/{board-QTX75RxT.d.mts → board-B4jLqmxn.d.mts} +5 -3
- package/dist/{board-6woxZtzD.d.ts → board-De8nLbk7.d.ts} +5 -3
- package/dist/{breadcrumbs-BVeDYPCw.d.ts → breadcrumbs-B1HVufC7.d.ts} +2 -2
- package/dist/{breadcrumbs-CP42mzYb.d.mts → breadcrumbs-BVQL2dNY.d.mts} +2 -2
- package/dist/browser/cavuno-board.global.min.js +1 -1
- package/dist/{chunk-ZCEAOJ6Y.mjs → chunk-T46JASKG.mjs} +4 -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 +82 -25
- package/dist/index.d.ts +82 -25
- package/dist/index.js +48 -3
- package/dist/index.mjs +45 -4
- package/dist/{jobs-DrdhICLO.d.mts → jobs-B7q3BgIK.d.mts} +1 -1
- package/dist/{jobs-BH1CgV8J.d.ts → jobs-Iv6Wkvvf.d.ts} +1 -1
- package/dist/{search-CCsijbzU.d.ts → search-CbDZwdyl.d.ts} +1 -1
- package/dist/{search-B9T6rAhd.d.mts → search-DC0jatQN.d.mts} +1 -1
- package/dist/seo-BDFUuvXM.d.ts +12 -0
- package/dist/seo-HCE7aCkh.d.mts +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 +6 -1
- package/skills/cavuno-board-filters/SKILL.md +17 -3
- package/skills/manifest.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,41 @@
|
|
|
3
3
|
This changelog records changes that affect Board API compatibility, exported
|
|
4
4
|
types, runtime behavior, or supported integration patterns.
|
|
5
5
|
|
|
6
|
+
## 4.9.0 — 2026-08-27
|
|
7
|
+
|
|
8
|
+
- **`@cavuno/board/seo-files`**: new dependency-free subpath with
|
|
9
|
+
`robotsResponse`, `adsTxtResponse`, `indexNowResponse`, and
|
|
10
|
+
`googleSiteVerificationMeta`. Headless frontends serve `robots.txt`,
|
|
11
|
+
`ads.txt`, and `indexnow-key.txt` byte-identically to hosted boards from
|
|
12
|
+
`board.seo()`; the robots body now includes `Disallow: /go/` and
|
|
13
|
+
`Disallow: /t/` like hosted boards.
|
|
14
|
+
- **Doctor**: `--frontend` probes gained `read.adsTxt`, `read.indexNow`,
|
|
15
|
+
`read.googleVerification`, and `read.oauthCallback`, and `read.robots` now
|
|
16
|
+
checks the `Sitemap:` line and `/go/` disallow. Results gained an additive
|
|
17
|
+
`warn` status (`DoctorSummary.warned`) that never fails the run; a file that
|
|
18
|
+
is served while unconfigured in the dashboard, or a missing OAuth callback
|
|
19
|
+
forward, warns instead of failing. Read probes run serially and retry a
|
|
20
|
+
503 child sitemap once.
|
|
21
|
+
- **`board.context()`**: exposes `ads` (`enabled`, `clientId`) and
|
|
22
|
+
`jobForm` visibility, typed as `PublicBoardAds` and `PublicBoardJobForm`.
|
|
23
|
+
Employer companies expose `deletionEnabled`.
|
|
24
|
+
- **Taxonomy lists** accept `sort: 'jobCount'` to order collections by live
|
|
25
|
+
job count.
|
|
26
|
+
- **Generated types** cover the `candidate.profile.updated` webhook event
|
|
27
|
+
and the operator candidate sync resources (`CandidateSyncSnapshot`,
|
|
28
|
+
`CandidateReconcileResult`, and related bodies).
|
|
29
|
+
|
|
30
|
+
## 4.8.0 — 2026-08-23
|
|
31
|
+
|
|
32
|
+
- **Country-gated Apply flows**: compatible starters can use
|
|
33
|
+
`board.jobs.createApplyIntent` for opaque external-apply redirects and
|
|
34
|
+
`board.jobs.prepareApplyApproval` before native Apply. The browser completes
|
|
35
|
+
country approval directly against the Cavuno Apply gateway, then returns the
|
|
36
|
+
opaque receipt through the existing `board.jobs.apply` call. New job payloads
|
|
37
|
+
expose `applyAction`, and native Apply accepts `approvalReceipt` plus the
|
|
38
|
+
matching server-owned `approvalSessionKey`. These additions keep destination
|
|
39
|
+
URLs and client-supplied country or IP values out of the trusted decision.
|
|
40
|
+
|
|
6
41
|
## 4.7.0 — 2026-08-23
|
|
7
42
|
|
|
8
43
|
- **`board.me.companies.recommendedTalent.list`**: candidates who fit one of
|
|
@@ -127,11 +127,40 @@ interface components {
|
|
|
127
127
|
companyName: string | null;
|
|
128
128
|
} | null;
|
|
129
129
|
};
|
|
130
|
+
ApplyApprovalPlan: {
|
|
131
|
+
/** @enum {string} */
|
|
132
|
+
object: "apply_approval_plan";
|
|
133
|
+
/** @enum {string} */
|
|
134
|
+
kind: "not_required";
|
|
135
|
+
} | {
|
|
136
|
+
/** @enum {string} */
|
|
137
|
+
object: "apply_approval_plan";
|
|
138
|
+
/** @enum {string} */
|
|
139
|
+
kind: "approval_required";
|
|
140
|
+
/** Format: uri */
|
|
141
|
+
approvalUrl: string;
|
|
142
|
+
/** Format: date-time */
|
|
143
|
+
expiresAt: string;
|
|
144
|
+
};
|
|
130
145
|
ApplyBody: {
|
|
131
146
|
name?: string;
|
|
132
147
|
/** Format: email */
|
|
133
148
|
email?: string;
|
|
134
149
|
coverNote?: string;
|
|
150
|
+
/** @description Opaque native country-approval receipt returned by the browser-direct Apply gateway. */
|
|
151
|
+
approvalReceipt?: string;
|
|
152
|
+
/** @description Server-owned board session key that prepared the country approval. */
|
|
153
|
+
approvalSessionKey?: string;
|
|
154
|
+
};
|
|
155
|
+
ApplyIntent: {
|
|
156
|
+
/** @description Opaque one-time Apply intent token. */
|
|
157
|
+
id: string;
|
|
158
|
+
/** @enum {string} */
|
|
159
|
+
object: "apply_intent";
|
|
160
|
+
/** Format: uri */
|
|
161
|
+
gatewayUrl: string;
|
|
162
|
+
/** Format: date-time */
|
|
163
|
+
expiresAt: string;
|
|
135
164
|
};
|
|
136
165
|
Block: {
|
|
137
166
|
/** @enum {string} */
|
|
@@ -329,6 +358,7 @@ interface components {
|
|
|
329
358
|
handle: string | null;
|
|
330
359
|
headline: string | null;
|
|
331
360
|
location: string | null;
|
|
361
|
+
countryCode: string | null;
|
|
332
362
|
/** @enum {string} */
|
|
333
363
|
profileVisibility: "hidden" | "logged_in_only" | "public";
|
|
334
364
|
/** @enum {string} */
|
|
@@ -634,6 +664,12 @@ interface components {
|
|
|
634
664
|
object: "conversation_ref";
|
|
635
665
|
conversationId: string | null;
|
|
636
666
|
};
|
|
667
|
+
CreateApplyApprovalBody: {
|
|
668
|
+
sessionKey: string;
|
|
669
|
+
};
|
|
670
|
+
CreateApplyIntentBody: {
|
|
671
|
+
sessionKey: string;
|
|
672
|
+
};
|
|
637
673
|
CreateCompanyBody: {
|
|
638
674
|
/** @description Public company website URL. Normalized to a canonical apex domain when stored. */
|
|
639
675
|
website?: string;
|
|
@@ -925,6 +961,8 @@ interface components {
|
|
|
925
961
|
linkedinUrl: string | null;
|
|
926
962
|
facebookUrl: string | null;
|
|
927
963
|
logoUrl: string | null;
|
|
964
|
+
/** @description Whether company admins may delete this company. Mirrors the board Features toggle `employerCompanyDeletionEnabled`. Absent board config defaults to `true`. When `false`, DELETE returns 403 `company_deletion_disabled` and clients should hide the danger zone. */
|
|
965
|
+
deletionEnabled: boolean;
|
|
928
966
|
};
|
|
929
967
|
EmployerCreateJobBody: {
|
|
930
968
|
/** @description Long-form description of the role. Up to 25,000 characters. */
|
|
@@ -1578,8 +1616,9 @@ interface components {
|
|
|
1578
1616
|
/** @enum {string} */
|
|
1579
1617
|
object: "notification_preference";
|
|
1580
1618
|
/** @enum {string} */
|
|
1581
|
-
channel: "messageEmails" | "applicationEmails";
|
|
1619
|
+
channel: "messageEmails" | "applicationEmails" | "recommendedJobEmails";
|
|
1582
1620
|
subscribed: boolean;
|
|
1621
|
+
waitlisted: boolean;
|
|
1583
1622
|
updatedAt: number | null;
|
|
1584
1623
|
};
|
|
1585
1624
|
PaywallOffer: {
|
|
@@ -1753,6 +1792,10 @@ interface components {
|
|
|
1753
1792
|
showCavunoBranding: boolean;
|
|
1754
1793
|
features: {
|
|
1755
1794
|
jobAlerts: boolean;
|
|
1795
|
+
/** @description Whether personalized candidate job recommendations are available on this board. Absent board config defaults to `true`. */
|
|
1796
|
+
jobRecommendationsEnabled: boolean;
|
|
1797
|
+
/** @description Whether the board-level Recommended talent toggle is enabled. The endpoint additionally requires the Candidate area and a non-off talent directory. Absent board config defaults to `false`. */
|
|
1798
|
+
recommendedTalentEnabled: boolean;
|
|
1756
1799
|
candidates: boolean;
|
|
1757
1800
|
employers: boolean;
|
|
1758
1801
|
blog: boolean;
|
|
@@ -1778,11 +1821,37 @@ interface components {
|
|
|
1778
1821
|
linkedInPartnerId: string | null;
|
|
1779
1822
|
cookieConsentRequired: boolean;
|
|
1780
1823
|
};
|
|
1824
|
+
/** @description Public AdSense switch + publisher id from board advertising settings. Not the slot map (Puck/Cavuno placements) and not `ads.txt` (`board.seo()`). */
|
|
1825
|
+
ads: {
|
|
1826
|
+
/** @description Whether AdSense is turned on for this board (Settings → Advertising). Headless frontends render their own placements only when this is true and `clientId` is a valid `ca-pub-` publisher id. Per-placement slot ids are not on this resource — applications own geometry. */
|
|
1827
|
+
enabled: boolean;
|
|
1828
|
+
/** @description AdSense publisher id (`ca-pub-` + 16 digits), or null when ads are off or the stored value is malformed. Already public on hosted pages as `data-ad-client`. */
|
|
1829
|
+
clientId: string | null;
|
|
1830
|
+
};
|
|
1781
1831
|
/** @description Operator-defined custom field definitions keyed by model (currently only `job`). Each key holds that model's definitions in display order. Company and talent keys are added when those models ship. The frontend uses these to render and localize each record's opaque `customFieldValues`. Display-only: not filterable or searchable in v1. */
|
|
1782
1832
|
customFields: {
|
|
1783
1833
|
/** @description This model's custom-field definitions in display order. Empty when the board defines none for the model. */
|
|
1784
1834
|
job: components["schemas"]["CustomFieldDefinition"][];
|
|
1785
1835
|
};
|
|
1836
|
+
/** @description Built-in job-form field visibility from Settings → Job form. Does not include custom fields (`customFields.job`) or operator-only constraints (required, bounds, allowlists). */
|
|
1837
|
+
jobForm: {
|
|
1838
|
+
salary: {
|
|
1839
|
+
/** @description Whether salary is captured on posting forms and shown on public job cards. Absent board config defaults to `true`. */
|
|
1840
|
+
visible: boolean;
|
|
1841
|
+
};
|
|
1842
|
+
seniority: {
|
|
1843
|
+
/** @description Whether seniority is captured on posting forms. Absent board config defaults to `true`. */
|
|
1844
|
+
visible: boolean;
|
|
1845
|
+
};
|
|
1846
|
+
location: {
|
|
1847
|
+
/** @description Whether office location is captured on posting forms. Absent board config defaults to `true`. */
|
|
1848
|
+
visible: boolean;
|
|
1849
|
+
};
|
|
1850
|
+
sponsorship: {
|
|
1851
|
+
/** @description Whether visa sponsorship is captured on posting forms. Absent board config defaults to `true`. */
|
|
1852
|
+
visible: boolean;
|
|
1853
|
+
};
|
|
1854
|
+
};
|
|
1786
1855
|
/** @description Operator-authored contact and social identity (settings › Contact & company details). Extracted from the former `footer` group in 4.0.0: contact is identity data, not layout. Brand/social URLs are sanitized to absolute http(s). */
|
|
1787
1856
|
contact: {
|
|
1788
1857
|
/** @description Public contact email (render as `mailto:`). Operator-set in settings › Contact & company details. */
|
|
@@ -1880,6 +1949,9 @@ interface components {
|
|
|
1880
1949
|
description: string | null;
|
|
1881
1950
|
/** @description Where candidates apply, or `null` if not specified. An HTTPS URL or `mailto:` URI. */
|
|
1882
1951
|
applicationUrl: string | null;
|
|
1952
|
+
isSponsored: boolean;
|
|
1953
|
+
/** @enum {string} */
|
|
1954
|
+
applyAction: "native" | "external_direct" | "gateway_external" | "gateway_native";
|
|
1883
1955
|
/** @description Hierarchical permit selection authored by the board owner. The three `remoteWorkPermit*` and `remoteWorldwide` fields are read-only derived projections. */
|
|
1884
1956
|
remotePermits: {
|
|
1885
1957
|
type: string;
|
|
@@ -2156,6 +2228,8 @@ interface components {
|
|
|
2156
2228
|
canonicalSlug: string;
|
|
2157
2229
|
/** @description Board-language name with source-name fallback. */
|
|
2158
2230
|
displayName: string;
|
|
2231
|
+
/** @description Live published-job count for this term on the board. Maintained as jobs publish and expire — not a tally of the current list page. */
|
|
2232
|
+
jobCount: number;
|
|
2159
2233
|
};
|
|
2160
2234
|
PublicVerifyBoardPasswordBody: {
|
|
2161
2235
|
/** @description The board password. */
|
|
@@ -2761,7 +2835,7 @@ interface components {
|
|
|
2761
2835
|
UnsubscribeBody: {
|
|
2762
2836
|
boardUserId: string;
|
|
2763
2837
|
/** @enum {string} */
|
|
2764
|
-
channel: "messageEmails" | "applicationEmails";
|
|
2838
|
+
channel: "messageEmails" | "applicationEmails" | "recommendedJobEmails";
|
|
2765
2839
|
token: string;
|
|
2766
2840
|
};
|
|
2767
2841
|
UpdateApplicationFactsBody: {
|
|
@@ -2778,6 +2852,7 @@ interface components {
|
|
|
2778
2852
|
handle?: string;
|
|
2779
2853
|
headline?: string;
|
|
2780
2854
|
location?: string;
|
|
2855
|
+
countryCode?: string | null;
|
|
2781
2856
|
/** @enum {string} */
|
|
2782
2857
|
profileVisibility?: "hidden" | "logged_in_only" | "public";
|
|
2783
2858
|
/** @enum {string} */
|
|
@@ -2832,7 +2907,7 @@ interface components {
|
|
|
2832
2907
|
};
|
|
2833
2908
|
UpdateNotificationPreferenceBody: {
|
|
2834
2909
|
/** @enum {string} */
|
|
2835
|
-
channel: "messageEmails" | "applicationEmails";
|
|
2910
|
+
channel: "messageEmails" | "applicationEmails" | "recommendedJobEmails";
|
|
2836
2911
|
subscribed: boolean;
|
|
2837
2912
|
};
|
|
2838
2913
|
UpdatePasswordBody: {
|
|
@@ -127,11 +127,40 @@ interface components {
|
|
|
127
127
|
companyName: string | null;
|
|
128
128
|
} | null;
|
|
129
129
|
};
|
|
130
|
+
ApplyApprovalPlan: {
|
|
131
|
+
/** @enum {string} */
|
|
132
|
+
object: "apply_approval_plan";
|
|
133
|
+
/** @enum {string} */
|
|
134
|
+
kind: "not_required";
|
|
135
|
+
} | {
|
|
136
|
+
/** @enum {string} */
|
|
137
|
+
object: "apply_approval_plan";
|
|
138
|
+
/** @enum {string} */
|
|
139
|
+
kind: "approval_required";
|
|
140
|
+
/** Format: uri */
|
|
141
|
+
approvalUrl: string;
|
|
142
|
+
/** Format: date-time */
|
|
143
|
+
expiresAt: string;
|
|
144
|
+
};
|
|
130
145
|
ApplyBody: {
|
|
131
146
|
name?: string;
|
|
132
147
|
/** Format: email */
|
|
133
148
|
email?: string;
|
|
134
149
|
coverNote?: string;
|
|
150
|
+
/** @description Opaque native country-approval receipt returned by the browser-direct Apply gateway. */
|
|
151
|
+
approvalReceipt?: string;
|
|
152
|
+
/** @description Server-owned board session key that prepared the country approval. */
|
|
153
|
+
approvalSessionKey?: string;
|
|
154
|
+
};
|
|
155
|
+
ApplyIntent: {
|
|
156
|
+
/** @description Opaque one-time Apply intent token. */
|
|
157
|
+
id: string;
|
|
158
|
+
/** @enum {string} */
|
|
159
|
+
object: "apply_intent";
|
|
160
|
+
/** Format: uri */
|
|
161
|
+
gatewayUrl: string;
|
|
162
|
+
/** Format: date-time */
|
|
163
|
+
expiresAt: string;
|
|
135
164
|
};
|
|
136
165
|
Block: {
|
|
137
166
|
/** @enum {string} */
|
|
@@ -329,6 +358,7 @@ interface components {
|
|
|
329
358
|
handle: string | null;
|
|
330
359
|
headline: string | null;
|
|
331
360
|
location: string | null;
|
|
361
|
+
countryCode: string | null;
|
|
332
362
|
/** @enum {string} */
|
|
333
363
|
profileVisibility: "hidden" | "logged_in_only" | "public";
|
|
334
364
|
/** @enum {string} */
|
|
@@ -634,6 +664,12 @@ interface components {
|
|
|
634
664
|
object: "conversation_ref";
|
|
635
665
|
conversationId: string | null;
|
|
636
666
|
};
|
|
667
|
+
CreateApplyApprovalBody: {
|
|
668
|
+
sessionKey: string;
|
|
669
|
+
};
|
|
670
|
+
CreateApplyIntentBody: {
|
|
671
|
+
sessionKey: string;
|
|
672
|
+
};
|
|
637
673
|
CreateCompanyBody: {
|
|
638
674
|
/** @description Public company website URL. Normalized to a canonical apex domain when stored. */
|
|
639
675
|
website?: string;
|
|
@@ -925,6 +961,8 @@ interface components {
|
|
|
925
961
|
linkedinUrl: string | null;
|
|
926
962
|
facebookUrl: string | null;
|
|
927
963
|
logoUrl: string | null;
|
|
964
|
+
/** @description Whether company admins may delete this company. Mirrors the board Features toggle `employerCompanyDeletionEnabled`. Absent board config defaults to `true`. When `false`, DELETE returns 403 `company_deletion_disabled` and clients should hide the danger zone. */
|
|
965
|
+
deletionEnabled: boolean;
|
|
928
966
|
};
|
|
929
967
|
EmployerCreateJobBody: {
|
|
930
968
|
/** @description Long-form description of the role. Up to 25,000 characters. */
|
|
@@ -1578,8 +1616,9 @@ interface components {
|
|
|
1578
1616
|
/** @enum {string} */
|
|
1579
1617
|
object: "notification_preference";
|
|
1580
1618
|
/** @enum {string} */
|
|
1581
|
-
channel: "messageEmails" | "applicationEmails";
|
|
1619
|
+
channel: "messageEmails" | "applicationEmails" | "recommendedJobEmails";
|
|
1582
1620
|
subscribed: boolean;
|
|
1621
|
+
waitlisted: boolean;
|
|
1583
1622
|
updatedAt: number | null;
|
|
1584
1623
|
};
|
|
1585
1624
|
PaywallOffer: {
|
|
@@ -1753,6 +1792,10 @@ interface components {
|
|
|
1753
1792
|
showCavunoBranding: boolean;
|
|
1754
1793
|
features: {
|
|
1755
1794
|
jobAlerts: boolean;
|
|
1795
|
+
/** @description Whether personalized candidate job recommendations are available on this board. Absent board config defaults to `true`. */
|
|
1796
|
+
jobRecommendationsEnabled: boolean;
|
|
1797
|
+
/** @description Whether the board-level Recommended talent toggle is enabled. The endpoint additionally requires the Candidate area and a non-off talent directory. Absent board config defaults to `false`. */
|
|
1798
|
+
recommendedTalentEnabled: boolean;
|
|
1756
1799
|
candidates: boolean;
|
|
1757
1800
|
employers: boolean;
|
|
1758
1801
|
blog: boolean;
|
|
@@ -1778,11 +1821,37 @@ interface components {
|
|
|
1778
1821
|
linkedInPartnerId: string | null;
|
|
1779
1822
|
cookieConsentRequired: boolean;
|
|
1780
1823
|
};
|
|
1824
|
+
/** @description Public AdSense switch + publisher id from board advertising settings. Not the slot map (Puck/Cavuno placements) and not `ads.txt` (`board.seo()`). */
|
|
1825
|
+
ads: {
|
|
1826
|
+
/** @description Whether AdSense is turned on for this board (Settings → Advertising). Headless frontends render their own placements only when this is true and `clientId` is a valid `ca-pub-` publisher id. Per-placement slot ids are not on this resource — applications own geometry. */
|
|
1827
|
+
enabled: boolean;
|
|
1828
|
+
/** @description AdSense publisher id (`ca-pub-` + 16 digits), or null when ads are off or the stored value is malformed. Already public on hosted pages as `data-ad-client`. */
|
|
1829
|
+
clientId: string | null;
|
|
1830
|
+
};
|
|
1781
1831
|
/** @description Operator-defined custom field definitions keyed by model (currently only `job`). Each key holds that model's definitions in display order. Company and talent keys are added when those models ship. The frontend uses these to render and localize each record's opaque `customFieldValues`. Display-only: not filterable or searchable in v1. */
|
|
1782
1832
|
customFields: {
|
|
1783
1833
|
/** @description This model's custom-field definitions in display order. Empty when the board defines none for the model. */
|
|
1784
1834
|
job: components["schemas"]["CustomFieldDefinition"][];
|
|
1785
1835
|
};
|
|
1836
|
+
/** @description Built-in job-form field visibility from Settings → Job form. Does not include custom fields (`customFields.job`) or operator-only constraints (required, bounds, allowlists). */
|
|
1837
|
+
jobForm: {
|
|
1838
|
+
salary: {
|
|
1839
|
+
/** @description Whether salary is captured on posting forms and shown on public job cards. Absent board config defaults to `true`. */
|
|
1840
|
+
visible: boolean;
|
|
1841
|
+
};
|
|
1842
|
+
seniority: {
|
|
1843
|
+
/** @description Whether seniority is captured on posting forms. Absent board config defaults to `true`. */
|
|
1844
|
+
visible: boolean;
|
|
1845
|
+
};
|
|
1846
|
+
location: {
|
|
1847
|
+
/** @description Whether office location is captured on posting forms. Absent board config defaults to `true`. */
|
|
1848
|
+
visible: boolean;
|
|
1849
|
+
};
|
|
1850
|
+
sponsorship: {
|
|
1851
|
+
/** @description Whether visa sponsorship is captured on posting forms. Absent board config defaults to `true`. */
|
|
1852
|
+
visible: boolean;
|
|
1853
|
+
};
|
|
1854
|
+
};
|
|
1786
1855
|
/** @description Operator-authored contact and social identity (settings › Contact & company details). Extracted from the former `footer` group in 4.0.0: contact is identity data, not layout. Brand/social URLs are sanitized to absolute http(s). */
|
|
1787
1856
|
contact: {
|
|
1788
1857
|
/** @description Public contact email (render as `mailto:`). Operator-set in settings › Contact & company details. */
|
|
@@ -1880,6 +1949,9 @@ interface components {
|
|
|
1880
1949
|
description: string | null;
|
|
1881
1950
|
/** @description Where candidates apply, or `null` if not specified. An HTTPS URL or `mailto:` URI. */
|
|
1882
1951
|
applicationUrl: string | null;
|
|
1952
|
+
isSponsored: boolean;
|
|
1953
|
+
/** @enum {string} */
|
|
1954
|
+
applyAction: "native" | "external_direct" | "gateway_external" | "gateway_native";
|
|
1883
1955
|
/** @description Hierarchical permit selection authored by the board owner. The three `remoteWorkPermit*` and `remoteWorldwide` fields are read-only derived projections. */
|
|
1884
1956
|
remotePermits: {
|
|
1885
1957
|
type: string;
|
|
@@ -2156,6 +2228,8 @@ interface components {
|
|
|
2156
2228
|
canonicalSlug: string;
|
|
2157
2229
|
/** @description Board-language name with source-name fallback. */
|
|
2158
2230
|
displayName: string;
|
|
2231
|
+
/** @description Live published-job count for this term on the board. Maintained as jobs publish and expire — not a tally of the current list page. */
|
|
2232
|
+
jobCount: number;
|
|
2159
2233
|
};
|
|
2160
2234
|
PublicVerifyBoardPasswordBody: {
|
|
2161
2235
|
/** @description The board password. */
|
|
@@ -2761,7 +2835,7 @@ interface components {
|
|
|
2761
2835
|
UnsubscribeBody: {
|
|
2762
2836
|
boardUserId: string;
|
|
2763
2837
|
/** @enum {string} */
|
|
2764
|
-
channel: "messageEmails" | "applicationEmails";
|
|
2838
|
+
channel: "messageEmails" | "applicationEmails" | "recommendedJobEmails";
|
|
2765
2839
|
token: string;
|
|
2766
2840
|
};
|
|
2767
2841
|
UpdateApplicationFactsBody: {
|
|
@@ -2778,6 +2852,7 @@ interface components {
|
|
|
2778
2852
|
handle?: string;
|
|
2779
2853
|
headline?: string;
|
|
2780
2854
|
location?: string;
|
|
2855
|
+
countryCode?: string | null;
|
|
2781
2856
|
/** @enum {string} */
|
|
2782
2857
|
profileVisibility?: "hidden" | "logged_in_only" | "public";
|
|
2783
2858
|
/** @enum {string} */
|
|
@@ -2832,7 +2907,7 @@ interface components {
|
|
|
2832
2907
|
};
|
|
2833
2908
|
UpdateNotificationPreferenceBody: {
|
|
2834
2909
|
/** @enum {string} */
|
|
2835
|
-
channel: "messageEmails" | "applicationEmails";
|
|
2910
|
+
channel: "messageEmails" | "applicationEmails" | "recommendedJobEmails";
|
|
2836
2911
|
subscribed: boolean;
|
|
2837
2912
|
};
|
|
2838
2913
|
UpdatePasswordBody: {
|
package/dist/bin.mjs
CHANGED
|
@@ -109,7 +109,14 @@ function summarize(results) {
|
|
|
109
109
|
const passed = results.filter((r) => r.status === "pass").map((r) => r.id);
|
|
110
110
|
const failed = results.filter((r) => r.status === "fail").map((r) => r.id);
|
|
111
111
|
const skipped = results.filter((r) => r.status === "skip").map((r) => r.id);
|
|
112
|
-
|
|
112
|
+
const warned = results.filter((r) => r.status === "warn").map((r) => r.id);
|
|
113
|
+
return {
|
|
114
|
+
exitCode: failed.length > 0 ? 1 : 0,
|
|
115
|
+
passed,
|
|
116
|
+
failed,
|
|
117
|
+
skipped,
|
|
118
|
+
warned
|
|
119
|
+
};
|
|
113
120
|
}
|
|
114
121
|
|
|
115
122
|
// src/doctor/cookie-conformance.ts
|
|
@@ -284,7 +291,7 @@ function checkCookieCodecConformance(projectRoot) {
|
|
|
284
291
|
}
|
|
285
292
|
|
|
286
293
|
// src/doctor/probe.ts
|
|
287
|
-
var FETCH_TIMEOUT_MS =
|
|
294
|
+
var FETCH_TIMEOUT_MS = 6e4;
|
|
288
295
|
async function probe(fetchImpl, url) {
|
|
289
296
|
try {
|
|
290
297
|
const response = await fetchImpl(url, {
|
|
@@ -310,7 +317,11 @@ var READ = {
|
|
|
310
317
|
jobs: record("read.jobs", 2),
|
|
311
318
|
jsonld: record("read.jsonld", 2),
|
|
312
319
|
sitemap: record("read.sitemap", 2),
|
|
313
|
-
robots: record("read.robots", 2)
|
|
320
|
+
robots: record("read.robots", 2),
|
|
321
|
+
adsTxt: record("read.adsTxt", 2),
|
|
322
|
+
indexNow: record("read.indexNow", 2),
|
|
323
|
+
googleVerification: record("read.googleVerification", 2),
|
|
324
|
+
oauthCallback: record("read.oauthCallback", 2)
|
|
314
325
|
};
|
|
315
326
|
function skipReadProbes(reason) {
|
|
316
327
|
return Object.values(READ).map((make) => make("skip", reason));
|
|
@@ -366,7 +377,11 @@ async function probeSitemap(fetchImpl, base) {
|
|
|
366
377
|
}
|
|
367
378
|
let urls = doc.urls;
|
|
368
379
|
if (doc.kind === "index") {
|
|
369
|
-
const
|
|
380
|
+
const childUrl = onFrontend(base, urls[0]);
|
|
381
|
+
let child = await probe(fetchImpl, childUrl);
|
|
382
|
+
if (!child.ok && (child.status === 503 || child.status === 0)) {
|
|
383
|
+
child = await probe(fetchImpl, childUrl);
|
|
384
|
+
}
|
|
370
385
|
const childDoc = child.ok ? parseSitemap(child.body) : null;
|
|
371
386
|
if (!childDoc || childDoc.urls.length === 0) {
|
|
372
387
|
return READ.sitemap(
|
|
@@ -380,14 +395,109 @@ async function probeSitemap(fetchImpl, base) {
|
|
|
380
395
|
const sample = await probe(fetchImpl, sampleUrl);
|
|
381
396
|
return sample.ok ? READ.sitemap("pass", `${urls.length} entries; sample page resolves`) : READ.sitemap("fail", `sitemap page ${sampleUrl} \u2192 HTTP ${sample.status}`);
|
|
382
397
|
}
|
|
383
|
-
|
|
398
|
+
function scoreRobots(robots, seo) {
|
|
399
|
+
if (!robots.ok) {
|
|
400
|
+
return READ.robots("fail", `robots.txt HTTP ${robots.status}`);
|
|
401
|
+
}
|
|
402
|
+
const missing = [];
|
|
403
|
+
if (!/user-agent\s*:/i.test(robots.body)) missing.push("User-agent");
|
|
404
|
+
if (!robots.body.includes("Disallow: /go/")) missing.push("Disallow: /go/");
|
|
405
|
+
if (seo) {
|
|
406
|
+
const canonicalBase = seo.canonicalBase.replace(/\/+$/, "");
|
|
407
|
+
const sitemapLine = `Sitemap: ${canonicalBase}/sitemap.xml`;
|
|
408
|
+
if (!robots.body.includes(sitemapLine)) missing.push(sitemapLine);
|
|
409
|
+
}
|
|
410
|
+
if (missing.includes("User-agent")) {
|
|
411
|
+
return READ.robots(
|
|
412
|
+
"fail",
|
|
413
|
+
"robots.txt returned 200 but has no User-agent directive"
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
return missing.length === 0 ? READ.robots("pass", "present") : READ.robots(
|
|
417
|
+
"warn",
|
|
418
|
+
`robots.txt is missing ${missing.join(", ")} (hosted boards emit it)`
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
function configuredText(value) {
|
|
422
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
423
|
+
}
|
|
424
|
+
function scoreOptionalTextFile(options) {
|
|
425
|
+
const { make, label, path, seo, probed } = options;
|
|
426
|
+
if (seo == null) {
|
|
427
|
+
return make("skip", "board seo not fetched");
|
|
428
|
+
}
|
|
429
|
+
const expected = configuredText(options.configured);
|
|
430
|
+
if (expected !== null) {
|
|
431
|
+
if (!probed.ok) {
|
|
432
|
+
return make("fail", `${path} HTTP ${probed.status}`);
|
|
433
|
+
}
|
|
434
|
+
if (probed.body.trimEnd() === expected.trimEnd()) {
|
|
435
|
+
return make("pass", `${label} matches dashboard`);
|
|
436
|
+
}
|
|
437
|
+
return make("fail", `${path} body does not match dashboard ${label}`);
|
|
438
|
+
}
|
|
439
|
+
if (probed.status === 404) {
|
|
440
|
+
return make("pass", "unconfigured; 404 as expected");
|
|
441
|
+
}
|
|
442
|
+
if (probed.status === 200) {
|
|
443
|
+
return make(
|
|
444
|
+
"warn",
|
|
445
|
+
`${label} served but not configured in dashboard \u2014 clone leftover?`
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
return make("fail", `${path} HTTP ${probed.status}`);
|
|
449
|
+
}
|
|
450
|
+
function escapeRegExp(value) {
|
|
451
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
452
|
+
}
|
|
453
|
+
function hasGoogleSiteVerificationMeta(html, token) {
|
|
454
|
+
const escaped = escapeRegExp(token);
|
|
455
|
+
const quoted = `["']${escaped}["']`;
|
|
456
|
+
const name = `name\\s*=\\s*["']google-site-verification["']`;
|
|
457
|
+
const content = `content\\s*=\\s*${quoted}`;
|
|
458
|
+
return new RegExp(`<meta\\b[^>]*\\b${name}[^>]*\\b${content}`, "i").test(html) || new RegExp(`<meta\\b[^>]*\\b${content}[^>]*\\b${name}`, "i").test(html);
|
|
459
|
+
}
|
|
460
|
+
function scoreGoogleVerification(homeHtml, seo) {
|
|
461
|
+
if (seo == null) {
|
|
462
|
+
return READ.googleVerification("skip", "board seo not fetched");
|
|
463
|
+
}
|
|
464
|
+
const token = configuredText(seo.googleSiteVerification);
|
|
465
|
+
if (token === null) {
|
|
466
|
+
return READ.googleVerification("skip", "no token configured");
|
|
467
|
+
}
|
|
468
|
+
return hasGoogleSiteVerificationMeta(homeHtml, token) ? READ.googleVerification("pass", "google-site-verification meta present") : READ.googleVerification(
|
|
469
|
+
"fail",
|
|
470
|
+
"home HTML is missing the google-site-verification meta tag"
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
var OAUTH_PROVIDERS = ["google", "linkedin"];
|
|
474
|
+
function scoreOAuthCallback(probed) {
|
|
475
|
+
const broken = OAUTH_PROVIDERS.filter(
|
|
476
|
+
(provider) => !(probed[provider].status === 400 && /missing code or state/i.test(probed[provider].body))
|
|
477
|
+
);
|
|
478
|
+
return broken.length === 0 ? READ.oauthCallback("pass", "callback paths reach the hosted app") : READ.oauthCallback(
|
|
479
|
+
"warn",
|
|
480
|
+
`${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`
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
async function runReadProbes(fetchImpl, frontendUrl, seo) {
|
|
384
484
|
const base = frontendUrl.replace(/\/$/, "");
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
485
|
+
const home = await probe(fetchImpl, base);
|
|
486
|
+
const jobsAndJsonLd = await probeJobsAndJsonLd(fetchImpl, base);
|
|
487
|
+
const sitemap = await probeSitemap(fetchImpl, base);
|
|
488
|
+
const robots = await probe(fetchImpl, `${base}/robots.txt`);
|
|
489
|
+
const adsTxt = await probe(fetchImpl, `${base}/ads.txt`);
|
|
490
|
+
const indexNow = await probe(fetchImpl, `${base}/indexnow-key.txt`);
|
|
491
|
+
const oauthCallback = {
|
|
492
|
+
google: await probe(
|
|
493
|
+
fetchImpl,
|
|
494
|
+
`${base}/api/board-auth/oauth/google/callback`
|
|
495
|
+
),
|
|
496
|
+
linkedin: await probe(
|
|
497
|
+
fetchImpl,
|
|
498
|
+
`${base}/api/board-auth/oauth/linkedin/callback`
|
|
499
|
+
)
|
|
500
|
+
};
|
|
391
501
|
return [
|
|
392
502
|
home.ok && /<(html|body|div|main)[\s>]/i.test(home.body) ? READ.home("pass", "home renders") : READ.home(
|
|
393
503
|
"fail",
|
|
@@ -395,10 +505,25 @@ async function runReadProbes(fetchImpl, frontendUrl) {
|
|
|
395
505
|
),
|
|
396
506
|
...jobsAndJsonLd,
|
|
397
507
|
sitemap,
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
508
|
+
scoreRobots(robots, seo),
|
|
509
|
+
scoreOptionalTextFile({
|
|
510
|
+
make: READ.adsTxt,
|
|
511
|
+
label: "ads.txt",
|
|
512
|
+
path: "/ads.txt",
|
|
513
|
+
seo,
|
|
514
|
+
configured: seo?.adsTxt,
|
|
515
|
+
probed: adsTxt
|
|
516
|
+
}),
|
|
517
|
+
scoreOptionalTextFile({
|
|
518
|
+
make: READ.indexNow,
|
|
519
|
+
label: "indexnow-key.txt",
|
|
520
|
+
path: "/indexnow-key.txt",
|
|
521
|
+
seo,
|
|
522
|
+
configured: seo?.indexNowKey,
|
|
523
|
+
probed: indexNow
|
|
524
|
+
}),
|
|
525
|
+
scoreGoogleVerification(home.body, seo),
|
|
526
|
+
scoreOAuthCallback(oauthCallback)
|
|
402
527
|
];
|
|
403
528
|
}
|
|
404
529
|
|
|
@@ -454,6 +579,28 @@ async function checkBoardResolves(fetchImpl, env) {
|
|
|
454
579
|
}
|
|
455
580
|
return STATIC_BOARD("pass", "publishable key resolves the board");
|
|
456
581
|
}
|
|
582
|
+
async function fetchBoardSeo(fetchImpl, env) {
|
|
583
|
+
const result = await probe(
|
|
584
|
+
fetchImpl,
|
|
585
|
+
`${apiBase(env.apiUrl)}/v1/boards/${encodeURIComponent(env.boardKey)}/seo`
|
|
586
|
+
);
|
|
587
|
+
if (!result.ok) return null;
|
|
588
|
+
try {
|
|
589
|
+
const parsed = JSON.parse(result.body);
|
|
590
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
591
|
+
const rec = parsed;
|
|
592
|
+
if (typeof rec.canonicalBase !== "string") return null;
|
|
593
|
+
const asNullableString = (value) => value === null || typeof value === "string" ? value : null;
|
|
594
|
+
return {
|
|
595
|
+
canonicalBase: rec.canonicalBase,
|
|
596
|
+
adsTxt: asNullableString(rec.adsTxt),
|
|
597
|
+
indexNowKey: asNullableString(rec.indexNowKey),
|
|
598
|
+
googleSiteVerification: asNullableString(rec.googleSiteVerification)
|
|
599
|
+
};
|
|
600
|
+
} catch {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
457
604
|
var SKILL_ROOTS = [
|
|
458
605
|
".claude/skills",
|
|
459
606
|
".agents/skills",
|
|
@@ -515,8 +662,13 @@ async function runDoctor(options) {
|
|
|
515
662
|
} else {
|
|
516
663
|
results.push(STATIC_API("skip", "PUBLIC_CAVUNO_API_URL not set"));
|
|
517
664
|
}
|
|
665
|
+
let seo = null;
|
|
518
666
|
if (envOk) {
|
|
519
|
-
|
|
667
|
+
const boardResult = await checkBoardResolves(fetchImpl, env);
|
|
668
|
+
results.push(boardResult);
|
|
669
|
+
if (boardResult.status === "pass") {
|
|
670
|
+
seo = await fetchBoardSeo(fetchImpl, env);
|
|
671
|
+
}
|
|
520
672
|
} else {
|
|
521
673
|
results.push(STATIC_BOARD("skip", "env checks failed \u2014 fix them first"));
|
|
522
674
|
}
|
|
@@ -525,7 +677,7 @@ async function runDoctor(options) {
|
|
|
525
677
|
...checkCookieCodecConformance(options.projectRoot ?? process.cwd())
|
|
526
678
|
);
|
|
527
679
|
results.push(
|
|
528
|
-
...options.frontendUrl ? await runReadProbes(fetchImpl, options.frontendUrl) : skipReadProbes("no --frontend <url> given")
|
|
680
|
+
...options.frontendUrl ? await runReadProbes(fetchImpl, options.frontendUrl, seo) : skipReadProbes("no --frontend <url> given")
|
|
529
681
|
);
|
|
530
682
|
return { results, summary: summarize(results) };
|
|
531
683
|
}
|
|
@@ -596,18 +748,21 @@ async function doctor(argv) {
|
|
|
596
748
|
});
|
|
597
749
|
console.log("\n@cavuno/board doctor");
|
|
598
750
|
for (const result of results) {
|
|
599
|
-
const mark = result.status === "pass" ? "\u2713" : result.status === "fail" ? "\u2717" : "\u2212";
|
|
751
|
+
const mark = result.status === "pass" ? "\u2713" : result.status === "fail" ? "\u2717" : result.status === "warn" ? "!" : "\u2212";
|
|
600
752
|
console.log(
|
|
601
753
|
` ${mark} [tier ${result.tier}] ${result.id} \u2014 ${result.detail}`
|
|
602
754
|
);
|
|
603
755
|
}
|
|
604
756
|
console.log(
|
|
605
757
|
`
|
|
606
|
-
${summary.passed.length} passed, ${summary.failed.length} failed, ${summary.skipped.length} skipped`
|
|
758
|
+
${summary.passed.length} passed, ${summary.failed.length} failed, ${summary.skipped.length} skipped, ${summary.warned.length} warned`
|
|
607
759
|
);
|
|
608
760
|
if (summary.skipped.length > 0) {
|
|
609
761
|
console.log(` Skipped (NOT verified): ${summary.skipped.join(", ")}`);
|
|
610
762
|
}
|
|
763
|
+
if (summary.warned.length > 0) {
|
|
764
|
+
console.log(` Warned: ${summary.warned.join(", ")}`);
|
|
765
|
+
}
|
|
611
766
|
if (!frontendUrl) {
|
|
612
767
|
console.log(
|
|
613
768
|
" Pass --frontend <url> to run the read probes against your board frontend."
|