@cavuno/board 1.2.0 → 1.3.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 +207 -7
- package/dist/index.d.ts +207 -7
- package/dist/index.js +164 -7
- package/dist/index.mjs +164 -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
package/dist/index.mjs
CHANGED
|
@@ -27,6 +27,9 @@ function isNotFound(e) {
|
|
|
27
27
|
function isUnauthorized(e) {
|
|
28
28
|
return isBoardApiError(e) && e.status === 401;
|
|
29
29
|
}
|
|
30
|
+
function isBoardPasswordRequired(e) {
|
|
31
|
+
return isBoardApiError(e) && e.status === 401 && e.code === "board_password_required";
|
|
32
|
+
}
|
|
30
33
|
function isForbidden(e) {
|
|
31
34
|
return isBoardApiError(e) && e.status === 403;
|
|
32
35
|
}
|
|
@@ -61,6 +64,7 @@ function toSearchParams(query) {
|
|
|
61
64
|
// src/storage.ts
|
|
62
65
|
var ACCESS_TOKEN_KEY = "cavuno_board_access_token";
|
|
63
66
|
var REFRESH_TOKEN_KEY = "cavuno_board_refresh_token";
|
|
67
|
+
var BOARD_ACCESS_GRANT_KEY = "cavuno_board_access_grant";
|
|
64
68
|
function isBrowser() {
|
|
65
69
|
return typeof globalThis.document !== "undefined";
|
|
66
70
|
}
|
|
@@ -105,7 +109,7 @@ async function clearSession(storage) {
|
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
// src/version.ts
|
|
108
|
-
var SDK_VERSION = "1.
|
|
112
|
+
var SDK_VERSION = "1.3.0";
|
|
109
113
|
|
|
110
114
|
// src/client.ts
|
|
111
115
|
function isRawBody(body) {
|
|
@@ -147,12 +151,14 @@ var BoardClient = class {
|
|
|
147
151
|
}
|
|
148
152
|
const token = await this.storage.getItem(ACCESS_TOKEN_KEY);
|
|
149
153
|
if (token !== null) headers.set("authorization", `Bearer ${token}`);
|
|
154
|
+
const grant = await this.storage.getItem(BOARD_ACCESS_GRANT_KEY);
|
|
155
|
+
if (grant !== null) headers.set("x-board-access", grant);
|
|
150
156
|
if (callHeaders) {
|
|
151
157
|
new Headers(callHeaders).forEach((value, key) => {
|
|
152
158
|
headers.set(key, value);
|
|
153
159
|
});
|
|
154
160
|
}
|
|
155
|
-
if ((method !== "GET" || headers.has("authorization")) && !headers.has("x-cavuno-sdk")) {
|
|
161
|
+
if ((method !== "GET" || headers.has("authorization") || headers.has("x-board-access")) && !headers.has("x-cavuno-sdk")) {
|
|
156
162
|
headers.set("x-cavuno-sdk", `board@${SDK_VERSION}`);
|
|
157
163
|
}
|
|
158
164
|
let req = {
|
|
@@ -163,7 +169,8 @@ var BoardClient = class {
|
|
|
163
169
|
this.options.logger?.debug(`${method} ${req.url}`);
|
|
164
170
|
const res = await globalThis.fetch(req.url, req.init);
|
|
165
171
|
this.options.logger?.debug(`${res.status} ${method} ${req.url}`);
|
|
166
|
-
if (this.options.onResponse)
|
|
172
|
+
if (this.options.onResponse)
|
|
173
|
+
await this.options.onResponse(res.clone(), req);
|
|
167
174
|
if (res.status === 204) return void 0;
|
|
168
175
|
if (res.ok) return await res.json();
|
|
169
176
|
let parsed;
|
|
@@ -358,6 +365,30 @@ function blogNamespace(client) {
|
|
|
358
365
|
`/blog/posts/${encodeURIComponent(postSlug)}`,
|
|
359
366
|
{ ...options, query }
|
|
360
367
|
);
|
|
368
|
+
},
|
|
369
|
+
/**
|
|
370
|
+
* The previous (older) and next (newer) posts for prev/next navigation.
|
|
371
|
+
*
|
|
372
|
+
* @example
|
|
373
|
+
* const { previous, next } = await board.blog.posts.adjacent('hello-world');
|
|
374
|
+
*/
|
|
375
|
+
adjacent(postSlug, options) {
|
|
376
|
+
return client.fetch(
|
|
377
|
+
`/blog/posts/${encodeURIComponent(postSlug)}/adjacent`,
|
|
378
|
+
options
|
|
379
|
+
);
|
|
380
|
+
},
|
|
381
|
+
/**
|
|
382
|
+
* Posts most similar to one post (the related-posts rail).
|
|
383
|
+
*
|
|
384
|
+
* @example
|
|
385
|
+
* const { data } = await board.blog.posts.similar('hello-world', { limit: 6 });
|
|
386
|
+
*/
|
|
387
|
+
similar(postSlug, query, options) {
|
|
388
|
+
return client.fetch(
|
|
389
|
+
`/blog/posts/${encodeURIComponent(postSlug)}/similar`,
|
|
390
|
+
{ ...options, query }
|
|
391
|
+
);
|
|
361
392
|
}
|
|
362
393
|
},
|
|
363
394
|
tags: {
|
|
@@ -463,6 +494,96 @@ function companiesNamespace(client) {
|
|
|
463
494
|
};
|
|
464
495
|
}
|
|
465
496
|
|
|
497
|
+
// src/namespaces/embed.ts
|
|
498
|
+
function embedNamespace(client) {
|
|
499
|
+
return {
|
|
500
|
+
/**
|
|
501
|
+
* List published jobs for an embeddable widget — the same featured-ranked
|
|
502
|
+
* cards as `board.jobs.list`, but UNGATED: the candidate paywall never
|
|
503
|
+
* applies, so the full page is always returned and there is no
|
|
504
|
+
* `gatedCount`. Powers the public "Powered by Cavuno" embed. `limit`
|
|
505
|
+
* defaults to 8 and is clamped to a maximum of 50.
|
|
506
|
+
*
|
|
507
|
+
* @example
|
|
508
|
+
* const { data, nextCursor } = await board.embed.jobs({ q: 'chef', limit: 8 });
|
|
509
|
+
*/
|
|
510
|
+
jobs(query, options) {
|
|
511
|
+
return client.fetch("/embed/jobs", {
|
|
512
|
+
...options,
|
|
513
|
+
query
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// src/namespaces/job-alerts.ts
|
|
520
|
+
function jobAlertsNamespace(client) {
|
|
521
|
+
return {
|
|
522
|
+
/** Subscribe an email to job alerts. Sends a double-opt-in confirmation email. */
|
|
523
|
+
subscribe(input, options) {
|
|
524
|
+
return client.fetch("/job-alerts", {
|
|
525
|
+
...options,
|
|
526
|
+
method: "POST",
|
|
527
|
+
body: input
|
|
528
|
+
});
|
|
529
|
+
},
|
|
530
|
+
/** Complete double opt-in with the token from the confirmation email. */
|
|
531
|
+
confirm(input, options) {
|
|
532
|
+
return client.fetch("/job-alerts/confirm", {
|
|
533
|
+
...options,
|
|
534
|
+
method: "POST",
|
|
535
|
+
body: input
|
|
536
|
+
});
|
|
537
|
+
},
|
|
538
|
+
/** Re-send the confirmation email for an unconfirmed subscription. */
|
|
539
|
+
resendConfirmation(input, options) {
|
|
540
|
+
return client.fetch(
|
|
541
|
+
"/job-alerts/resend-confirmation",
|
|
542
|
+
{ ...options, method: "POST", body: input }
|
|
543
|
+
);
|
|
544
|
+
},
|
|
545
|
+
/** Read a subscription + its preferences for the manage page (HMAC token). */
|
|
546
|
+
manage(query, options) {
|
|
547
|
+
return client.fetch("/job-alerts/manage", {
|
|
548
|
+
...options,
|
|
549
|
+
query
|
|
550
|
+
});
|
|
551
|
+
},
|
|
552
|
+
/** Deactivate a subscription via the HMAC manage token. */
|
|
553
|
+
unsubscribe(input, options) {
|
|
554
|
+
return client.fetch("/job-alerts/unsubscribe", {
|
|
555
|
+
...options,
|
|
556
|
+
method: "POST",
|
|
557
|
+
body: input
|
|
558
|
+
});
|
|
559
|
+
},
|
|
560
|
+
/** Re-activate a previously unsubscribed subscription via the manage token. */
|
|
561
|
+
resubscribe(input, options) {
|
|
562
|
+
return client.fetch("/job-alerts/resubscribe", {
|
|
563
|
+
...options,
|
|
564
|
+
method: "POST",
|
|
565
|
+
body: input
|
|
566
|
+
});
|
|
567
|
+
},
|
|
568
|
+
/** Edit a preference's filters/frequency via the manage token. */
|
|
569
|
+
updatePreference(input, options) {
|
|
570
|
+
return client.fetch("/job-alerts/preferences", {
|
|
571
|
+
...options,
|
|
572
|
+
method: "POST",
|
|
573
|
+
body: input
|
|
574
|
+
});
|
|
575
|
+
},
|
|
576
|
+
/** Delete a preference via the manage token. */
|
|
577
|
+
deletePreference(input, options) {
|
|
578
|
+
return client.fetch("/job-alerts/preferences", {
|
|
579
|
+
...options,
|
|
580
|
+
method: "DELETE",
|
|
581
|
+
body: input
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
466
587
|
// src/namespaces/jobs.ts
|
|
467
588
|
function jobsNamespace(client) {
|
|
468
589
|
return {
|
|
@@ -587,6 +708,30 @@ function meNamespace(client) {
|
|
|
587
708
|
};
|
|
588
709
|
}
|
|
589
710
|
|
|
711
|
+
// src/namespaces/password.ts
|
|
712
|
+
function passwordNamespace(client) {
|
|
713
|
+
return {
|
|
714
|
+
/**
|
|
715
|
+
* Exchange a board password for an access grant and store it. After this
|
|
716
|
+
* resolves, every subsequent read auto-carries the grant as the
|
|
717
|
+
* `X-Board-Access` header — until the password rotates, after which reads
|
|
718
|
+
* fail with 401 `board_password_required` and you must `verify()` again
|
|
719
|
+
* (the SDK never auto-retries — verify is rate-limited — and never
|
|
720
|
+
* auto-clears; the host re-challenges). On the server (`nostore` storage)
|
|
721
|
+
* the grant is returned but not persisted; pass it per-call instead.
|
|
722
|
+
*/
|
|
723
|
+
async verify(password, options) {
|
|
724
|
+
const grant = await client.fetch("/password/verify", {
|
|
725
|
+
...options,
|
|
726
|
+
method: "POST",
|
|
727
|
+
body: { password }
|
|
728
|
+
});
|
|
729
|
+
await client.storage.setItem(BOARD_ACCESS_GRANT_KEY, grant.token);
|
|
730
|
+
return grant;
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
590
735
|
// src/namespaces/redirects.ts
|
|
591
736
|
function redirectsNamespace(client) {
|
|
592
737
|
return {
|
|
@@ -628,9 +773,16 @@ function taxonomyNamespace(client) {
|
|
|
628
773
|
skills: taxonomyResolver(client, "skills"),
|
|
629
774
|
places: {
|
|
630
775
|
...taxonomyResolver(client, "places"),
|
|
631
|
-
/**
|
|
632
|
-
|
|
633
|
-
|
|
776
|
+
/**
|
|
777
|
+
* Without `query`: list every place used by a published job, with its
|
|
778
|
+
* live job count (the locations directory). With `query.q` (≥2 chars):
|
|
779
|
+
* location autocomplete — the top name matches ranked.
|
|
780
|
+
*/
|
|
781
|
+
list(query, options) {
|
|
782
|
+
return client.fetch("/places", {
|
|
783
|
+
...options,
|
|
784
|
+
query
|
|
785
|
+
});
|
|
634
786
|
}
|
|
635
787
|
}
|
|
636
788
|
};
|
|
@@ -675,22 +827,27 @@ function createBoardClient(options) {
|
|
|
675
827
|
return client.fetch("/seo", options2);
|
|
676
828
|
},
|
|
677
829
|
jobs: jobsNamespace(client),
|
|
830
|
+
embed: embedNamespace(client),
|
|
678
831
|
companies: companiesNamespace(client),
|
|
679
832
|
blog: blogNamespace(client),
|
|
680
833
|
auth: authNamespace(client),
|
|
681
834
|
me: meNamespace(client),
|
|
835
|
+
password: passwordNamespace(client),
|
|
682
836
|
taxonomy: taxonomyNamespace(client),
|
|
683
|
-
redirects: redirectsNamespace(client)
|
|
837
|
+
redirects: redirectsNamespace(client),
|
|
838
|
+
jobAlerts: jobAlertsNamespace(client)
|
|
684
839
|
};
|
|
685
840
|
}
|
|
686
841
|
export {
|
|
687
842
|
ACCESS_TOKEN_KEY,
|
|
843
|
+
BOARD_ACCESS_GRANT_KEY,
|
|
688
844
|
BoardApiError,
|
|
689
845
|
BoardClient,
|
|
690
846
|
REFRESH_TOKEN_KEY,
|
|
691
847
|
SDK_VERSION,
|
|
692
848
|
createBoardClient,
|
|
693
849
|
isBoardApiError,
|
|
850
|
+
isBoardPasswordRequired,
|
|
694
851
|
isConflict,
|
|
695
852
|
isForbidden,
|
|
696
853
|
isNotFound,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill-corpus loader. Node-only (reads the shipped `skills/` directory from
|
|
3
|
+
* the installed package) — kept out of the isomorphic core and exposed via the
|
|
4
|
+
* `@cavuno/board/skills` subpath export. Both `npx @cavuno/board setup` and the
|
|
5
|
+
* in-admin sidekick (ADR-0033) read the corpus through here, so the two doors
|
|
6
|
+
* stay fed from one source.
|
|
7
|
+
*/
|
|
8
|
+
interface SkillManifestEntry {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
/** Path to the SKILL.md, relative to the package root. */
|
|
12
|
+
path: string;
|
|
13
|
+
/** Framework slug for flavor skills; `null` for framework-agnostic core skills. */
|
|
14
|
+
framework: string | null;
|
|
15
|
+
category: 'core' | 'flavor';
|
|
16
|
+
}
|
|
17
|
+
interface SkillManifest {
|
|
18
|
+
version: string;
|
|
19
|
+
skills: SkillManifestEntry[];
|
|
20
|
+
}
|
|
21
|
+
interface LoadedSkill extends SkillManifestEntry {
|
|
22
|
+
content: string;
|
|
23
|
+
}
|
|
24
|
+
interface SkillCorpus {
|
|
25
|
+
version: string;
|
|
26
|
+
skills: LoadedSkill[];
|
|
27
|
+
}
|
|
28
|
+
/** Resolve a package-root-relative path (e.g. a manifest `path`) to an absolute path. */
|
|
29
|
+
declare function resolveFromPackageRoot(relativePath: string): string;
|
|
30
|
+
declare function loadSkillManifest(): SkillManifest;
|
|
31
|
+
/**
|
|
32
|
+
* Load the full skill corpus — manifest metadata plus each skill's markdown
|
|
33
|
+
* `content`. The sidekick injects `content` into its agent context; an external
|
|
34
|
+
* Claude Code instead receives the files copied by the setup command.
|
|
35
|
+
*/
|
|
36
|
+
declare function loadSkillCorpus(): SkillCorpus;
|
|
37
|
+
|
|
38
|
+
export { type LoadedSkill, type SkillCorpus, type SkillManifest, type SkillManifestEntry, loadSkillCorpus, loadSkillManifest, resolveFromPackageRoot };
|
package/dist/skills.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill-corpus loader. Node-only (reads the shipped `skills/` directory from
|
|
3
|
+
* the installed package) — kept out of the isomorphic core and exposed via the
|
|
4
|
+
* `@cavuno/board/skills` subpath export. Both `npx @cavuno/board setup` and the
|
|
5
|
+
* in-admin sidekick (ADR-0033) read the corpus through here, so the two doors
|
|
6
|
+
* stay fed from one source.
|
|
7
|
+
*/
|
|
8
|
+
interface SkillManifestEntry {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
/** Path to the SKILL.md, relative to the package root. */
|
|
12
|
+
path: string;
|
|
13
|
+
/** Framework slug for flavor skills; `null` for framework-agnostic core skills. */
|
|
14
|
+
framework: string | null;
|
|
15
|
+
category: 'core' | 'flavor';
|
|
16
|
+
}
|
|
17
|
+
interface SkillManifest {
|
|
18
|
+
version: string;
|
|
19
|
+
skills: SkillManifestEntry[];
|
|
20
|
+
}
|
|
21
|
+
interface LoadedSkill extends SkillManifestEntry {
|
|
22
|
+
content: string;
|
|
23
|
+
}
|
|
24
|
+
interface SkillCorpus {
|
|
25
|
+
version: string;
|
|
26
|
+
skills: LoadedSkill[];
|
|
27
|
+
}
|
|
28
|
+
/** Resolve a package-root-relative path (e.g. a manifest `path`) to an absolute path. */
|
|
29
|
+
declare function resolveFromPackageRoot(relativePath: string): string;
|
|
30
|
+
declare function loadSkillManifest(): SkillManifest;
|
|
31
|
+
/**
|
|
32
|
+
* Load the full skill corpus — manifest metadata plus each skill's markdown
|
|
33
|
+
* `content`. The sidekick injects `content` into its agent context; an external
|
|
34
|
+
* Claude Code instead receives the files copied by the setup command.
|
|
35
|
+
*/
|
|
36
|
+
declare function loadSkillCorpus(): SkillCorpus;
|
|
37
|
+
|
|
38
|
+
export { type LoadedSkill, type SkillCorpus, type SkillManifest, type SkillManifestEntry, loadSkillCorpus, loadSkillManifest, resolveFromPackageRoot };
|
package/dist/skills.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/skills.ts
|
|
21
|
+
var skills_exports = {};
|
|
22
|
+
__export(skills_exports, {
|
|
23
|
+
loadSkillCorpus: () => loadSkillCorpus,
|
|
24
|
+
loadSkillManifest: () => loadSkillManifest,
|
|
25
|
+
resolveFromPackageRoot: () => resolveFromPackageRoot
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(skills_exports);
|
|
28
|
+
|
|
29
|
+
// ../../node_modules/.pnpm/tsup@8.5.1_jiti@2.7.0_postcss@8.5.14_tsx@4.21.0_typescript@5.9.3_yaml@2.8.4/node_modules/tsup/assets/cjs_shims.js
|
|
30
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
31
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
32
|
+
|
|
33
|
+
// src/skills.ts
|
|
34
|
+
var import_node_fs = require("fs");
|
|
35
|
+
var import_node_path = require("path");
|
|
36
|
+
var import_node_url = require("url");
|
|
37
|
+
function packageRoot() {
|
|
38
|
+
return (0, import_node_path.resolve)((0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl)), "..");
|
|
39
|
+
}
|
|
40
|
+
function resolveFromPackageRoot(relativePath) {
|
|
41
|
+
return (0, import_node_path.resolve)(packageRoot(), relativePath);
|
|
42
|
+
}
|
|
43
|
+
function loadSkillManifest() {
|
|
44
|
+
const manifestPath = resolveFromPackageRoot("skills/manifest.json");
|
|
45
|
+
return JSON.parse((0, import_node_fs.readFileSync)(manifestPath, "utf8"));
|
|
46
|
+
}
|
|
47
|
+
function loadSkillCorpus() {
|
|
48
|
+
const manifest = loadSkillManifest();
|
|
49
|
+
return {
|
|
50
|
+
version: manifest.version,
|
|
51
|
+
skills: manifest.skills.map((skill) => ({
|
|
52
|
+
...skill,
|
|
53
|
+
content: (0, import_node_fs.readFileSync)(resolveFromPackageRoot(skill.path), "utf8")
|
|
54
|
+
}))
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
58
|
+
0 && (module.exports = {
|
|
59
|
+
loadSkillCorpus,
|
|
60
|
+
loadSkillManifest,
|
|
61
|
+
resolveFromPackageRoot
|
|
62
|
+
});
|
package/dist/skills.mjs
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// src/skills.ts
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { dirname, resolve } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
function packageRoot() {
|
|
6
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
|
+
}
|
|
8
|
+
function resolveFromPackageRoot(relativePath) {
|
|
9
|
+
return resolve(packageRoot(), relativePath);
|
|
10
|
+
}
|
|
11
|
+
function loadSkillManifest() {
|
|
12
|
+
const manifestPath = resolveFromPackageRoot("skills/manifest.json");
|
|
13
|
+
return JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
14
|
+
}
|
|
15
|
+
function loadSkillCorpus() {
|
|
16
|
+
const manifest = loadSkillManifest();
|
|
17
|
+
return {
|
|
18
|
+
version: manifest.version,
|
|
19
|
+
skills: manifest.skills.map((skill) => ({
|
|
20
|
+
...skill,
|
|
21
|
+
content: readFileSync(resolveFromPackageRoot(skill.path), "utf8")
|
|
22
|
+
}))
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
loadSkillCorpus,
|
|
27
|
+
loadSkillManifest,
|
|
28
|
+
resolveFromPackageRoot
|
|
29
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cavuno/board",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Typed isomorphic client for the Cavuno Board API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
"main": "./dist/index.js",
|
|
16
16
|
"module": "./dist/index.mjs",
|
|
17
17
|
"types": "./dist/index.d.ts",
|
|
18
|
+
"bin": {
|
|
19
|
+
"cavuno-board": "./dist/bin.mjs"
|
|
20
|
+
},
|
|
18
21
|
"exports": {
|
|
19
22
|
".": {
|
|
20
23
|
"import": {
|
|
@@ -25,10 +28,22 @@
|
|
|
25
28
|
"types": "./dist/index.d.ts",
|
|
26
29
|
"default": "./dist/index.js"
|
|
27
30
|
}
|
|
28
|
-
}
|
|
31
|
+
},
|
|
32
|
+
"./skills": {
|
|
33
|
+
"import": {
|
|
34
|
+
"types": "./dist/skills.d.mts",
|
|
35
|
+
"default": "./dist/skills.mjs"
|
|
36
|
+
},
|
|
37
|
+
"require": {
|
|
38
|
+
"types": "./dist/skills.d.ts",
|
|
39
|
+
"default": "./dist/skills.js"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"./skills/*": "./skills/*"
|
|
29
43
|
},
|
|
30
44
|
"files": [
|
|
31
|
-
"dist"
|
|
45
|
+
"dist",
|
|
46
|
+
"skills"
|
|
32
47
|
],
|
|
33
48
|
"publishConfig": {
|
|
34
49
|
"access": "public"
|
|
@@ -36,15 +51,17 @@
|
|
|
36
51
|
"scripts": {
|
|
37
52
|
"build": "tsup",
|
|
38
53
|
"clean": "git clean -xdf .turbo dist node_modules",
|
|
39
|
-
"typecheck": "tsgo --noEmit",
|
|
54
|
+
"typecheck": "tsgo --noEmit && tsgo --noEmit -p tsconfig.node.json",
|
|
40
55
|
"test": "vitest run",
|
|
56
|
+
"gen:skills-manifest": "tsx scripts/generate-skills-manifest.ts",
|
|
41
57
|
"assert-publish-target": "node -e \"const p=require('./package.json'); if(p.name!=='@cavuno/board'){throw new Error('Refusing to publish: package.json name is '+p.name+', expected @cavuno/board')}; if(p.private){throw new Error('Refusing to publish: package.json has private:true')}\"",
|
|
42
|
-
"prepublishOnly": "pnpm run assert-publish-target && pnpm run build"
|
|
58
|
+
"prepublishOnly": "pnpm run assert-publish-target && pnpm run gen:skills-manifest && pnpm run build"
|
|
43
59
|
},
|
|
44
60
|
"devDependencies": {
|
|
45
61
|
"@kit/tsconfig": "workspace:*",
|
|
46
62
|
"@types/node": "catalog:",
|
|
47
63
|
"tsup": "^8.4.0",
|
|
64
|
+
"tsx": "^4.19.2",
|
|
48
65
|
"vitest": "^2.1.9"
|
|
49
66
|
}
|
|
50
67
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cavuno-board-auth
|
|
3
|
+
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.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Board-user authentication
|
|
7
|
+
|
|
8
|
+
Board users (candidates, employers) authenticate with a short-lived bearer access token plus a refresh token. The SDK manages the pair via pluggable async storage. There is exactly one auth mode — bearer JWT (no cookie/session mode).
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
|
|
12
|
+
- Sign-up / sign-in / sign-out for board users.
|
|
13
|
+
- Email verification and password reset flows.
|
|
14
|
+
- Wiring authenticated calls (`me`, saved jobs, applications).
|
|
15
|
+
|
|
16
|
+
## When not to use
|
|
17
|
+
|
|
18
|
+
- Anonymous reads (jobs/companies/blog) — no auth needed.
|
|
19
|
+
- Board-password gating — that's a separate grant; see `cavuno-board-errors`.
|
|
20
|
+
|
|
21
|
+
## Register and login
|
|
22
|
+
|
|
23
|
+
```ts snippet
|
|
24
|
+
await board.auth.register({
|
|
25
|
+
role: 'candidate',
|
|
26
|
+
method: 'emailpass',
|
|
27
|
+
email: 'ada@example.com',
|
|
28
|
+
password: 'a-strong-password',
|
|
29
|
+
displayName: 'Ada',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const session = await board.auth.login({
|
|
33
|
+
email: 'ada@example.com',
|
|
34
|
+
password: 'a-strong-password',
|
|
35
|
+
});
|
|
36
|
+
session.boardUser.email; // the signed-in user
|
|
37
|
+
session.accessToken; // bearer; never expose to the browser bundle on SSR
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`register`/`login`/`refresh` persist the returned token pair to storage; `logout` clears it. The SDK never navigates — your app owns redirects and verification UX.
|
|
41
|
+
|
|
42
|
+
## Storage modes
|
|
43
|
+
|
|
44
|
+
`auth.storage` is `'memory'` | `'nostore'` | a `CustomStorage`. Defaults: **`memory` in the browser, `nostore` on the server**. Browser login works out of the box; shared SSR instances stay stateless.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { createBoardClient } from '@cavuno/board';
|
|
48
|
+
|
|
49
|
+
// Browser/SPA: token held in memory on the instance.
|
|
50
|
+
const board = createBoardClient({
|
|
51
|
+
baseUrl: 'https://api.cavuno.com',
|
|
52
|
+
board: 'pk_a8f3...',
|
|
53
|
+
auth: { storage: 'memory' },
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
A `CustomStorage` implements async `getItem`/`setItem`/`removeItem` — back it with `localStorage`, IndexedDB, Redis, or your own store.
|
|
58
|
+
|
|
59
|
+
## No auto-refresh on 401 — handle it explicitly
|
|
60
|
+
|
|
61
|
+
An expired access token surfaces as a `BoardApiError` you detect with `isUnauthorized`. The SDK does **not** silently refresh — refresh tokens are single-use with atomic rotation, so safe refresh under concurrency needs a single-flight guard you own.
|
|
62
|
+
|
|
63
|
+
```ts snippet
|
|
64
|
+
import { isUnauthorized } from '@cavuno/board';
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
return await board.me.retrieve();
|
|
68
|
+
} catch (err) {
|
|
69
|
+
if (isUnauthorized(err)) {
|
|
70
|
+
await board.auth.refresh(); // reads the stored refresh token, rotates the pair
|
|
71
|
+
return await board.me.retrieve();
|
|
72
|
+
}
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Under concurrency, wrap `auth.refresh()` in a single-flight promise so parallel 401s trigger exactly one rotation (the reference flavor encodes this once — see `cavuno-board-tanstack-start`).
|
|
78
|
+
|
|
79
|
+
## refresh / logout token sourcing
|
|
80
|
+
|
|
81
|
+
Both accept an optional body `{ refreshToken }`. When omitted, the SDK reads the token from storage and throws if neither exists — so `nostore` (server) callers must pass it explicitly:
|
|
82
|
+
|
|
83
|
+
```ts snippet
|
|
84
|
+
await board.auth.refresh({ refreshToken }); // server: explicit
|
|
85
|
+
await board.auth.logout({ refreshToken }); // revokes server-side, clears storage
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Server-side pattern (keep tokens out of the browser)
|
|
89
|
+
|
|
90
|
+
On SSR, do not hold the session on a shared instance. Keep the token pair in an httpOnly cookie owned by your app and pass it per call:
|
|
91
|
+
|
|
92
|
+
```ts snippet
|
|
93
|
+
// `accessToken` comes from your httpOnly cookie, read in server code.
|
|
94
|
+
// Per-call options are the 2nd argument; the 1st is `query` (pass undefined).
|
|
95
|
+
const me = await board.me.retrieve(undefined, {
|
|
96
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Email verification & password reset
|
|
101
|
+
|
|
102
|
+
```ts snippet
|
|
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
|
+
```
|
|
107
|
+
|
|
108
|
+
## Checklist
|
|
109
|
+
|
|
110
|
+
- [ ] Bearer tokens never reach the browser bundle on SSR (httpOnly cookie + per-call header).
|
|
111
|
+
- [ ] 401s handled explicitly with `isUnauthorized` + `auth.refresh()`.
|
|
112
|
+
- [ ] Concurrent refresh guarded by single-flight.
|
|
113
|
+
- [ ] `nostore` callers pass `{ refreshToken }` to `refresh`/`logout`.
|