@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.
- package/CHANGELOG.md +22 -0
- package/README.md +12 -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 +58 -66
- package/skills/cavuno-board-api-client/SKILL.md +78 -0
- package/skills/cavuno-board-applications/SKILL.md +42 -52
- package/skills/cavuno-board-auth/SKILL.md +39 -60
- package/skills/cavuno-board-blog/SKILL.md +64 -49
- package/skills/cavuno-board-companies/SKILL.md +75 -53
- package/skills/cavuno-board-errors/SKILL.md +32 -45
- package/skills/cavuno-board-filters/SKILL.md +68 -75
- package/skills/cavuno-board-format/SKILL.md +65 -66
- package/skills/cavuno-board-i18n/SKILL.md +43 -68
- package/skills/cavuno-board-job-alerts/SKILL.md +72 -57
- package/skills/cavuno-board-jobs/SKILL.md +67 -55
- package/skills/cavuno-board-messaging/SKILL.md +57 -59
- package/skills/cavuno-board-paywall/SKILL.md +52 -54
- package/skills/cavuno-board-post-a-job/SKILL.md +122 -0
- package/skills/cavuno-board-salaries/SKILL.md +77 -50
- package/skills/cavuno-board-search-suggestions/SKILL.md +86 -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 +34 -161
- package/skills/cavuno-board-server-sessions/SKILL.md +147 -0
- package/skills/cavuno-board-setup/SKILL.md +46 -79
- package/skills/cavuno-board-sitemap/SKILL.md +78 -91
- package/skills/cavuno-board-smoke-test/SKILL.md +38 -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,44 +1,25 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cavuno-board-account
|
|
3
|
-
description: Candidate
|
|
3
|
+
description: Candidate self-service boundary with @cavuno/board. Use for account, profile, resume onboarding, avatar, experience, education, skills, languages, or notification preferences.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Candidate
|
|
6
|
+
# Candidate self-service boundary
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Signed-in candidate data lives under `board.me`. Browser calls use the bearer token in `auth.storage`; server calls pass it per request in `options.headers`. See `cavuno-board-auth` for cookie-based server sessions.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
The only anonymous method here is token-based email unsubscribe. Applications, employer companies, messaging, and alerts have their own skills.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
- Profile editor: bio/headline/handle, experience, education, skills, languages, avatar.
|
|
14
|
-
- Resume-driven onboarding (upload → parse → auto-populated profile).
|
|
15
|
-
- Email notification settings + the unsubscribe link in emails.
|
|
12
|
+
The host application owns forms, file pickers, and cookie plumbing; this SDK surface supplies data operations.
|
|
16
13
|
|
|
17
|
-
##
|
|
14
|
+
## Account and profile
|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
- Applications and saved jobs — `cavuno-board-applications`.
|
|
21
|
-
- The employer facet (`me.companies.*`), messaging (`me.conversations.*`), job alerts (`me.alerts.*`).
|
|
22
|
-
|
|
23
|
-
Out of scope — do not invent exports: the SDK ships no form components, file-picker UI, or upload widgets; the host app owns all UI and cookie plumbing.
|
|
24
|
-
|
|
25
|
-
## Account: retrieve and delete
|
|
16
|
+
Account deletion is a synchronous, irreversible cascade over the profile, collections, saved jobs, alerts, avatar, and resume. Obtain explicit confirmation before calling it.
|
|
26
17
|
|
|
27
18
|
```ts snippet
|
|
28
|
-
const me = await board.me.retrieve();
|
|
29
|
-
await board.me.delete();
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
`delete` removes the account and all dependent data (profile, collections, saved jobs, alerts, avatar/resume files). Gate it behind an explicit confirmation.
|
|
33
|
-
|
|
34
|
-
## Profile: retrieve, update, handle availability
|
|
19
|
+
const me = await board.me.retrieve();
|
|
20
|
+
await board.me.delete();
|
|
35
21
|
|
|
36
|
-
The profile is a singleton. `update` is a merge-patch — omitted fields stay unchanged.
|
|
37
|
-
|
|
38
|
-
```ts snippet
|
|
39
22
|
const profile = await board.me.profile.retrieve();
|
|
40
|
-
profile.handle; profile.headline; profile.jobSearchStatus;
|
|
41
|
-
|
|
42
23
|
await board.me.profile.update({
|
|
43
24
|
headline: 'Staff Engineer',
|
|
44
25
|
jobSearchStatus: 'open_to_offers',
|
|
@@ -48,33 +29,40 @@ await board.me.profile.update({
|
|
|
48
29
|
const { available } = await board.me.profile.handleAvailable('jane');
|
|
49
30
|
```
|
|
50
31
|
|
|
51
|
-
|
|
32
|
+
Profile updates are merge-patches. Handle availability is advisory—your current handle counts as available—and the write re-checks uniqueness.
|
|
52
33
|
|
|
53
|
-
## Experience and education
|
|
34
|
+
## Experience and education
|
|
54
35
|
|
|
55
|
-
Both collections are id-keyed CRUD
|
|
36
|
+
Both collections are id-keyed CRUD with merge-patch updates. Experience creation requires `title`, `companyName`, and `startDate`; education creation requires `institutionName`.
|
|
56
37
|
|
|
57
38
|
```ts snippet
|
|
58
|
-
const
|
|
59
|
-
const
|
|
39
|
+
const page = await board.me.profile.listExperience();
|
|
40
|
+
const experience = await board.me.profile.createExperience({
|
|
60
41
|
title: 'Staff Engineer',
|
|
61
42
|
companyName: 'Acme',
|
|
62
43
|
startDate: '2022-01',
|
|
63
44
|
});
|
|
64
|
-
await board.me.profile.updateExperience(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
45
|
+
await board.me.profile.updateExperience(experience.id, {
|
|
46
|
+
endDate: '2025-06',
|
|
47
|
+
});
|
|
48
|
+
await board.me.profile.deleteExperience(experience.id);
|
|
49
|
+
|
|
50
|
+
const education = await board.me.profile.createEducation({
|
|
51
|
+
institutionName: 'Example University',
|
|
52
|
+
});
|
|
53
|
+
await board.me.profile.listEducation();
|
|
54
|
+
await board.me.profile.updateEducation(education.id, {});
|
|
55
|
+
await board.me.profile.deleteEducation(education.id);
|
|
68
56
|
```
|
|
69
57
|
|
|
70
|
-
##
|
|
58
|
+
## Full-set fields
|
|
71
59
|
|
|
72
|
-
`updateSkills`
|
|
60
|
+
`updateSkills` and `updateLanguages` replace the whole ordered set. Read-modify-write any value the user intends to retain.
|
|
73
61
|
|
|
74
62
|
```ts snippet
|
|
75
63
|
const current = await board.me.profile.listSkills();
|
|
76
64
|
await board.me.profile.updateSkills({
|
|
77
|
-
skills: [...current.data.map((
|
|
65
|
+
skills: [...current.data.map((skill) => skill.name), 'TypeScript'],
|
|
78
66
|
});
|
|
79
67
|
|
|
80
68
|
await board.me.profile.updateLanguages({
|
|
@@ -82,53 +70,53 @@ await board.me.profile.updateLanguages({
|
|
|
82
70
|
});
|
|
83
71
|
```
|
|
84
72
|
|
|
85
|
-
Both return the
|
|
73
|
+
Both updates return the complete updated list.
|
|
86
74
|
|
|
87
|
-
##
|
|
75
|
+
## Files
|
|
88
76
|
|
|
89
|
-
|
|
77
|
+
The SDK builds multipart `FormData` with field `file` from a `Blob` or `File`. Avatar uploads accept JPEG, PNG, or WebP up to 5 MB.
|
|
90
78
|
|
|
91
79
|
```ts snippet
|
|
92
80
|
const { avatarUrl } = await board.me.profile.uploadAvatar(file);
|
|
93
81
|
```
|
|
94
82
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
`upload` starts an **async parse** that auto-populates the profile. It returns the resume with `parseStatus: 'parsing'` — poll `retrieve()` until `parsed` or `failed`, then re-read `profile.*` for the populated fields. Options: `keepResumeOnFile`, `importMode: 'append_only' | 'replace_all'`, `confirmReplaceAll`.
|
|
83
|
+
Resume upload starts an asynchronous parse that may populate the profile. Poll within a fixed budget, surface failure, and keep manual editing available if parsing runs long.
|
|
98
84
|
|
|
99
85
|
```ts snippet
|
|
100
|
-
let resume = await board.me.resume.upload(file, {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
86
|
+
let resume = await board.me.resume.upload(file, {
|
|
87
|
+
keepResumeOnFile: true,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const maxPolls = 30;
|
|
91
|
+
for (let poll = 0; resume.parseStatus === 'parsing' && poll < maxPolls; poll++) {
|
|
92
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
105
93
|
resume = await board.me.resume.retrieve();
|
|
106
94
|
}
|
|
95
|
+
|
|
107
96
|
if (resume.parseStatus === 'parsed') {
|
|
108
|
-
|
|
97
|
+
await board.me.profile.retrieve();
|
|
109
98
|
} else if (resume.parseStatus === 'failed') {
|
|
110
|
-
resume.parseFailureReason;
|
|
99
|
+
showParseFailure(resume.parseFailureReason);
|
|
111
100
|
} else {
|
|
112
|
-
|
|
113
|
-
// keep the manual profile editor usable — never block on the parse.
|
|
101
|
+
showDelayedParseState();
|
|
114
102
|
}
|
|
115
103
|
```
|
|
116
104
|
|
|
117
|
-
`
|
|
105
|
+
Upload options also include `importMode: 'append_only' | 'replace_all'` and `confirmReplaceAll`. `resume.file.url` is a short-lived signed URL. `parseStatus` is null before any parse. `board.me.resume.delete()` removes the stored file and keep-on-file consent while retaining imported profile fields.
|
|
118
106
|
|
|
119
|
-
## Notification preferences
|
|
107
|
+
## Notification preferences
|
|
120
108
|
|
|
121
|
-
|
|
109
|
+
Authenticated settings expose `messageEmails` and `applicationEmails`. Updating one channel returns the full set.
|
|
122
110
|
|
|
123
111
|
```ts snippet
|
|
124
|
-
const
|
|
112
|
+
const preferences = await board.me.notificationPreferences.retrieve();
|
|
125
113
|
await board.me.notificationPreferences.update({
|
|
126
114
|
channel: 'messageEmails',
|
|
127
115
|
subscribed: false,
|
|
128
116
|
});
|
|
129
117
|
```
|
|
130
118
|
|
|
131
|
-
|
|
119
|
+
Email links use an anonymous HMAC token as authorization. Read all three inputs from the link query:
|
|
132
120
|
|
|
133
121
|
```ts snippet
|
|
134
122
|
await board.me.notificationPreferences.unsubscribeWithToken({
|
|
@@ -138,10 +126,14 @@ await board.me.notificationPreferences.unsubscribeWithToken({
|
|
|
138
126
|
});
|
|
139
127
|
```
|
|
140
128
|
|
|
141
|
-
##
|
|
129
|
+
## Completion gate
|
|
130
|
+
|
|
131
|
+
- Profile update is visible after retrieval.
|
|
132
|
+
- Adding a skill preserves every existing skill.
|
|
133
|
+
- Uploaded `avatarUrl` renders.
|
|
134
|
+
- Resume parsing reaches `parsed`, reports `parseFailureReason`, or reaches the explicit delayed state without blocking editing.
|
|
135
|
+
- Anonymous unsubscribe works in a logged-out browser.
|
|
136
|
+
|
|
137
|
+
## Cavuno SDK reference
|
|
142
138
|
|
|
143
|
-
|
|
144
|
-
- [ ] Adding a skill preserves the existing ones (the full-replace round-trip is in place).
|
|
145
|
-
- [ ] After `uploadAvatar`, the returned `avatarUrl` renders.
|
|
146
|
-
- [ ] Resume upload reaches `parsed` (or surfaces `failed` + `parseFailureReason`), and the profile shows parsed data.
|
|
147
|
-
- [ ] The email unsubscribe link works in a logged-out browser.
|
|
139
|
+
For setup and API details beyond this workflow, use the [Cavuno Board SDK documentation](https://cavuno.com/docs/sdk).
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cavuno-board-api-client
|
|
3
|
+
description: Configure the @cavuno/board API client. Use when creating the shared client, adding global hooks or headers, passing framework fetch options, or calling an endpoint through the typed escape hatch.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Configure the Board API client
|
|
7
|
+
|
|
8
|
+
Create one client and keep its shared state board-scoped, not user-scoped.
|
|
9
|
+
|
|
10
|
+
## Create the shared client
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { createBoardClient } from '@cavuno/board';
|
|
14
|
+
|
|
15
|
+
export const board = createBoardClient({
|
|
16
|
+
board: process.env.PUBLIC_CAVUNO_BOARD!,
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The `board` option accepts a `pk_…` key, `boards_…` ID, or slug. Prefer the immutable, publishable `pk_…` key for deployed frontends. The client defaults to `https://api.cavuno.com`; use `baseUrl` only for a Cavuno-supplied alternate origin.
|
|
21
|
+
|
|
22
|
+
**Complete when:** the app imports one reused client and `board.context()` identifies the expected board.
|
|
23
|
+
|
|
24
|
+
## Configure the request pipeline
|
|
25
|
+
|
|
26
|
+
Every namespace uses the same pipeline: board base path, global headers, stored browser credentials, request/response hooks, and per-call options.
|
|
27
|
+
|
|
28
|
+
```ts no-check
|
|
29
|
+
const board = createBoardClient({
|
|
30
|
+
board: process.env.PUBLIC_CAVUNO_BOARD!,
|
|
31
|
+
globalHeaders: { 'Accept-Language': 'en' },
|
|
32
|
+
onRequest: async (request) => request,
|
|
33
|
+
onResponse: async (response, request) => {
|
|
34
|
+
console.info(request.url, response.status);
|
|
35
|
+
},
|
|
36
|
+
logger: console,
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Use `onRequest` to replace or adjust the request. Use `onResponse` for observation; it receives a response clone, so reading it leaves SDK parsing intact.
|
|
41
|
+
|
|
42
|
+
**Complete when:** cross-cutting behavior is expressed once in client configuration and a request proves the hook or header is active.
|
|
43
|
+
|
|
44
|
+
## Pass runtime state per call
|
|
45
|
+
|
|
46
|
+
Every method accepts a trailing `FetchOptions`: `RequestInit` without `body`, plus `query`. Fetch-native and framework-specific fields pass through unchanged.
|
|
47
|
+
|
|
48
|
+
```ts snippet
|
|
49
|
+
await board.jobs.list(
|
|
50
|
+
{ limit: 20 },
|
|
51
|
+
{ next: { revalidate: 60, tags: ['jobs'] } });
|
|
52
|
+
|
|
53
|
+
const controller = new AbortController();
|
|
54
|
+
await board.jobs.list(
|
|
55
|
+
{ limit: 20 },
|
|
56
|
+
{ cache: 'force-cache', signal: controller.signal });
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
A module-scoped client is safe for concurrent SSR requests when it uses the server default `nostore` and each request supplies its own bearer or grant headers. `cavuno-board-server-sessions` is the authority for that session pattern. Browser persistence belongs to `cavuno-board-auth`.
|
|
60
|
+
|
|
61
|
+
**Complete when:** a shared server client contains no per-user state and each authenticated request supplies its own headers.
|
|
62
|
+
|
|
63
|
+
## Use the escape hatch for an untyped endpoint
|
|
64
|
+
|
|
65
|
+
`board.client.fetch<T>(path, options)` keeps the full request pipeline while providing a response type locally.
|
|
66
|
+
|
|
67
|
+
```ts snippet
|
|
68
|
+
const data = await board.client.fetch<{
|
|
69
|
+
object: 'list';
|
|
70
|
+
data: unknown[];
|
|
71
|
+
}>('/some-new-endpoint', { query: { limit: 5 } });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Complete when:** the call uses a board-relative path, supplies an honest response type, and no parallel raw-fetch client was introduced.
|
|
75
|
+
|
|
76
|
+
## Cavuno SDK reference
|
|
77
|
+
|
|
78
|
+
For setup and API details beyond this workflow, use the [Cavuno Board SDK documentation](https://cavuno.com/docs/sdk).
|
|
@@ -1,110 +1,100 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cavuno-board-applications
|
|
3
|
-
description:
|
|
3
|
+
description: Candidate application handoff with @cavuno/board. Use for native or guest apply, application tracking, resume attachment, withdrawal, and saved jobs.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# Candidate application handoff
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Treat applying as a handoff: submission belongs to `board.jobs`; the resulting candidate record belongs to `board.me`. The `me.*` side requires a board-user bearer token. See `cavuno-board-auth` for authentication.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
External jobs expose `applicationUrl`; link to it instead of creating a native application. Employer pipeline work belongs to `board.me.companies.applicants.*`.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
- Attaching a resume to an application.
|
|
14
|
-
- "My applications" tracker: list, detail, edit facts, withdraw.
|
|
15
|
-
- Save/unsave-job buttons and the saved-jobs page.
|
|
12
|
+
The host application owns apply forms and file-pickers; the SDK owns submission data operations.
|
|
16
13
|
|
|
17
|
-
##
|
|
14
|
+
## Submit
|
|
18
15
|
|
|
19
|
-
-
|
|
20
|
-
- The employer's pipeline view of the same data (`me.companies.applicants.*`).
|
|
21
|
-
- Candidate profile/resume self-service — `cavuno-board-account`.
|
|
22
|
-
|
|
23
|
-
Out of scope — do not invent exports: the SDK provides no apply-form components, file-picker UI, or auth cookie plumbing — the host app owns those. There is also **no guest-claim method** on the SDK: after a guest signs up, their applications are associated via a server-driven magic-link email flow, not an SDK call.
|
|
24
|
-
|
|
25
|
-
## Submit an application
|
|
26
|
-
|
|
27
|
-
`jobs.apply` is optional-auth on one URL. Signed in, name/email derive from the candidate profile — send at most a `coverNote`. A guest supplies `name` + `email` (allowed only when the board permits applying without sign-up; otherwise the call fails). Idempotent — a repeat apply returns the existing application.
|
|
16
|
+
`jobs.apply` is optional-auth and idempotent. A signed-in candidate supplies at most `coverNote`; a guest supplies `name` and `email`, when the board permits guest applications.
|
|
28
17
|
|
|
29
18
|
```ts snippet
|
|
30
|
-
// Signed-in candidate:
|
|
31
19
|
const application = await board.jobs.apply('senior-chef', {
|
|
32
20
|
coverNote: 'Excited to cook here.',
|
|
33
21
|
});
|
|
34
22
|
|
|
35
|
-
|
|
36
|
-
const guestApp = await board.jobs.apply('senior-chef', {
|
|
23
|
+
const guestApplication = await board.jobs.apply('senior-chef', {
|
|
37
24
|
name: 'Ada Lovelace',
|
|
38
25
|
email: 'ada@example.com',
|
|
39
26
|
});
|
|
40
27
|
```
|
|
41
28
|
|
|
29
|
+
Guest applications are claimed through the server-driven magic-link flow; the SDK has no guest-claim method.
|
|
30
|
+
|
|
42
31
|
## Attach a resume
|
|
43
32
|
|
|
44
|
-
|
|
33
|
+
Pass the `Blob` or `File`; the SDK builds multipart `FormData`. A signed-in candidate targets their own application. A guest must pass the id returned by `apply`.
|
|
45
34
|
|
|
46
35
|
```ts snippet
|
|
47
|
-
// Signed-in:
|
|
48
36
|
await board.jobs.uploadApplicationResume('senior-chef', file);
|
|
49
|
-
// Guest:
|
|
50
37
|
await board.jobs.uploadApplicationResume('senior-chef', file, {
|
|
51
|
-
applicationId:
|
|
38
|
+
applicationId: guestApplication.id,
|
|
52
39
|
});
|
|
53
40
|
```
|
|
54
41
|
|
|
55
|
-
|
|
42
|
+
This step is complete when the returned application has `resumeFilename` set.
|
|
56
43
|
|
|
57
|
-
|
|
44
|
+
## Derive the apply-button state
|
|
45
|
+
|
|
46
|
+
`jobs.myApplication` returns the signed-in candidate's application and throws a 404 when none exists. Derive the UI from that result rather than a local submitted flag.
|
|
58
47
|
|
|
59
48
|
```ts snippet
|
|
60
49
|
import { isNotFound } from '@cavuno/board';
|
|
61
50
|
|
|
62
51
|
try {
|
|
63
52
|
const mine = await board.jobs.myApplication('senior-chef');
|
|
64
|
-
|
|
65
|
-
} catch (
|
|
66
|
-
if (!isNotFound(
|
|
67
|
-
|
|
53
|
+
renderApplicationStatus(mine.status);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (!isNotFound(error)) throw error;
|
|
56
|
+
renderApplyForm();
|
|
68
57
|
}
|
|
69
58
|
```
|
|
70
59
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
`Application.status` is a coarse candidate-facing state **derived from the employer's pipeline stage**: `'applied' | 'interviewing' | 'negotiation' | 'hired' | 'archived'`. It changes server-side as the employer moves the applicant — the surface is plain REST, so re-fetch to observe updates; nothing pushes to the client. `job` on the application is nullable (the job may have been removed).
|
|
60
|
+
`Application.status` is a polled candidate-facing projection of the employer stage: `'applied' | 'interviewing' | 'negotiation' | 'hired' | 'archived'`. Its `job` may be null after job removal.
|
|
74
61
|
|
|
75
|
-
##
|
|
62
|
+
## Track and manage applications
|
|
76
63
|
|
|
77
64
|
```ts snippet
|
|
78
|
-
const
|
|
79
|
-
const
|
|
65
|
+
const page = await board.me.applications.list({ limit: 20 }); // newest first
|
|
66
|
+
const application = await board.me.applications.retrieve(applicationId);
|
|
80
67
|
|
|
81
|
-
// Merge-patch the candidate-facing facts — only while still editable:
|
|
82
68
|
await board.me.applications.updateFacts(applicationId, {
|
|
83
69
|
coverNote: 'Updated after our call.',
|
|
84
70
|
});
|
|
85
71
|
|
|
86
|
-
await board.me.applications.withdraw(applicationId);
|
|
72
|
+
await board.me.applications.withdraw(applicationId);
|
|
87
73
|
```
|
|
88
74
|
|
|
89
|
-
`updateFacts`
|
|
75
|
+
`updateFacts` merge-patches `candidateName`, `candidateEmail`, `candidateHeadline`, `candidateLocation`, and `coverNote`. `withdraw` permanently deletes the application, so obtain explicit confirmation first.
|
|
90
76
|
|
|
91
|
-
##
|
|
77
|
+
## Save jobs
|
|
92
78
|
|
|
93
|
-
|
|
79
|
+
Saved rows embed the full `PublicJob`. `save` converges on the existing row and `unsave` is idempotent.
|
|
94
80
|
|
|
95
81
|
```ts snippet
|
|
96
82
|
const saved = await board.me.savedJobs.list({ limit: 20 });
|
|
97
|
-
saved.data[0]?.job.title;
|
|
83
|
+
saved.data[0]?.job.title;
|
|
98
84
|
|
|
99
|
-
await board.me.savedJobs.save({ jobId: job.id });
|
|
100
|
-
await board.me.savedJobs.unsave(job.id);
|
|
85
|
+
await board.me.savedJobs.save({ jobId: job.id });
|
|
86
|
+
await board.me.savedJobs.unsave(job.id);
|
|
101
87
|
```
|
|
102
88
|
|
|
103
|
-
##
|
|
89
|
+
## Completion gate
|
|
90
|
+
|
|
91
|
+
- Signed-in submission omits name and email; guest submission includes both.
|
|
92
|
+
- Repeating `apply` returns the same application id.
|
|
93
|
+
- Resume upload returns an application with `resumeFilename`.
|
|
94
|
+
- `myApplication` plus `isNotFound` drives the apply-button state.
|
|
95
|
+
- Re-fetching after withdrawal removes the application.
|
|
96
|
+
- Repeating `unsave` leaves the saved list consistent.
|
|
97
|
+
|
|
98
|
+
## Cavuno SDK reference
|
|
104
99
|
|
|
105
|
-
|
|
106
|
-
- [ ] A second apply to the same job returns the same application id (no duplicate).
|
|
107
|
-
- [ ] After `uploadApplicationResume`, the application's `resumeFilename` is set.
|
|
108
|
-
- [ ] The apply button flips state via `myApplication` + `isNotFound`, not a local flag.
|
|
109
|
-
- [ ] Withdraw removes the row from `me.applications.list` on re-fetch.
|
|
110
|
-
- [ ] Unsave leaves the saved-jobs list consistent even when clicked twice.
|
|
100
|
+
For setup and API details beyond this workflow, use the [Cavuno Board SDK documentation](https://cavuno.com/docs/sdk).
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cavuno-board-auth
|
|
3
|
-
description: Authenticate board users
|
|
3
|
+
description: Authenticate Cavuno board users. Use for registration, login, logout, token refresh, email verification, password recovery, magic links, OAuth, or authenticated board-user calls.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# Authenticate board users
|
|
7
7
|
|
|
8
|
-
Board users
|
|
8
|
+
Board users authenticate with a short-lived bearer access token and a single-use refresh token. The SDK persists returned token pairs through its configured async storage and leaves navigation to the app.
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## 1. Choose the storage boundary
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
- Email verification and password reset flows.
|
|
14
|
-
- Wiring authenticated calls (`me`, saved jobs, applications).
|
|
12
|
+
`auth.storage` accepts `'memory'`, `'local'`, `'session'`, `'nostore'`, or a `CustomStorage` with async `getItem`, `setItem`, and `removeItem` methods.
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
- Browsers default to `'memory'`; choose `'local'` for persistence across tabs and reloads or `'session'` for tab-scoped persistence.
|
|
15
|
+
- Servers default to `'nostore'`. Keep tokens in the app's httpOnly cookie and follow `cavuno-board-server-sessions` for every SSR session and refresh rule.
|
|
16
|
+
- A custom store is appropriate when the app already owns a user-scoped persistence boundary.
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
- Board-password gating — that's a separate grant; see `cavuno-board-errors`.
|
|
18
|
+
`'local'` and `'session'` are browser runtimes; constructing them off-browser fails immediately.
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
**Complete when:** the selected store matches the runtime and a shared server client remains `nostore`.
|
|
21
|
+
|
|
22
|
+
## 2. Establish the session
|
|
22
23
|
|
|
23
24
|
```ts snippet
|
|
24
25
|
await board.auth.register({
|
|
@@ -33,81 +34,59 @@ const session = await board.auth.login({
|
|
|
33
34
|
email: 'ada@example.com',
|
|
34
35
|
password: 'a-strong-password',
|
|
35
36
|
});
|
|
36
|
-
session.boardUser.email; // the signed-in user
|
|
37
|
-
session.accessToken; // bearer; never expose to the browser bundle on SSR
|
|
38
37
|
```
|
|
39
38
|
|
|
40
|
-
`register
|
|
41
|
-
|
|
42
|
-
## Storage modes
|
|
43
|
-
|
|
44
|
-
`auth.storage` is `'memory'` | `'nostore'` | `'local'` | `'session'` | a `CustomStorage`. Defaults: **`memory` in the browser, `nostore` on the server**. Browser login works out of the box; shared SSR instances stay stateless.
|
|
39
|
+
`register`, `login`, `consumeMagicLink`, `exchangeOAuth`, and `refresh` persist the returned session. Use the returned `boardUser` for identity and verification state.
|
|
45
40
|
|
|
46
|
-
|
|
47
|
-
import { createBoardClient } from '@cavuno/board';
|
|
48
|
-
|
|
49
|
-
// Browser/SPA: 'local' persists the pair across tabs + reloads via
|
|
50
|
-
// localStorage; 'session' scopes it to the tab; 'memory' drops it on reload.
|
|
51
|
-
const board = createBoardClient({
|
|
52
|
-
board: 'pk_a8f3...',
|
|
53
|
-
auth: { storage: 'local' },
|
|
54
|
-
});
|
|
55
|
-
```
|
|
41
|
+
For alternate entry points, use `requestMagicLink` / `consumeMagicLink` or the OAuth authorization and exchange methods exposed under `board.auth`.
|
|
56
42
|
|
|
57
|
-
|
|
43
|
+
**Complete when:** the chosen entry flow returns a session and `board.me.retrieve()` resolves as that board user.
|
|
58
44
|
|
|
59
|
-
##
|
|
45
|
+
## 3. Handle expiry explicitly
|
|
60
46
|
|
|
61
|
-
|
|
47
|
+
The SDK surfaces an expired access token as `BoardApiError`; it does not refresh and replay a failed request automatically. In a browser app, classify the failure with `isUnauthorized`, serialize refresh through one in-flight promise, then retry the original operation once with the rotated session.
|
|
62
48
|
|
|
63
49
|
```ts snippet
|
|
64
50
|
import { isUnauthorized } from '@cavuno/board';
|
|
65
51
|
|
|
66
52
|
try {
|
|
67
53
|
return await board.me.retrieve();
|
|
68
|
-
} catch (
|
|
69
|
-
if (isUnauthorized(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
throw err;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (!isUnauthorized(error)) throw error;
|
|
56
|
+
await refreshOnce();
|
|
57
|
+
return board.me.retrieve();
|
|
74
58
|
}
|
|
75
59
|
```
|
|
76
60
|
|
|
77
|
-
|
|
61
|
+
On SSR, use the module-scoped `createSessionRefresher` pattern in `cavuno-board-server-sessions`; it is the single source of truth for concurrency, cookies, retry limits, and per-call authorization headers.
|
|
78
62
|
|
|
79
|
-
|
|
63
|
+
**Complete when:** concurrent expiry paths share one rotation and the original operation is attempted at most once after a successful refresh.
|
|
80
64
|
|
|
81
|
-
|
|
65
|
+
## 4. Refresh and logout from the right source
|
|
66
|
+
|
|
67
|
+
`refresh()` and `logout()` read the refresh token from configured storage when their body is omitted. A `nostore` caller passes it explicitly:
|
|
82
68
|
|
|
83
69
|
```ts snippet
|
|
84
|
-
await board.auth.refresh({ refreshToken });
|
|
85
|
-
await board.auth.logout({ refreshToken });
|
|
70
|
+
await board.auth.refresh({ refreshToken });
|
|
71
|
+
await board.auth.logout({ refreshToken });
|
|
86
72
|
```
|
|
87
73
|
|
|
88
|
-
|
|
74
|
+
Successful logout revokes the refresh token and clears SDK storage. A failed logout request retains stored tokens so the app can retry revocation or deliberately choose a local-only sign-out.
|
|
75
|
+
|
|
76
|
+
**Complete when:** logout revokes the server session, clears the app-owned cookie or browser store, and an authenticated read is handled as signed out.
|
|
89
77
|
|
|
90
|
-
|
|
78
|
+
## Verification and recovery
|
|
91
79
|
|
|
92
80
|
```ts snippet
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
headers: { authorization: `Bearer ${accessToken}` },
|
|
97
|
-
});
|
|
81
|
+
await board.auth.verifyEmail({ token });
|
|
82
|
+
await board.auth.forgotPassword({ email: 'ada@example.com' });
|
|
83
|
+
await board.auth.resetPassword({ token, password: 'a-new-password' });
|
|
98
84
|
```
|
|
99
85
|
|
|
100
|
-
|
|
86
|
+
The signed-in OTP verification, resend, magic-link, and OAuth branches use their corresponding `board.auth` methods. Password-reset requests preserve account privacy; a successful single-use reset invalidates existing sessions.
|
|
101
87
|
|
|
102
|
-
|
|
103
|
-
await board.auth.verifyEmail({ token }); // token from the email link
|
|
104
|
-
await board.auth.forgotPassword({ email }); // sends the reset email
|
|
105
|
-
await board.auth.resetPassword({ token, password }); // token from the email link
|
|
106
|
-
```
|
|
88
|
+
**Complete when:** each implemented auth route exercises its success state and typed failure state, with bearer tokens absent from server-rendered browser payloads.
|
|
107
89
|
|
|
108
|
-
##
|
|
90
|
+
## Cavuno SDK reference
|
|
109
91
|
|
|
110
|
-
|
|
111
|
-
- [ ] 401s handled explicitly with `isUnauthorized` + `auth.refresh()`.
|
|
112
|
-
- [ ] Concurrent refresh guarded by single-flight.
|
|
113
|
-
- [ ] `nostore` callers pass `{ refreshToken }` to `refresh`/`logout`.
|
|
92
|
+
For setup and API details beyond this workflow, use the [Cavuno Board SDK documentation](https://cavuno.com/docs/sdk).
|