@cavuno/board 1.2.1 → 1.4.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/dist/bin.mjs +74 -0
- package/dist/index.d.mts +266 -5
- package/dist/index.d.ts +266 -5
- package/dist/index.js +214 -7
- package/dist/index.mjs +214 -7
- package/dist/skills.d.mts +38 -0
- package/dist/skills.d.ts +38 -0
- package/dist/skills.js +62 -0
- package/dist/skills.mjs +29 -0
- package/package.json +22 -5
- package/skills/cavuno-board-auth/SKILL.md +113 -0
- package/skills/cavuno-board-client/SKILL.md +93 -0
- package/skills/cavuno-board-errors/SKILL.md +86 -0
- package/skills/cavuno-board-jobs/SKILL.md +93 -0
- package/skills/cavuno-board-setup/SKILL.md +96 -0
- package/skills/flavors/tanstack-start/SKILL.md +102 -0
- package/skills/manifest.json +47 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cavuno-board-tanstack-start
|
|
3
|
+
description: TanStack-Start-on-Cloudflare-Workers reference wiring for a headless Cavuno board — SSR loaders calling @cavuno/board server-side, the session held in an __Host- httpOnly cookie owned by the app, a single-flight refresh helper, and FetchOptions cache passthrough on Workers.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Reference flavor: TanStack Start on Cloudflare Workers
|
|
7
|
+
|
|
8
|
+
This is the framework-specific layer for the reference starter (`wollemiahq/cavuno-board-starter`). The core skills (`cavuno-board-client`, `-jobs`, `-auth`, `-errors`) define the SDK surface; this skill shows how to wire it into TanStack Start on Workers. Read the core skills first.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
|
|
12
|
+
- The project depends on `@tanstack/react-start`.
|
|
13
|
+
- You need the SSR-loader + httpOnly-cookie + single-flight-refresh patterns.
|
|
14
|
+
|
|
15
|
+
## One shared, stateless client
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { createBoardClient } from '@cavuno/board';
|
|
19
|
+
|
|
20
|
+
// Module-scoped, no auth.storage → safe across concurrent Workers requests.
|
|
21
|
+
export const board = createBoardClient({
|
|
22
|
+
baseUrl: process.env.PUBLIC_CAVUNO_API_URL!,
|
|
23
|
+
board: process.env.PUBLIC_CAVUNO_BOARD!,
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Read in server loaders; cache via FetchOptions
|
|
28
|
+
|
|
29
|
+
Call the SDK only on the server (route loaders / server functions), never from the browser with a token. On Workers, pass cache directives straight through:
|
|
30
|
+
|
|
31
|
+
```ts no-check
|
|
32
|
+
import { createFileRoute } from '@tanstack/react-start';
|
|
33
|
+
import { board } from '~/lib/board';
|
|
34
|
+
|
|
35
|
+
export const Route = createFileRoute('/jobs')({
|
|
36
|
+
loader: async () => {
|
|
37
|
+
// `cf` rides straight to the Workers fetch — the SDK needs no framework knowledge.
|
|
38
|
+
return board.jobs.list({ limit: 20 }, { cf: { cacheTtl: 60 } } as never);
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Session in an `__Host-` httpOnly cookie owned by the app
|
|
44
|
+
|
|
45
|
+
The SDK never owns the cookie. Store the bearer pair in an `__Host-`-prefixed httpOnly cookie set by your server functions, read it on each request, and pass the token per call:
|
|
46
|
+
|
|
47
|
+
```ts no-check
|
|
48
|
+
import { board } from '~/lib/board';
|
|
49
|
+
import { readSessionCookie } from '~/lib/session.server';
|
|
50
|
+
|
|
51
|
+
export async function loadMe(request: Request) {
|
|
52
|
+
const { accessToken } = readSessionCookie(request);
|
|
53
|
+
// Pass the token via `options` (2nd arg). The 1st arg is `query`.
|
|
54
|
+
return board.me.retrieve(undefined, {
|
|
55
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Single-flight refresh
|
|
61
|
+
|
|
62
|
+
Refresh tokens are single-use; concurrent 401s must trigger exactly one rotation. Encode it once and reuse everywhere:
|
|
63
|
+
|
|
64
|
+
```ts no-check
|
|
65
|
+
import { isUnauthorized } from '@cavuno/board';
|
|
66
|
+
import { board } from '~/lib/board';
|
|
67
|
+
|
|
68
|
+
let inflight: Promise<void> | null = null;
|
|
69
|
+
|
|
70
|
+
// `getAccessToken`/`getRefreshToken` read your __Host- cookie (the SDK never
|
|
71
|
+
// touches it). On a 401 we refresh exactly once — concurrent callers await the
|
|
72
|
+
// same `inflight` promise — then retry `run` with the rotated token. Your
|
|
73
|
+
// refresh handler must write the new pair to the cookie so the retry reads it.
|
|
74
|
+
export async function withFreshSession<T>(
|
|
75
|
+
getAccessToken: () => string,
|
|
76
|
+
getRefreshToken: () => string,
|
|
77
|
+
run: (accessToken: string) => Promise<T>,
|
|
78
|
+
): Promise<T> {
|
|
79
|
+
try {
|
|
80
|
+
return await run(getAccessToken());
|
|
81
|
+
} catch (err) {
|
|
82
|
+
if (!isUnauthorized(err)) throw err;
|
|
83
|
+
inflight ??= board.auth
|
|
84
|
+
.refresh({ refreshToken: getRefreshToken() })
|
|
85
|
+
.then(() => undefined)
|
|
86
|
+
.finally(() => (inflight = null));
|
|
87
|
+
await inflight;
|
|
88
|
+
return run(getAccessToken()); // retry with the rotated token
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Canonical URLs
|
|
94
|
+
|
|
95
|
+
Mirror the hosted board's public URLs so indexed links survive a hosted → headless migration: job detail at `/companies/$companySlug/jobs/$jobSlug`, listings at `/jobs`, `/jobs/$keyword`, `/jobs/locations/$slug`.
|
|
96
|
+
|
|
97
|
+
## Checklist
|
|
98
|
+
|
|
99
|
+
- [ ] All SDK calls are server-side; no bearer token in the browser bundle.
|
|
100
|
+
- [ ] Session in an `__Host-` httpOnly cookie owned by the app, passed per call.
|
|
101
|
+
- [ ] Single-flight refresh wraps concurrent 401s.
|
|
102
|
+
- [ ] Public route paths mirror the hosted board's canonical URLs.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.4.0",
|
|
3
|
+
"skills": [
|
|
4
|
+
{
|
|
5
|
+
"name": "cavuno-board-auth",
|
|
6
|
+
"description": "Authenticate board users with the @cavuno/board SDK — register, login, refresh, logout, email verification and password reset. Covers bearer-JWT storage modes, the deliberate no-auto-refresh-on-401 rule (and single-flight handling), and the server-side httpOnly-cookie pattern that keeps tokens out of the browser.",
|
|
7
|
+
"path": "skills/cavuno-board-auth/SKILL.md",
|
|
8
|
+
"framework": null,
|
|
9
|
+
"category": "core"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"name": "cavuno-board-client",
|
|
13
|
+
"description": "Create and configure the @cavuno/board client — baseUrl and the pk_ board identifier, global headers, request/response hooks, per-call FetchOptions caching passthrough, the client.fetch escape hatch, and the rule that keeps one shared instance safe under SSR.",
|
|
14
|
+
"path": "skills/cavuno-board-client/SKILL.md",
|
|
15
|
+
"framework": null,
|
|
16
|
+
"category": "core"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "cavuno-board-errors",
|
|
20
|
+
"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).",
|
|
21
|
+
"path": "skills/cavuno-board-errors/SKILL.md",
|
|
22
|
+
"framework": null,
|
|
23
|
+
"category": "core"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "cavuno-board-jobs",
|
|
27
|
+
"description": "Browse, search, and render jobs with the @cavuno/board SDK — jobs.list, jobs.search, jobs.retrieve, jobs.similar. Covers the slim card vs full job shapes, storefront pagination (count/limit/offset + opaque cursor), filters, and the candidate-paywall gatedCount.",
|
|
28
|
+
"path": "skills/cavuno-board-jobs/SKILL.md",
|
|
29
|
+
"framework": null,
|
|
30
|
+
"category": "core"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "cavuno-board-setup",
|
|
34
|
+
"description": "End-to-end orchestrator for building a headless Cavuno job board with the @cavuno/board SDK. Start here after `npx @cavuno/board setup` copies the skills — detect the framework, wire the client, render board context, jobs browsing and detail, board-user auth and saved jobs, handle errors and access gating, then verify.",
|
|
35
|
+
"path": "skills/cavuno-board-setup/SKILL.md",
|
|
36
|
+
"framework": null,
|
|
37
|
+
"category": "core"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "cavuno-board-tanstack-start",
|
|
41
|
+
"description": "TanStack-Start-on-Cloudflare-Workers reference wiring for a headless Cavuno board — SSR loaders calling @cavuno/board server-side, the session held in an __Host- httpOnly cookie owned by the app, a single-flight refresh helper, and FetchOptions cache passthrough on Workers.",
|
|
42
|
+
"path": "skills/flavors/tanstack-start/SKILL.md",
|
|
43
|
+
"framework": "tanstack-start",
|
|
44
|
+
"category": "flavor"
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|