@cavuno/board 2.0.5 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +12 -3
  3. package/dist/{_spec-BKtVFcV9.d.mts → _spec-nkWAraS0.d.mts} +4 -18
  4. package/dist/{_spec-BKtVFcV9.d.ts → _spec-nkWAraS0.d.ts} +4 -18
  5. package/dist/bin.mjs +40 -57
  6. package/dist/{board-3Xj_Y-Tx.d.ts → board-NE98PlwN.d.ts} +2 -3
  7. package/dist/{board-pNN710Zc.d.mts → board-muu8BI8P.d.mts} +2 -3
  8. package/dist/doctor.js +21 -47
  9. package/dist/doctor.mjs +24 -50
  10. package/dist/filters.d.mts +2 -2
  11. package/dist/filters.d.ts +2 -2
  12. package/dist/format.d.mts +3 -3
  13. package/dist/format.d.ts +3 -3
  14. package/dist/index.d.mts +11 -27
  15. package/dist/index.d.ts +11 -27
  16. package/dist/index.js +3 -3
  17. package/dist/index.mjs +3 -3
  18. package/dist/{jobs-C4qRfCdP.d.ts → jobs-BiEvL9d7.d.ts} +1 -1
  19. package/dist/{jobs-k84C0v_8.d.mts → jobs-lVvKCBCU.d.mts} +1 -1
  20. package/dist/{salaries--J5mTylN.d.ts → salaries-BahBpxWT.d.ts} +2 -2
  21. package/dist/{salaries-C9YLO5NG.d.mts → salaries-ChSqDBAM.d.mts} +2 -2
  22. package/dist/{search-CE_qQiM7.d.mts → search-CCyFWPi3.d.mts} +1 -1
  23. package/dist/{search-CLRQ_El9.d.ts → search-CyRvVMUp.d.ts} +1 -1
  24. package/dist/seo.d.mts +4 -4
  25. package/dist/seo.d.ts +4 -4
  26. package/dist/server.d.mts +5 -5
  27. package/dist/server.d.ts +5 -5
  28. package/dist/sitemap.d.mts +5 -5
  29. package/dist/sitemap.d.ts +5 -5
  30. package/dist/skills.d.mts +15 -4
  31. package/dist/skills.d.ts +15 -4
  32. package/dist/skills.js +30 -7
  33. package/dist/skills.mjs +30 -8
  34. package/dist/suggest.d.mts +2 -2
  35. package/dist/suggest.d.ts +2 -2
  36. package/package.json +1 -11
  37. package/skills/cavuno-board-account/SKILL.md +58 -66
  38. package/skills/cavuno-board-api-client/SKILL.md +78 -0
  39. package/skills/cavuno-board-applications/SKILL.md +42 -52
  40. package/skills/cavuno-board-auth/SKILL.md +39 -60
  41. package/skills/cavuno-board-blog/SKILL.md +64 -49
  42. package/skills/cavuno-board-companies/SKILL.md +75 -53
  43. package/skills/cavuno-board-errors/SKILL.md +32 -45
  44. package/skills/cavuno-board-filters/SKILL.md +68 -75
  45. package/skills/cavuno-board-format/SKILL.md +65 -66
  46. package/skills/cavuno-board-i18n/SKILL.md +43 -68
  47. package/skills/cavuno-board-job-alerts/SKILL.md +72 -57
  48. package/skills/cavuno-board-jobs/SKILL.md +67 -55
  49. package/skills/cavuno-board-messaging/SKILL.md +57 -59
  50. package/skills/cavuno-board-paywall/SKILL.md +52 -54
  51. package/skills/cavuno-board-post-a-job/SKILL.md +122 -0
  52. package/skills/cavuno-board-salaries/SKILL.md +77 -50
  53. package/skills/cavuno-board-search-suggestions/SKILL.md +86 -0
  54. package/skills/cavuno-board-seo/BLOG.md +40 -0
  55. package/skills/cavuno-board-seo/JOB_AND_BREADCRUMBS.md +49 -0
  56. package/skills/cavuno-board-seo/LISTING.md +36 -0
  57. package/skills/cavuno-board-seo/SALARY.md +61 -0
  58. package/skills/cavuno-board-seo/SKILL.md +34 -161
  59. package/skills/cavuno-board-server-sessions/SKILL.md +147 -0
  60. package/skills/cavuno-board-setup/SKILL.md +46 -79
  61. package/skills/cavuno-board-sitemap/SKILL.md +78 -91
  62. package/skills/cavuno-board-smoke-test/SKILL.md +38 -69
  63. package/skills/manifest.json +43 -57
  64. package/dist/theme.d.mts +0 -115
  65. package/dist/theme.d.ts +0 -115
  66. package/dist/theme.js +0 -195
  67. package/dist/theme.mjs +0 -174
  68. package/skills/cavuno-board-client/SKILL.md +0 -89
  69. package/skills/cavuno-board-job-posting/SKILL.md +0 -116
  70. package/skills/cavuno-board-server/SKILL.md +0 -229
  71. package/skills/cavuno-board-suggest/SKILL.md +0 -119
  72. package/skills/cavuno-board-theme/SKILL.md +0 -57
  73. package/skills/flavors/tanstack-start/SKILL.md +0 -102
@@ -1,76 +1,80 @@
1
1
  ---
2
2
  name: cavuno-board-blog
3
- description: Build the blog with the @cavuno/board SDK — post archives (blog.posts.list with tagSlug/authorSlug/featured), the post page (blog.posts.retrieve + adjacent + similar), blog.tags and blog.authors, and free-text blog.search. Covers the summary-vs-detail split (html on the single read only), cursor pagination, slug-change redirects (redirected/newSlug), and the SEO fields the API actually returns.
3
+ description: Public blog reads with @cavuno/board. Use for post archives, post detail, adjacent or similar posts, tag and author pages, or blog search.
4
4
  ---
5
5
 
6
- # Blog: posts, tags, authors, search
6
+ # Blog
7
7
 
8
- Every list and search returns `PublicBlogPostSummary`; only `posts.retrieve` returns the full `PublicBlogPost` with the `html` body.
8
+ Collections return `PublicBlogPostSummary`; only `blog.posts.retrieve`
9
+ returns `PublicBlogPost` with the rendered `html` body. Read
10
+ `board.context().features.blog` when deciding whether the board exposes a blog.
11
+ The host app owns authoring, feeds, sitemaps, and OG-image routes.
9
12
 
10
- ## When to use
13
+ ## Build archives
11
14
 
12
- - The blog index, tag archives, author archives, and featured rails.
13
- - The post page: body, prev/next nav, related-posts rail.
14
- - Blog search boxes.
15
-
16
- ## When not to use
17
-
18
- - Writing posts/tags/authors — that is the admin API; this SDK is the public read surface.
19
- - Deciding whether the board has a blog at all — read `features.blog` from the board context.
20
-
21
- Out of scope — do not invent exports: no RSS feed, sitemap, or OG-image generation — the host app builds those routes from the data here (`publishedAt`, `canonicalUrl`, `ogImageUrl` are all returned).
22
-
23
- ## List posts and archives
24
-
25
- `blog.posts.list` returns a `ListEnvelope<PublicBlogPostSummary>`. `BlogPostsListQuery` supports `limit` (1–100), `cursor`, `tagSlug`, `authorSlug`, and `featured: 'true'` (opt-in only — the string literal, not a boolean). Blog lists page by cursor only; there is no `offset` and no total `count`.
15
+ `blog.posts.list` returns `ListEnvelope<PublicBlogPostSummary>`. Its query
16
+ accepts `limit` (1–100), `cursor`, `tagSlug`, `authorSlug`, and
17
+ `featured: 'true'`. `featured` is the string literal. Blog lists use cursor
18
+ pagination and expose neither `offset` nor total `count`.
26
19
 
27
20
  ```ts snippet
28
21
  const page = await board.blog.posts.list({ tagSlug: 'news', limit: 12 });
29
22
  for (const post of page.data) {
30
23
  post.title;
31
- post.customExcerpt; // null unless the author wrote one
32
- post.coverUrl; // null when no cover image
24
+ post.customExcerpt;
25
+ post.coverUrl;
33
26
  post.readingTimeMin;
34
- post.authors; // embedded: id, name, slug, bio, avatarUrl, social URLs
35
- post.tags; // embedded: id, name, slug, description
27
+ post.authors;
28
+ post.tags;
36
29
  }
37
30
  const next = page.nextCursor
38
- ? await board.blog.posts.list({ tagSlug: 'news', limit: 12, cursor: page.nextCursor })
39
- : null; // nextCursor is null when hasMore is false
31
+ ? await board.blog.posts.list({
32
+ tagSlug: 'news',
33
+ limit: 12,
34
+ cursor: page.nextCursor,
35
+ })
36
+ : null;
40
37
  ```
41
38
 
42
- ## Render a post
39
+ Embedded author rows carry `id`, `name`, `slug`, `bio`, `avatarUrl`, and
40
+ social URLs. Embedded tag rows carry `id`, `name`, `slug`, and `description`.
41
+
42
+ ## Render a post and canonicalize its slug
43
43
 
44
- `posts.retrieve` adds the detail-only fields: `html` (the rendered rich-text body, nullable), `ogImageUrl`, `featureImageCaption`, `seoTitle`, `seoDescription`, `redirected`, `newSlug`.
44
+ `posts.retrieve` adds `html`, `ogImageUrl`, `featureImageCaption`, `seoTitle`,
45
+ `seoDescription`, `redirected`, and `newSlug`. Old slugs still resolve: when
46
+ `redirected` and `newSlug` are set, redirect to `newSlug` before rendering.
45
47
 
46
48
  ```ts snippet
47
49
  const post = await board.blog.posts.retrieve('hello-world');
48
50
  if (post.redirected && post.newSlug) {
49
- // the slug changed — redirect to the post at newSlug instead of rendering here
51
+ // Redirect to the post route at post.newSlug.
50
52
  }
51
- post.html; // inject as the article body
52
- post.seoTitle ?? post.title; // <title>
53
- post.seoDescription ?? post.customExcerpt; // meta description
54
- post.canonicalUrl; // author-set canonical override, or null
55
- post.ogImageUrl ?? post.coverUrl; // social card image
53
+ post.html;
54
+ post.seoTitle ?? post.title;
55
+ post.seoDescription ?? post.customExcerpt;
56
+ post.canonicalUrl;
57
+ post.ogImageUrl ?? post.coverUrl;
56
58
  ```
57
59
 
58
- Anti-pattern: don't render a post at a stale URL. When `redirected` is true, issue a redirect to `newSlug` — old slugs keep resolving, but the canonical location moved.
60
+ Fetch detail for the post page; summaries have no `html` field.
59
61
 
60
- Anti-pattern: don't look for `html` on list items. Summaries never carry it; fetch `retrieve` for the post page only.
61
-
62
- ## Prev/next and related
62
+ ## Add post navigation
63
63
 
64
64
  ```ts snippet
65
65
  const { previous, next } = await board.blog.posts.adjacent('hello-world');
66
- // previous = older post, next = newer; each a summary or null
66
+ // previous is older; next is newer; either can be null.
67
67
 
68
- const rail = await board.blog.posts.similar('hello-world', { limit: 6 }); // 1–20, default 6
68
+ const rail = await board.blog.posts.similar('hello-world', { limit: 6 });
69
69
  ```
70
70
 
71
- ## Tags and authors
71
+ `similar` accepts `limit` 1–20 and defaults to 6.
72
+
73
+ ## Build tag and author pages
72
74
 
73
- Both are list + retrieve-by-slug. `PublicBlogTag`: `id`, `name`, `slug`, `description`. `PublicBlogAuthor`: `id`, `name`, `slug`, `bio`, `avatarUrl`, `websiteUrl`, `twitterUrl`, `linkedinUrl`, `githubUrl`.
75
+ Tags and authors each provide list plus retrieve-by-slug. Combine a retrieved
76
+ entity with `posts.list({ tagSlug })` or `posts.list({ authorSlug })` for its
77
+ archive.
74
78
 
75
79
  ```ts snippet
76
80
  const { data: tags } = await board.blog.tags.list();
@@ -79,21 +83,32 @@ const { data: authors } = await board.blog.authors.list();
79
83
  const author = await board.blog.authors.retrieve('jane');
80
84
  ```
81
85
 
82
- Archive pages combine the two: `tags.retrieve` for the header, `posts.list({ tagSlug })` for the posts.
86
+ `PublicBlogTag` carries `id`, `name`, `slug`, and `description`.
87
+ `PublicBlogAuthor` carries `id`, `name`, `slug`, `bio`, `avatarUrl`,
88
+ `websiteUrl`, `twitterUrl`, `linkedinUrl`, and `githubUrl`.
83
89
 
84
- ## Search
90
+ ## Search posts
85
91
 
86
- `blog.search` posts a `BlogSearchBody` (`query` up to 200 chars, optional `cursor`, `limit` 1–50 — note the lower cap than lists) and returns a `SearchEnvelope<PublicBlogPostSummary>`:
92
+ `blog.search` posts `BlogSearchBody`: `query` up to 200 characters, optional
93
+ `cursor`, and `limit` 1–50. It returns
94
+ `SearchEnvelope<PublicBlogPostSummary>`.
87
95
 
88
96
  ```ts snippet
89
97
  const results = await board.blog.search({ query: 'launch', limit: 10 });
90
98
  results.data[0]?.slug;
91
99
  ```
92
100
 
93
- ## Verify
101
+ ## Completion gate
102
+
103
+ Finish only after every applicable check passes:
104
+
105
+ - Archive and search rows render summaries; the post page fetches detail for
106
+ `html`.
107
+ - A retrieved old slug redirects to `newSlug` before rendering.
108
+ - Featured archives send the exact string `featured: 'true'`.
109
+ - Cursor paging reaches `nextCursor: null` without relying on count or offset.
110
+ - Newest and oldest posts handle the null side of `adjacent`.
111
+
112
+ ## Cavuno SDK reference
94
113
 
95
- - A list item has no `html` key; `posts.retrieve` of the same slug returns it (string or null).
96
- - Retrieving a post by an old slug returns `redirected: true` with `newSlug` set, and your route redirects there.
97
- - `posts.list({ featured: 'true' })` returns only posts with `featured: true`.
98
- - Cursor paging terminates: `nextCursor` is `null` on the last page.
99
- - `adjacent` on the newest post returns `next: null`; on the oldest, `previous: null`.
114
+ For setup and API details beyond this workflow, use the [Cavuno Board SDK documentation](https://cavuno.com/docs/sdk).
@@ -1,99 +1,121 @@
1
1
  ---
2
2
  name: cavuno-board-companies
3
- description: Build company surfaces with the @cavuno/board SDK — the companies index (companies.list / companies.search with the market filter), the company profile (companies.retrieve, listJobs, similar), the markets sub-surface (companies.markets + markets.resolve with its 308 redirectTo), and company salary pages (companies.salaries + salaries.category). Covers PublicCompany vs PublicCompanyDetail and envelope pagination.
3
+ description: Company catalog reads with @cavuno/board. Use for company indexes, market archives, profiles, company job rails, similar companies, or company salary pages.
4
4
  ---
5
5
 
6
- # Companies: index, profile, markets, salaries
6
+ # Companies
7
7
 
8
- Lists and search return `PublicCompany`; only `retrieve` returns `PublicCompanyDetail` (which adds `markets`). Company salary pages are their own sub-surface under `companies.salaries`.
8
+ Use `PublicCompany` for list and search rows. Use `PublicCompanyDetail` from
9
+ `companies.retrieve` for a profile; it alone adds `markets`.
9
10
 
10
- ## When to use
11
+ Board-wide jobs use `jobs.*`, board-wide salary hubs use `salaries.*`, and
12
+ employer self-service uses authenticated `board.me.companies.*`. The host app
13
+ owns admin writes, sitemaps, and OG-image routes.
11
14
 
12
- - The companies index page and its market (sector) filter.
13
- - The company profile page: detail, open-jobs list, similar-companies rail.
14
- - Per-company salary pages (`/companies/:slug/salaries[/:category]`).
15
+ ## List and search
15
16
 
16
- ## When not to use
17
-
18
- - Board-wide job browse/search — `jobs.*` (see cavuno-board-jobs).
19
- - Board-wide salary hubs (titles/skills/locations) — `board.salaries.*`.
20
- - Employer self-service (claiming/managing a company) — the authed `board.me.companies.*` surface.
21
-
22
- Out of scope — do not invent exports: no company create/update/logo upload (that is the admin API, not this SDK), and no sitemap or OG-image generation — the host app owns those routes.
23
-
24
- ## List, search, market filter
25
-
26
- `companies.list` returns a `CompanyListEnvelope`: `ListEnvelope<PublicCompany>` plus optional `relatedSearches` (market suggestions). `CompaniesListQuery` supports `limit` (1–100), `cursor`, `offset` (takes precedence over `cursor`; pair with the response `count` to page in parallel), and `marketSlug` — unknown market slugs 404.
17
+ `companies.list` returns `CompanyListEnvelope`: `ListEnvelope<PublicCompany>`
18
+ plus optional market `relatedSearches`. Its query accepts `limit` (1–100),
19
+ `cursor`, `offset`, and `marketSlug`. `offset` takes precedence over `cursor`;
20
+ use it with `count` for numbered or parallel paging. An unknown `marketSlug`
21
+ returns 404.
27
22
 
28
23
  ```ts snippet
29
- const page = await board.companies.list({ limit: 20, marketSlug: 'cybersecurity' });
30
- page.hasMore;
31
- page.nextCursor; // null when hasMore is false
24
+ const page = await board.companies.list({
25
+ limit: 20,
26
+ marketSlug: 'cybersecurity',
27
+ });
32
28
  for (const company of page.data) {
33
29
  company.name;
34
30
  company.publishedJobCount;
35
- company.links.public; // canonical URL, or null when the company lacks a slug
31
+ company.links.public; // null when the company has no slug
36
32
  }
37
33
  ```
38
34
 
39
- `companies.search` posts a `CompaniesSearchBody` (`query` free text matched against the company name, up to 200 chars; optional `marketSlug`, `cursor`, `limit` 1–100) and returns a `SearchEnvelope<PublicCompany>`:
35
+ `companies.search` posts a `CompaniesSearchBody`: `query` matched against the
36
+ company name (up to 200 characters), optional `marketSlug`, `cursor`, and
37
+ `limit` (1–100). It returns `SearchEnvelope<PublicCompany>`.
40
38
 
41
39
  ```ts snippet
42
40
  const results = await board.companies.search({ query: 'acme', limit: 20 });
43
41
  ```
44
42
 
45
- ## Markets (sectors)
43
+ ## Resolve markets
44
+
45
+ `companies.markets` is callable and carries `.resolve`. The call returns
46
+ `CompanyMarket` rows (`slug`, `name`, `companyCount`) ranked by company count;
47
+ it accepts `limit` (1–200, default 100) and optional `search`.
46
48
 
47
- `companies.markets` is callable AND carries `.resolve`. The call lists the board's markets ranked by company count (`CompanyMarket`: `slug`, `name`, `companyCount`; `limit` 1–200, default 100 — a top-N preview; optional `search`). `.resolve(slug)` returns a `TaxonomyResolution` — `sourceSlug`, `canonicalSlug`, `displayName`, and `redirectTo` (the canonical slug to 308 to when the inbound slug differs; `null` otherwise).
49
+ Resolve an inbound slug before loading an archive. `TaxonomyResolution`
50
+ returns `sourceSlug`, `canonicalSlug`, `displayName`, and `redirectTo`. Issue a
51
+ 308 to `redirectTo` when present, then use the resolved slug as `marketSlug`.
48
52
 
49
53
  ```ts snippet
50
54
  const { data: markets } = await board.companies.markets({ search: 'robotics' });
51
55
  const market = await board.companies.markets.resolve('cybersecurity');
52
56
  if (market.redirectTo) {
53
- // 308 the market-scoped browse to the canonical slug
57
+ // Return a 308 to the same archive at market.redirectTo.
54
58
  }
55
59
  ```
56
60
 
57
- Resolve first, then pass the resolved slug as `marketSlug` — don't guess slugs (unknown ones 404).
58
-
59
- ## Profile: retrieve, jobs, similar
61
+ ## Render a profile
60
62
 
61
- `PublicCompany` (lists/search) carries: `id`, `name`, `slug`, `website`, `logoUrl`, `description`, `jobCount`, `publishedJobCount`, `links.public`. `PublicCompanyDetail` (retrieve only) adds `markets: CompanyMarketRef[]` (`name` + source `slug`; empty when none).
63
+ `PublicCompany` carries `id`, `name`, `slug`, `website`, `logoUrl`,
64
+ `description`, `jobCount`, `publishedJobCount`, and `links.public`.
65
+ `PublicCompanyDetail` adds `markets: CompanyMarketRef[]`, whose rows contain
66
+ `name` and source `slug`.
62
67
 
63
68
  ```ts snippet
64
- const company = await board.companies.retrieve('acme'); // PublicCompanyDetail
65
- company.markets; // detail-only
69
+ const company = await board.companies.retrieve('acme');
70
+ company.markets;
66
71
 
67
72
  const jobs = await board.companies.listJobs('acme', { limit: 10 });
68
- // JobCardListEnvelope — same PublicJobCard shape as jobs.list; cursor + limit only
69
-
70
- const rail = await board.companies.similar('acme', { limit: 6 }); // 1–20, default 6
73
+ const rail = await board.companies.similar('acme', { limit: 6 });
71
74
  ```
72
75
 
73
- `similar` uses the hosted ranking (most open roles first) and excludes the company itself.
74
-
75
- Anti-pattern: don't render `markets` from a list row — it only exists on the detail. Fetch `retrieve` for the profile page.
76
+ `listJobs` returns the same `JobCardListEnvelope` as `jobs.list` and accepts
77
+ only cursor plus limit. `similar` accepts `limit` 1–20 (default 6), excludes
78
+ the current company, and ranks by open roles. Fetch `retrieve` before reading
79
+ `markets`; list and search rows have no `markets` field.
76
80
 
77
- ## Company salaries
81
+ ## Render company salaries
78
82
 
79
- `companies.salaries` is callable AND carries `.category`. The call is the company salary overview (`CompanySalary`): `overallSalary` (nullable), `bySeniority` rows vs the board baseline (`diffPercent`), `competitors`, `topLocations`, `byCategory`, board-wide baselines, and `currency`. `.category(companySlug, categorySlug, { locale })` is one job category at the company (`CompanyCategorySalary`).
83
+ `companies.salaries` is callable and carries `.category`. The overview returns
84
+ `CompanySalary`: nullable `overallSalary`, `bySeniority` rows with board
85
+ comparison `diffPercent`, `competitors`, `topLocations`, `byCategory`,
86
+ board-wide baselines, and `currency`.
80
87
 
81
88
  ```ts snippet
82
89
  const overview = await board.companies.salaries('acme');
83
- overview.bySeniority[0]?.diffPercent; // vs the board baseline, or null
84
-
85
- const cat = await board.companies.salaries.category('acme', 'software-engineer', {
86
- locale: 'de',
87
- });
88
- cat.categorySourceSlug; // immutable English slug
89
- cat.categoryCanonicalSlug; // board-language slug — 308 to it when the inbound differs
90
+ overview.bySeniority[0]?.diffPercent;
91
+
92
+ const category = await board.companies.salaries.category(
93
+ 'acme',
94
+ 'software-engineer',
95
+ { locale: 'de' });
96
+ category.categorySourceSlug;
97
+ category.categoryCanonicalSlug;
90
98
  ```
91
99
 
92
- Pass `{ locale }` for board-language category names; the company slug/name are never localized. The response returns BOTH `categorySourceSlug` and `categoryCanonicalSlug` — the consumer issues the 308 itself.
100
+ Pass `{ locale }` for board-language category names. Company identity remains
101
+ untranslated. The API returns both the immutable English
102
+ `categorySourceSlug` and the board-language `categoryCanonicalSlug`; the host
103
+ route issues a 308 when the inbound category slug differs from the canonical
104
+ one.
105
+
106
+ ## Completion gate
107
+
108
+ Finish only after every applicable check passes:
109
+
110
+ - A known company is a `public_company`; only its retrieve response has a
111
+ `markets` array.
112
+ - A listed market slug works as `marketSlug`, while an invented one produces a
113
+ handled 404.
114
+ - Cursor pagination reaches `nextCursor: null` without repeating a page.
115
+ - A non-canonical market or salary-category route returns a 308 to the slug
116
+ supplied by the API.
117
+ - Salary figures and comparisons come directly from `CompanySalary` fields.
93
118
 
94
- ## Verify
119
+ ## Cavuno SDK reference
95
120
 
96
- - `companies.retrieve('<known-slug>')` returns `object: 'public_company'` with a `markets` array; the same company in `companies.list` has no `markets` key.
97
- - A slug from `companies.markets()` works as `marketSlug`; a made-up slug returns 404 (`isNotFound`).
98
- - Paginating with `nextCursor` terminates: `nextCursor` is `null` on the last page.
99
- - `companies.salaries.category(...)` with a non-canonical category slug returns a differing `categoryCanonicalSlug`, and your route 308s to it.
121
+ For setup and API details beyond this workflow, use the [Cavuno Board SDK documentation](https://cavuno.com/docs/sdk).
@@ -1,86 +1,73 @@
1
1
  ---
2
2
  name: cavuno-board-errors
3
- description: Handle errors and access gating with the @cavuno/board SDK — the BoardApiError shape, the typed guards (isNotFound, isUnauthorized, isValidationError, isRateLimited, isForbidden, isConflict), and the board-password flow (isBoardPasswordRequired → password.verify → X-Board-Access grant).
3
+ description: Classify Board API failures and board-password challenges. Use when mapping SDK errors to UI states, retries, support diagnostics, or password-gated access.
4
4
  ---
5
5
 
6
- # Errors and access gating
6
+ # Handle errors and access gates
7
7
 
8
- Every SDK method throws on a non-2xx response. The error keeps the server's full typed envelope — never string-match messages.
8
+ Every non-2xx SDK response throws `BoardApiError`. Branch on its typed guards and codes; messages remain presentation text rather than control flow.
9
9
 
10
- ## When to use
11
-
12
- - Branching on failures (not found, unauthorized, validation, rate limit).
13
- - Unlocking a password-protected board.
14
-
15
- ## The BoardApiError shape
10
+ ## Error contract
16
11
 
17
12
  ```ts no-check
18
13
  class BoardApiError extends Error {
19
14
  status: number;
20
- code: string; // `<domain>_<snake_reason>`
21
- details?: unknown; // structured, per-code
15
+ code: string;
16
+ details?: unknown;
22
17
  requestId?: string;
23
- raw: unknown; // parsed body, untouched
18
+ raw: unknown;
24
19
  }
25
20
  ```
26
21
 
27
- ## Branch with the typed guards
22
+ Use the narrowest matching guard, render its domain state, and rethrow unmatched failures:
28
23
 
29
24
  ```ts snippet
30
25
  import {
31
26
  isBoardApiError,
32
27
  isNotFound,
28
+ isRateLimited,
33
29
  isUnauthorized,
34
30
  isValidationError,
35
- isRateLimited,
36
- isForbidden,
37
- isConflict,
38
31
  } from '@cavuno/board';
39
32
 
40
33
  try {
41
34
  return await board.jobs.retrieve('senior-chef');
42
- } catch (err) {
43
- if (isNotFound(err)) return null; // 404 → render not-found
44
- if (isUnauthorized(err)) { // 401 → refresh or sign in
45
- /* see cavuno-board-auth */
46
- }
47
- if (isValidationError(err)) { // 400 validation_bad_request → field errors in err.details
35
+ } catch (error) {
36
+ if (isNotFound(error)) return null;
37
+ if (isUnauthorized(error)) return showSignIn();
38
+ if (isValidationError(error)) return showFieldErrors(error.details);
39
+ if (isRateLimited(error)) return scheduleRetry();
40
+ if (isBoardApiError(error)) {
41
+ console.error(error.code, error.requestId);
48
42
  }
49
- if (isRateLimited(err)) { // 429 → back off
50
- }
51
- if (isBoardApiError(err)) {
52
- console.error(err.code, err.requestId); // log code + requestId for support
53
- }
54
- throw err;
43
+ throw error;
55
44
  }
56
45
  ```
57
46
 
58
- `isForbidden` (403) and `isConflict` (409) round out the set. Use `err.requestId` in support reports.
47
+ The remaining status guards are `isForbidden` for 403 and `isConflict` for 409. Record `code` and `requestId` in support diagnostics. Because v1 may add error codes, unmatched `BoardApiError` instances still need a safe generic state.
48
+
49
+ **Complete when:** every expected failure has one typed branch, an invalid resource renders not-found, and unmatched failures retain their original error.
59
50
 
60
- ## Password-protected boards
51
+ ## Board-password challenge
61
52
 
62
- A gated board answers reads with `isBoardPasswordRequired` until the visitor presents a grant. Exchange the password once with `password.verify()`; the SDK stores the grant and attaches it as `X-Board-Access` on every subsequent read automatically.
53
+ `isBoardPasswordRequired` distinguishes the `board_password_required` 401 from an expired board-user session. Exchange the visitor's password once; browser storage then attaches the returned grant as `X-Board-Access` on later reads.
63
54
 
64
55
  ```ts snippet
65
56
  import { isBoardPasswordRequired } from '@cavuno/board';
66
57
 
67
58
  try {
68
- await board.jobs.list({ limit: 20 });
69
- } catch (err) {
70
- if (isBoardPasswordRequired(err)) {
71
- await board.password.verify(userEnteredPassword); // stores the grant
72
- await board.jobs.list({ limit: 20 }); // now passes the wall
73
- } else {
74
- throw err;
75
- }
59
+ return await board.jobs.list({ limit: 20 });
60
+ } catch (error) {
61
+ if (!isBoardPasswordRequired(error)) throw error;
62
+ await board.password.verify(userEnteredPassword);
63
+ return board.jobs.list({ limit: 20 });
76
64
  }
77
65
  ```
78
66
 
79
- The grant is identical for every visitor of the board and is not a user session. On SSR (`nostore`), pass it per call as `{ headers: { 'x-board-access': grant } }` instead of relying on stored state.
67
+ The grant is board access, not a user session. Server-rendered apps keep it in the app-owned grant cookie and pass it per request; `cavuno-board-server-sessions` defines that cookie and redirect contract. A fresh `board_password_required` response means the grant expired or the password rotated, so return to the challenge flow.
68
+
69
+ **Complete when:** a valid password unlocks one retry, an invalid password renders its typed error, and a stale server grant is cleared before rechallenge.
80
70
 
81
- ## Checklist
71
+ ## Cavuno SDK reference
82
72
 
83
- - [ ] Failures branched via guards, never message string-matching.
84
- - [ ] `404` → handled not-found path (not a crash).
85
- - [ ] `err.requestId` logged for support.
86
- - [ ] Password-gated boards call `password.verify()` on `isBoardPasswordRequired`.
73
+ For setup and API details beyond this workflow, use the [Cavuno Board SDK documentation](https://cavuno.com/docs/sdk).