@cavuno/board 2.0.5 → 3.0.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 +16 -0
- package/README.md +1 -3
- package/dist/{_spec-BKtVFcV9.d.mts → _spec-nkWAraS0.d.mts} +4 -18
- package/dist/{_spec-BKtVFcV9.d.ts → _spec-nkWAraS0.d.ts} +4 -18
- package/dist/bin.mjs +40 -57
- package/dist/{board-3Xj_Y-Tx.d.ts → board-NE98PlwN.d.ts} +2 -3
- package/dist/{board-pNN710Zc.d.mts → board-muu8BI8P.d.mts} +2 -3
- package/dist/doctor.js +21 -47
- package/dist/doctor.mjs +24 -50
- package/dist/filters.d.mts +2 -2
- package/dist/filters.d.ts +2 -2
- package/dist/format.d.mts +3 -3
- package/dist/format.d.ts +3 -3
- package/dist/index.d.mts +11 -27
- package/dist/index.d.ts +11 -27
- package/dist/index.js +3 -3
- package/dist/index.mjs +3 -3
- package/dist/{jobs-C4qRfCdP.d.ts → jobs-BiEvL9d7.d.ts} +1 -1
- package/dist/{jobs-k84C0v_8.d.mts → jobs-lVvKCBCU.d.mts} +1 -1
- package/dist/{salaries--J5mTylN.d.ts → salaries-BahBpxWT.d.ts} +2 -2
- package/dist/{salaries-C9YLO5NG.d.mts → salaries-ChSqDBAM.d.mts} +2 -2
- package/dist/{search-CE_qQiM7.d.mts → search-CCyFWPi3.d.mts} +1 -1
- package/dist/{search-CLRQ_El9.d.ts → search-CyRvVMUp.d.ts} +1 -1
- package/dist/seo.d.mts +4 -4
- package/dist/seo.d.ts +4 -4
- package/dist/server.d.mts +5 -5
- package/dist/server.d.ts +5 -5
- package/dist/sitemap.d.mts +5 -5
- package/dist/sitemap.d.ts +5 -5
- package/dist/skills.d.mts +15 -4
- package/dist/skills.d.ts +15 -4
- package/dist/skills.js +30 -7
- package/dist/skills.mjs +30 -8
- package/dist/suggest.d.mts +2 -2
- package/dist/suggest.d.ts +2 -2
- package/package.json +1 -11
- package/skills/cavuno-board-account/SKILL.md +54 -66
- package/skills/cavuno-board-api-client/SKILL.md +74 -0
- package/skills/cavuno-board-applications/SKILL.md +38 -52
- package/skills/cavuno-board-auth/SKILL.md +36 -61
- package/skills/cavuno-board-blog/SKILL.md +60 -49
- package/skills/cavuno-board-companies/SKILL.md +71 -53
- package/skills/cavuno-board-errors/SKILL.md +29 -46
- package/skills/cavuno-board-filters/SKILL.md +64 -75
- package/skills/cavuno-board-format/SKILL.md +63 -68
- package/skills/cavuno-board-i18n/SKILL.md +33 -62
- package/skills/cavuno-board-job-alerts/SKILL.md +68 -57
- package/skills/cavuno-board-jobs/SKILL.md +63 -55
- package/skills/cavuno-board-messaging/SKILL.md +53 -59
- package/skills/cavuno-board-paywall/SKILL.md +48 -54
- package/skills/cavuno-board-post-a-job/SKILL.md +118 -0
- package/skills/cavuno-board-salaries/SKILL.md +73 -50
- package/skills/cavuno-board-search-suggestions/SKILL.md +82 -0
- package/skills/cavuno-board-seo/BLOG.md +40 -0
- package/skills/cavuno-board-seo/JOB_AND_BREADCRUMBS.md +49 -0
- package/skills/cavuno-board-seo/LISTING.md +36 -0
- package/skills/cavuno-board-seo/SALARY.md +61 -0
- package/skills/cavuno-board-seo/SKILL.md +32 -163
- package/skills/cavuno-board-server-sessions/SKILL.md +143 -0
- package/skills/cavuno-board-setup/SKILL.md +43 -80
- package/skills/cavuno-board-sitemap/SKILL.md +76 -93
- package/skills/cavuno-board-smoke-test/SKILL.md +34 -69
- package/skills/manifest.json +43 -57
- package/dist/theme.d.mts +0 -115
- package/dist/theme.d.ts +0 -115
- package/dist/theme.js +0 -195
- package/dist/theme.mjs +0 -174
- package/skills/cavuno-board-client/SKILL.md +0 -89
- package/skills/cavuno-board-job-posting/SKILL.md +0 -116
- package/skills/cavuno-board-server/SKILL.md +0 -229
- package/skills/cavuno-board-suggest/SKILL.md +0 -119
- package/skills/cavuno-board-theme/SKILL.md +0 -57
- package/skills/flavors/tanstack-start/SKILL.md +0 -102
|
@@ -1,76 +1,80 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cavuno-board-blog
|
|
3
|
-
description:
|
|
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
|
|
6
|
+
# Blog
|
|
7
7
|
|
|
8
|
-
|
|
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
|
-
##
|
|
13
|
+
## Build archives
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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;
|
|
32
|
-
post.coverUrl;
|
|
24
|
+
post.customExcerpt;
|
|
25
|
+
post.coverUrl;
|
|
33
26
|
post.readingTimeMin;
|
|
34
|
-
post.authors;
|
|
35
|
-
post.tags;
|
|
27
|
+
post.authors;
|
|
28
|
+
post.tags;
|
|
36
29
|
}
|
|
37
30
|
const next = page.nextCursor
|
|
38
|
-
? await board.blog.posts.list({
|
|
39
|
-
|
|
31
|
+
? await board.blog.posts.list({
|
|
32
|
+
tagSlug: 'news',
|
|
33
|
+
limit: 12,
|
|
34
|
+
cursor: page.nextCursor,
|
|
35
|
+
})
|
|
36
|
+
: null;
|
|
40
37
|
```
|
|
41
38
|
|
|
42
|
-
|
|
39
|
+
Embedded author rows carry `id`, `name`, `slug`, `bio`, `avatarUrl`, and
|
|
40
|
+
social URLs. Embedded tag rows carry `id`, `name`, `slug`, and `description`.
|
|
43
41
|
|
|
44
|
-
|
|
42
|
+
## Render a post and canonicalize its slug
|
|
43
|
+
|
|
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
|
-
//
|
|
51
|
+
// Redirect to the post route at post.newSlug.
|
|
50
52
|
}
|
|
51
|
-
post.html;
|
|
52
|
-
post.seoTitle ?? post.title;
|
|
53
|
-
post.seoDescription ?? post.customExcerpt;
|
|
54
|
-
post.canonicalUrl;
|
|
55
|
-
post.ogImageUrl ?? post.coverUrl;
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
Anti-pattern: don't look for `html` on list items. Summaries never carry it; fetch `retrieve` for the post page only.
|
|
60
|
+
Fetch detail for the post page; summaries have no `html` field.
|
|
61
61
|
|
|
62
|
-
##
|
|
62
|
+
## Add post navigation
|
|
63
63
|
|
|
64
64
|
```ts snippet
|
|
65
65
|
const { previous, next } = await board.blog.posts.adjacent('hello-world');
|
|
66
|
-
// previous
|
|
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 });
|
|
68
|
+
const rail = await board.blog.posts.similar('hello-world', { limit: 6 });
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
`similar` accepts `limit` 1–20 and defaults to 6.
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
## Build tag and author pages
|
|
74
|
+
|
|
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,28 @@ const { data: authors } = await board.blog.authors.list();
|
|
|
79
83
|
const author = await board.blog.authors.retrieve('jane');
|
|
80
84
|
```
|
|
81
85
|
|
|
82
|
-
|
|
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
|
|
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
|
-
##
|
|
101
|
+
## Completion gate
|
|
102
|
+
|
|
103
|
+
Finish only after every applicable check passes:
|
|
94
104
|
|
|
95
|
-
-
|
|
96
|
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
99
|
-
-
|
|
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`.
|
|
@@ -1,99 +1,117 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cavuno-board-companies
|
|
3
|
-
description:
|
|
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
|
|
6
|
+
# Companies
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Use `PublicCompany` for list and search rows. Use `PublicCompanyDetail` from
|
|
9
|
+
`companies.retrieve` for a profile; it alone adds `markets`.
|
|
9
10
|
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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({
|
|
30
|
-
|
|
31
|
-
|
|
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; //
|
|
31
|
+
company.links.public; // null when the company has no slug
|
|
36
32
|
}
|
|
37
33
|
```
|
|
38
34
|
|
|
39
|
-
`companies.search` posts a `CompaniesSearchBody
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
//
|
|
57
|
+
// Return a 308 to the same archive at market.redirectTo.
|
|
54
58
|
}
|
|
55
59
|
```
|
|
56
60
|
|
|
57
|
-
|
|
61
|
+
## Render a profile
|
|
58
62
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
`
|
|
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');
|
|
65
|
-
company.markets;
|
|
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
|
-
|
|
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
|
-
`
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
##
|
|
81
|
+
## Render company salaries
|
|
78
82
|
|
|
79
|
-
`companies.salaries` is callable
|
|
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;
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
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
|
|
93
107
|
|
|
94
|
-
|
|
108
|
+
Finish only after every applicable check passes:
|
|
95
109
|
|
|
96
|
-
-
|
|
97
|
-
|
|
98
|
-
-
|
|
99
|
-
|
|
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.
|
|
@@ -1,86 +1,69 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cavuno-board-errors
|
|
3
|
-
description:
|
|
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
|
-
#
|
|
6
|
+
# Handle errors and access gates
|
|
7
7
|
|
|
8
|
-
Every SDK
|
|
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
|
-
##
|
|
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;
|
|
21
|
-
details?: unknown;
|
|
15
|
+
code: string;
|
|
16
|
+
details?: unknown;
|
|
22
17
|
requestId?: string;
|
|
23
|
-
raw: unknown;
|
|
18
|
+
raw: unknown;
|
|
24
19
|
}
|
|
25
20
|
```
|
|
26
21
|
|
|
27
|
-
|
|
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 (
|
|
43
|
-
if (isNotFound(
|
|
44
|
-
if (isUnauthorized(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (
|
|
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
|
-
|
|
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`
|
|
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
|
-
##
|
|
51
|
+
## Board-password challenge
|
|
61
52
|
|
|
62
|
-
|
|
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 (
|
|
70
|
-
if (isBoardPasswordRequired(
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
80
|
-
|
|
81
|
-
## Checklist
|
|
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.
|
|
82
68
|
|
|
83
|
-
|
|
84
|
-
- [ ] `404` → handled not-found path (not a crash).
|
|
85
|
-
- [ ] `err.requestId` logged for support.
|
|
86
|
-
- [ ] Password-gated boards call `password.verify()` on `isBoardPasswordRequired`.
|
|
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.
|