@cavuno/board 1.0.0 → 1.2.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/index.d.mts CHANGED
@@ -1,3 +1,244 @@
1
+ /**
2
+ * Stripe-shaped success envelopes (`01-conventions.md` §5.1). The
3
+ * server MAY add top-level fields; consumers MUST ignore unknown
4
+ * fields.
5
+ */
6
+ /**
7
+ * Medusa-style storefront pagination fields (ADR-0037 §7), populated only by
8
+ * the board jobs catalog reads (browse / search / company-jobs); absent on
9
+ * every other list/search. `nextCursor` is preserved alongside (offset-encoded).
10
+ */
11
+ interface StorefrontPagination {
12
+ /** Total matching results ("X jobs"). */
13
+ count?: number;
14
+ /** The page size used for this response. */
15
+ limit?: number;
16
+ /** Number of items skipped before this page. */
17
+ offset?: number;
18
+ /** Items hidden behind the candidate paywall for the current viewer; absent/0 when entitled. */
19
+ gatedCount?: number;
20
+ }
21
+ interface ListEnvelope<T> extends StorefrontPagination {
22
+ object: 'list';
23
+ url: string;
24
+ hasMore: boolean;
25
+ /** `null` when `hasMore` is false — always present, never undefined. */
26
+ nextCursor: string | null;
27
+ data: T[];
28
+ }
29
+ interface SearchEnvelope<T> extends StorefrontPagination {
30
+ object: 'search_result';
31
+ url: string;
32
+ hasMore: boolean;
33
+ nextCursor: string | null;
34
+ data: T[];
35
+ }
36
+
37
+ type RemoteOption = 'on_site' | 'hybrid' | 'remote';
38
+ type EmploymentType = 'full_time' | 'part_time' | 'contract' | 'internship' | 'temporary' | 'volunteer' | 'other';
39
+ type Seniority = 'entry_level' | 'associate' | 'mid_level' | 'senior' | 'lead' | 'principal' | 'director' | 'executive';
40
+ type EducationRequirement = 'high_school' | 'associate_degree' | 'bachelor_degree' | 'professional_certificate' | 'postgraduate_degree' | 'no_requirements';
41
+ interface OfficeLocation {
42
+ countryCode: string | null;
43
+ country: string | null;
44
+ locality: string | null;
45
+ city: string | null;
46
+ region: string | null;
47
+ regionCode: string | null;
48
+ postalCode: string | null;
49
+ displayName: string | null;
50
+ }
51
+ interface JobCompany {
52
+ id: string;
53
+ name: string | null;
54
+ slug: string | null;
55
+ logoUrl: string | null;
56
+ website: string | null;
57
+ }
58
+ interface RemotePermit {
59
+ type: string;
60
+ value: string;
61
+ }
62
+ interface RemoteTimezone {
63
+ type: string;
64
+ value: string;
65
+ plusMinus?: number;
66
+ }
67
+ interface PublicJob {
68
+ id: string;
69
+ object: 'public_job';
70
+ title: string;
71
+ slug: string | null;
72
+ status: string;
73
+ companyId: string | null;
74
+ employmentType: string | null;
75
+ remoteOption: string | null;
76
+ seniority: string | null;
77
+ salaryMin: number | null;
78
+ salaryMax: number | null;
79
+ salaryCurrency: string | null;
80
+ salaryTimeframe: string | null;
81
+ isFeatured: boolean;
82
+ publishedAt: string | null;
83
+ expiresAt: string | null;
84
+ createdAt: string;
85
+ updatedAt: string;
86
+ description: string | null;
87
+ applicationUrl: string | null;
88
+ /** Canonical hierarchical permit selection. */
89
+ remotePermits: RemotePermit[];
90
+ /** Read-only — derived from `remotePermits`. */
91
+ remoteWorldwide: boolean | null;
92
+ /** Canonical hierarchical timezone selection. */
93
+ remoteTimezones: RemoteTimezone[];
94
+ /** Read-only — derived from `remoteTimezones`. */
95
+ remoteAllowedTzOffsets: number[];
96
+ /** Read-only — derived from `remotePermits`. */
97
+ remoteWorkPermitCountryCodes: string[];
98
+ /** Read-only — derived from `remotePermits`. */
99
+ remoteWorkPermitSubdivisionCodes: string[];
100
+ remoteSponsorship: 'yes' | 'no' | 'unknown';
101
+ educationRequirements: EducationRequirement[];
102
+ experienceMonths: number | null;
103
+ experienceInPlaceOfEducation: boolean | null;
104
+ inOfficePeriod: 'per_week' | 'per_month' | 'per_year' | null;
105
+ inOfficeFrequency: number | null;
106
+ company: JobCompany | null;
107
+ officeLocations: OfficeLocation[];
108
+ /** Resolved taxonomy — same `{slug,name}` shape as `PublicJobCard`; names joined server-side. */
109
+ categories: Array<{
110
+ slug: string;
111
+ name: string;
112
+ }>;
113
+ skills: Array<{
114
+ slug: string;
115
+ name: string;
116
+ }>;
117
+ /**
118
+ * Place ancestor chain (country → region → city) for the breadcrumb. Each
119
+ * `slug` is the English SOURCE slug — the key `taxonomy.places.resolve()`
120
+ * accepts. On a localized board it may differ from the canonical
121
+ * board-language URL slug (the board router 308-redirects the source slug to
122
+ * the canonical one); link via `/jobs/locations/:slug`.
123
+ */
124
+ placeHierarchy: Array<{
125
+ slug: string;
126
+ name: string;
127
+ }>;
128
+ links: {
129
+ public: string | null;
130
+ };
131
+ }
132
+ /**
133
+ * The slim listing card (ADR-0037 §1) returned by `jobs.list` / `jobs.search`
134
+ * / `companies.listJobs`. Full enrichment (description, officeLocations, the
135
+ * structured remote blocks, …) lives on `jobs.retrieve` (`PublicJob`).
136
+ * Transcribed from `serializeJobCard`.
137
+ */
138
+ interface PublicJobCard {
139
+ id: string;
140
+ object: 'job_card';
141
+ slug: string;
142
+ title: string;
143
+ publishedAt: string | null;
144
+ employmentType: string | null;
145
+ remoteOption: string | null;
146
+ /** Remote-region label for remote jobs (e.g. "Worldwide"); `null` otherwise. */
147
+ remoteLocationLabel: string | null;
148
+ salaryMin: number | null;
149
+ salaryMax: number | null;
150
+ salaryCurrency: string | null;
151
+ salaryTimeframe: string | null;
152
+ isFeatured: boolean;
153
+ locationLabel: string | null;
154
+ company: {
155
+ slug: string;
156
+ name: string;
157
+ logoUrl: string | null;
158
+ } | null;
159
+ categories: Array<{
160
+ slug: string;
161
+ name: string;
162
+ }>;
163
+ skills: Array<{
164
+ slug: string;
165
+ name: string;
166
+ }>;
167
+ links: {
168
+ public: string | null;
169
+ };
170
+ /**
171
+ * Long-form description (HTML), or `null`. Present ONLY when the list/search
172
+ * was called with `fields: '+description'` (the RSS feed's sparse-fieldset
173
+ * opt-in); `undefined`/absent on the default slim card.
174
+ */
175
+ description?: string | null;
176
+ }
177
+ /** Derived category/skill suggestion on the jobs browse list (ADR-0037 §8). */
178
+ interface RelatedSearch {
179
+ type: 'category' | 'skill';
180
+ slug: string;
181
+ term: string;
182
+ count: number;
183
+ }
184
+ /** The browse list envelope — `PublicJobCard`s + storefront fields + `relatedSearches`. */
185
+ interface JobCardListEnvelope extends ListEnvelope<PublicJobCard> {
186
+ relatedSearches?: RelatedSearch[];
187
+ }
188
+ /** The search envelope — `PublicJobCard`s + storefront fields. */
189
+ type JobCardSearchEnvelope = SearchEnvelope<PublicJobCard>;
190
+ type JobsListQuery = {
191
+ cursor?: string;
192
+ /** 1–100. */
193
+ limit?: number;
194
+ /** Storefront page offset; takes precedence over `cursor`. `offset + limit` ≤ 10,000. */
195
+ offset?: number;
196
+ /** Single or repeated (up to 10) — repeated params are OR-matched. */
197
+ companyId?: string | string[];
198
+ remoteOption?: RemoteOption | RemoteOption[];
199
+ employmentType?: EmploymentType | EmploymentType[];
200
+ seniority?: Seniority | Seniority[];
201
+ /** Place slug for a geo radius search; unresolvable slugs are ignored. */
202
+ location?: string;
203
+ /** Radius in km around `location` (10–250; default 50). */
204
+ radius?: number;
205
+ /** Category slug seed (the `/jobs/[keyword]` page) — server resolves it to the English source name; unresolvable → 404. */
206
+ category?: string;
207
+ /** Skill slug seed (the `/jobs/skills/[skill]` page) — server-resolved; unresolvable → 404. */
208
+ skill?: string;
209
+ /** Sparse fieldset (Medusa-style `+field`). Only `'+description'` is supported — adds `description` to each card. */
210
+ fields?: string;
211
+ };
212
+ type JobsSimilarQuery = {
213
+ /** How many similar jobs to return (1–20; default 5). */
214
+ limit?: number;
215
+ };
216
+ interface JobsSearchBody {
217
+ /** Free-text query, up to 200 characters. */
218
+ query?: string;
219
+ filters?: {
220
+ /** Up to 10 values each. */
221
+ companyId?: string[];
222
+ remoteOption?: RemoteOption[];
223
+ employmentType?: EmploymentType[];
224
+ seniority?: Seniority[];
225
+ /** ISO 8601 datetime bounds. */
226
+ publishedAt?: {
227
+ gte?: string;
228
+ lte?: string;
229
+ };
230
+ /** Place slug for a geo radius search. */
231
+ location?: string;
232
+ /** Radius in km around `location` (10–250; default 50). */
233
+ radius?: number;
234
+ };
235
+ cursor?: string;
236
+ /** 1–100. */
237
+ limit?: number;
238
+ /** Storefront page offset; takes precedence over `cursor`. */
239
+ offset?: number;
240
+ }
241
+
1
242
  type Awaitable<T> = T | Promise<T>;
2
243
  /**
3
244
  * Async token storage. The SDK reads the access token from storage on
@@ -104,6 +345,27 @@ interface PublicBoard {
104
345
  theme: PublicBoardTheme | null;
105
346
  }
106
347
 
348
+ /**
349
+ * The public SEO-infra payload (`board.seo()`). The four values a headless
350
+ * frontend rebuilds `robots.txt` / `ads.txt` / `indexnow-key.txt` (+ the Google
351
+ * site-verification `<meta>`) from — byte-identically to the hosted board.
352
+ */
353
+ interface BoardSeo {
354
+ object: 'board_seo';
355
+ /** Verbatim `ads.txt` content, or `null` when not configured (the handler 404s). */
356
+ adsTxt: string | null;
357
+ /** IndexNow key-file content, or `null` when not configured (the handler 404s). */
358
+ indexNowKey: string | null;
359
+ /** Google site-verification token for the `<meta>` tag, or `null`. */
360
+ googleSiteVerification: string | null;
361
+ /**
362
+ * The board's canonical base URL (honours a configured custom domain). Build
363
+ * the `robots.txt` `Sitemap:` line (`${canonicalBase}/sitemap.xml`) and
364
+ * canonical links from it — NOT the request origin.
365
+ */
366
+ canonicalBase: string;
367
+ }
368
+
107
369
  /**
108
370
  * Error raised for every non-2xx Board API response.
109
371
  *
@@ -151,7 +413,7 @@ declare function isConflict(e: unknown): e is BoardApiError;
151
413
  * constant because the package is platform-neutral and cannot read
152
414
  * package.json at runtime.
153
415
  */
154
- declare const SDK_VERSION = "1.0.0";
416
+ declare const SDK_VERSION = "1.2.0";
155
417
 
156
418
  interface BoardUser {
157
419
  id: string;
@@ -202,6 +464,19 @@ interface ResetPasswordBody {
202
464
  password: string;
203
465
  }
204
466
 
467
+ /**
468
+ * The result of resolving a path against the board's configured redirects
469
+ * (`board.redirects.resolve()`). A headless frontend 308s to `target`, or 404s
470
+ * when it's `null`.
471
+ */
472
+ interface RedirectResolution {
473
+ object: 'redirect_resolution';
474
+ /** The path that was resolved. */
475
+ path: string;
476
+ /** The redirect target (board-relative), or `null` when no redirect matches. */
477
+ target: string | null;
478
+ }
479
+
205
480
  /** Author shape embedded on posts (no `object` discriminator). */
206
481
  interface BlogAuthorEmbed {
207
482
  id: string;
@@ -268,27 +543,6 @@ interface BlogSearchBody {
268
543
  limit?: number;
269
544
  }
270
545
 
271
- /**
272
- * Stripe-shaped success envelopes (`01-conventions.md` §5.1). The
273
- * server MAY add top-level fields; consumers MUST ignore unknown
274
- * fields.
275
- */
276
- interface ListEnvelope<T> {
277
- object: 'list';
278
- url: string;
279
- hasMore: boolean;
280
- /** `null` when `hasMore` is false — always present, never undefined. */
281
- nextCursor: string | null;
282
- data: T[];
283
- }
284
- interface SearchEnvelope<T> {
285
- object: 'search_result';
286
- url: string;
287
- hasMore: boolean;
288
- nextCursor: string | null;
289
- data: T[];
290
- }
291
-
292
546
  interface PublicCompany {
293
547
  id: string;
294
548
  object: 'public_company';
@@ -321,110 +575,6 @@ interface CompaniesSearchBody {
321
575
  limit?: number;
322
576
  }
323
577
 
324
- type RemoteOption = 'on_site' | 'hybrid' | 'remote';
325
- type EmploymentType = 'full_time' | 'part_time' | 'contract' | 'internship' | 'temporary' | 'volunteer' | 'other';
326
- type Seniority = 'entry_level' | 'associate' | 'mid_level' | 'senior' | 'lead' | 'principal' | 'director' | 'executive';
327
- type EducationRequirement = 'high_school' | 'associate_degree' | 'bachelor_degree' | 'professional_certificate' | 'postgraduate_degree' | 'no_requirements';
328
- interface OfficeLocation {
329
- countryCode: string | null;
330
- country: string | null;
331
- locality: string | null;
332
- city: string | null;
333
- region: string | null;
334
- regionCode: string | null;
335
- postalCode: string | null;
336
- displayName: string | null;
337
- }
338
- interface JobCompany {
339
- id: string;
340
- name: string | null;
341
- slug: string | null;
342
- logoUrl: string | null;
343
- website: string | null;
344
- }
345
- interface RemotePermit {
346
- type: string;
347
- value: string;
348
- }
349
- interface RemoteTimezone {
350
- type: string;
351
- value: string;
352
- plusMinus?: number;
353
- }
354
- interface PublicJob {
355
- id: string;
356
- object: 'public_job';
357
- title: string;
358
- slug: string | null;
359
- status: string;
360
- companyId: string | null;
361
- employmentType: string | null;
362
- remoteOption: string | null;
363
- seniority: string | null;
364
- salaryMin: number | null;
365
- salaryMax: number | null;
366
- salaryCurrency: string | null;
367
- salaryTimeframe: string | null;
368
- isFeatured: boolean;
369
- publishedAt: string | null;
370
- expiresAt: string | null;
371
- createdAt: string;
372
- updatedAt: string;
373
- description: string | null;
374
- applicationUrl: string | null;
375
- /** Canonical hierarchical permit selection. */
376
- remotePermits: RemotePermit[];
377
- /** Read-only — derived from `remotePermits`. */
378
- remoteWorldwide: boolean | null;
379
- /** Canonical hierarchical timezone selection. */
380
- remoteTimezones: RemoteTimezone[];
381
- /** Read-only — derived from `remoteTimezones`. */
382
- remoteAllowedTzOffsets: number[];
383
- /** Read-only — derived from `remotePermits`. */
384
- remoteWorkPermitCountryCodes: string[];
385
- /** Read-only — derived from `remotePermits`. */
386
- remoteWorkPermitSubdivisionCodes: string[];
387
- remoteSponsorship: 'yes' | 'no' | 'unknown';
388
- educationRequirements: EducationRequirement[];
389
- experienceMonths: number | null;
390
- experienceInPlaceOfEducation: boolean | null;
391
- inOfficePeriod: 'per_week' | 'per_month' | 'per_year' | null;
392
- inOfficeFrequency: number | null;
393
- company: JobCompany | null;
394
- officeLocations: OfficeLocation[];
395
- links: {
396
- public: string | null;
397
- };
398
- }
399
- type JobsListQuery = {
400
- cursor?: string;
401
- /** 1–100. */
402
- limit?: number;
403
- companyId?: string;
404
- remoteOption?: RemoteOption;
405
- employmentType?: EmploymentType;
406
- seniority?: Seniority;
407
- };
408
- interface JobsSearchBody {
409
- /** Free-text query, up to 200 characters. */
410
- query?: string;
411
- filters?: {
412
- /** Up to 10 values each. */
413
- companyId?: string[];
414
- remoteOption?: RemoteOption[];
415
- employmentType?: EmploymentType[];
416
- seniority?: Seniority[];
417
- /** ISO 8601 datetime bounds. */
418
- publishedAt?: {
419
- gte?: string;
420
- lte?: string;
421
- };
422
- };
423
- cursor?: string;
424
- /** 1–100. */
425
- limit?: number;
426
- }
427
-
428
578
  /**
429
579
  * The embedded `job` is the SAME `public_job` shape the anonymous jobs
430
580
  * list emits — saved rows and search rows render with one component.
@@ -446,6 +596,53 @@ interface SaveJobBody {
446
596
  jobId: string;
447
597
  }
448
598
 
599
+ interface TaxonomyGeo {
600
+ lat: number | null;
601
+ lng: number | null;
602
+ countryCode: string | null;
603
+ regionCode: string | null;
604
+ region: string | null;
605
+ city: string | null;
606
+ locality: string | null;
607
+ placeType: string | null;
608
+ }
609
+ /**
610
+ * Page-meta for a resolved taxonomy slug. `sourceSlug` is the immutable
611
+ * English search key; `canonicalSlug` is the board-language URL; `redirectTo`
612
+ * is the canonical slug to 308 to when the inbound slug isn't canonical (the
613
+ * host app emits the redirect — the SDK never navigates). `geo` is set for
614
+ * place resolutions only.
615
+ */
616
+ interface TaxonomyResolution {
617
+ object: 'taxonomy_resolution';
618
+ type: 'category' | 'skill' | 'place';
619
+ sourceSlug: string;
620
+ canonicalSlug: string;
621
+ displayName: string;
622
+ redirectTo: string | null;
623
+ geo: TaxonomyGeo | null;
624
+ }
625
+ /**
626
+ * A place in the board's locations directory (`taxonomy.places.list()` →
627
+ * `GET /places`), the data the `/jobs/locations/` index renders. `jobCount` is
628
+ * subtree-summed (a parent counts its descendants); `id`/`parentId` carry the
629
+ * hierarchy so consumers rebuild the same nested tree the hosted index shows.
630
+ */
631
+ interface PublicPlace {
632
+ object: 'place';
633
+ /** Stable place identity (locations-tree edge endpoint). */
634
+ id: string;
635
+ /** Parent place's `id`; `null` for a root place. */
636
+ parentId: string | null;
637
+ /** Public slug (links to `/jobs/locations/:slug`); `null` if unslugged. */
638
+ slug: string | null;
639
+ name: string;
640
+ placeType: string;
641
+ countryCode: string | null;
642
+ regionCode: string | null;
643
+ jobCount: number;
644
+ }
645
+
449
646
  interface CreateBoardClientOptions {
450
647
  baseUrl: string;
451
648
  /** Board identifier: `pk_…` key (provisioned default) | `boards_…` ID | slug. */
@@ -489,16 +686,26 @@ declare function createBoardClient(options: CreateBoardClientOptions): {
489
686
  * const { name, theme } = await board.context();
490
687
  */
491
688
  context(options?: FetchOptions): Promise<PublicBoard>;
689
+ /**
690
+ * Board SEO infra — `ads.txt`, IndexNow key, Google site-verification, and
691
+ * the canonical base URL. Rebuild `robots.txt` / `ads.txt` /
692
+ * `indexnow-key.txt` from it.
693
+ *
694
+ * @example
695
+ * const { adsTxt, canonicalBase } = await board.seo();
696
+ */
697
+ seo(options?: FetchOptions): Promise<BoardSeo>;
492
698
  jobs: {
493
- list(query?: JobsListQuery, options?: FetchOptions): Promise<ListEnvelope<PublicJob>>;
699
+ list(query?: JobsListQuery, options?: FetchOptions): Promise<JobCardListEnvelope>;
494
700
  retrieve(jobSlug: string, query?: Record<string, never>, options?: FetchOptions): Promise<PublicJob>;
495
- search(body: JobsSearchBody, query?: Record<string, never>, options?: FetchOptions): Promise<SearchEnvelope<PublicJob>>;
701
+ search(body: JobsSearchBody, query?: Record<string, never>, options?: FetchOptions): Promise<JobCardSearchEnvelope>;
702
+ similar(jobSlug: string, query?: JobsSimilarQuery, options?: FetchOptions): Promise<ListEnvelope<PublicJobCard>>;
496
703
  };
497
704
  companies: {
498
705
  list(query?: CompaniesListQuery, options?: FetchOptions): Promise<ListEnvelope<PublicCompany>>;
499
706
  retrieve(companySlug: string, query?: Record<string, never>, options?: FetchOptions): Promise<PublicCompany>;
500
707
  search(body: CompaniesSearchBody, query?: Record<string, never>, options?: FetchOptions): Promise<SearchEnvelope<PublicCompany>>;
501
- listJobs(companySlug: string, query?: CompanyJobsListQuery, options?: FetchOptions): Promise<ListEnvelope<PublicJob>>;
708
+ listJobs(companySlug: string, query?: CompanyJobsListQuery, options?: FetchOptions): Promise<JobCardListEnvelope>;
502
709
  };
503
710
  blog: {
504
711
  posts: {
@@ -532,7 +739,22 @@ declare function createBoardClient(options: CreateBoardClientOptions): {
532
739
  unsave(jobId: string, query?: Record<string, never>, options?: FetchOptions): Promise<void>;
533
740
  };
534
741
  };
742
+ taxonomy: {
743
+ categories: {
744
+ resolve(slug: string, options?: FetchOptions): Promise<TaxonomyResolution>;
745
+ };
746
+ skills: {
747
+ resolve(slug: string, options?: FetchOptions): Promise<TaxonomyResolution>;
748
+ };
749
+ places: {
750
+ list(options?: FetchOptions): Promise<ListEnvelope<PublicPlace>>;
751
+ resolve(slug: string, options?: FetchOptions): Promise<TaxonomyResolution>;
752
+ };
753
+ };
754
+ redirects: {
755
+ resolve(path: string, options?: FetchOptions): Promise<RedirectResolution>;
756
+ };
535
757
  };
536
758
  type BoardSdk = ReturnType<typeof createBoardClient>;
537
759
 
538
- export { ACCESS_TOKEN_KEY, type Awaitable, type BlogAuthorEmbed, type BlogPostsListQuery, type BlogSearchBody, type BlogTagEmbed, BoardApiError, type BoardAuthSession, BoardClient, type BoardRequest, type BoardSdk, type BoardUser, type CompaniesListQuery, type CompaniesSearchBody, type CompanyJobsListQuery, type CreateBoardClientOptions, type CustomStorage, type EducationRequirement, type EmploymentType, type FetchOptions, type ForgotPasswordBody, type JobCompany, type JobsListQuery, type JobsSearchBody, type ListEnvelope, type Logger, type LoginBody, type LogoutBody, type OfficeLocation, type PublicBlogAuthor, type PublicBlogPost, type PublicBlogPostSummary, type PublicBlogTag, type PublicBoard, type PublicBoardAnalytics, type PublicBoardFeatures, type PublicBoardTheme, type PublicCompany, type PublicJob, REFRESH_TOKEN_KEY, type RefreshBody, type RegisterBody, type RemoteOption, type RemotePermit, type RemoteTimezone, type ResetPasswordBody, SDK_VERSION, type SaveJobBody, type SavedJob, type SavedJobsListQuery, type SearchEnvelope, type Seniority, type StorageMode, type VerifyEmailBody, createBoardClient, isBoardApiError, isConflict, isForbidden, isNotFound, isRateLimited, isUnauthorized, isValidationError };
760
+ export { ACCESS_TOKEN_KEY, type Awaitable, type BlogAuthorEmbed, type BlogPostsListQuery, type BlogSearchBody, type BlogTagEmbed, BoardApiError, type BoardAuthSession, BoardClient, type BoardRequest, type BoardSdk, type BoardSeo, type BoardUser, type CompaniesListQuery, type CompaniesSearchBody, type CompanyJobsListQuery, type CreateBoardClientOptions, type CustomStorage, type EducationRequirement, type EmploymentType, type FetchOptions, type ForgotPasswordBody, type JobCardListEnvelope, type JobCardSearchEnvelope, type JobCompany, type JobsListQuery, type JobsSearchBody, type ListEnvelope, type Logger, type LoginBody, type LogoutBody, type OfficeLocation, type PublicBlogAuthor, type PublicBlogPost, type PublicBlogPostSummary, type PublicBlogTag, type PublicBoard, type PublicBoardAnalytics, type PublicBoardFeatures, type PublicBoardTheme, type PublicCompany, type PublicJob, type PublicJobCard, type PublicPlace, REFRESH_TOKEN_KEY, type RedirectResolution, type RefreshBody, type RegisterBody, type RelatedSearch, type RemoteOption, type RemotePermit, type RemoteTimezone, type ResetPasswordBody, SDK_VERSION, type SaveJobBody, type SavedJob, type SavedJobsListQuery, type SearchEnvelope, type Seniority, type StorageMode, type StorefrontPagination, type TaxonomyGeo, type TaxonomyResolution, type VerifyEmailBody, createBoardClient, isBoardApiError, isConflict, isForbidden, isNotFound, isRateLimited, isUnauthorized, isValidationError };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,244 @@
1
+ /**
2
+ * Stripe-shaped success envelopes (`01-conventions.md` §5.1). The
3
+ * server MAY add top-level fields; consumers MUST ignore unknown
4
+ * fields.
5
+ */
6
+ /**
7
+ * Medusa-style storefront pagination fields (ADR-0037 §7), populated only by
8
+ * the board jobs catalog reads (browse / search / company-jobs); absent on
9
+ * every other list/search. `nextCursor` is preserved alongside (offset-encoded).
10
+ */
11
+ interface StorefrontPagination {
12
+ /** Total matching results ("X jobs"). */
13
+ count?: number;
14
+ /** The page size used for this response. */
15
+ limit?: number;
16
+ /** Number of items skipped before this page. */
17
+ offset?: number;
18
+ /** Items hidden behind the candidate paywall for the current viewer; absent/0 when entitled. */
19
+ gatedCount?: number;
20
+ }
21
+ interface ListEnvelope<T> extends StorefrontPagination {
22
+ object: 'list';
23
+ url: string;
24
+ hasMore: boolean;
25
+ /** `null` when `hasMore` is false — always present, never undefined. */
26
+ nextCursor: string | null;
27
+ data: T[];
28
+ }
29
+ interface SearchEnvelope<T> extends StorefrontPagination {
30
+ object: 'search_result';
31
+ url: string;
32
+ hasMore: boolean;
33
+ nextCursor: string | null;
34
+ data: T[];
35
+ }
36
+
37
+ type RemoteOption = 'on_site' | 'hybrid' | 'remote';
38
+ type EmploymentType = 'full_time' | 'part_time' | 'contract' | 'internship' | 'temporary' | 'volunteer' | 'other';
39
+ type Seniority = 'entry_level' | 'associate' | 'mid_level' | 'senior' | 'lead' | 'principal' | 'director' | 'executive';
40
+ type EducationRequirement = 'high_school' | 'associate_degree' | 'bachelor_degree' | 'professional_certificate' | 'postgraduate_degree' | 'no_requirements';
41
+ interface OfficeLocation {
42
+ countryCode: string | null;
43
+ country: string | null;
44
+ locality: string | null;
45
+ city: string | null;
46
+ region: string | null;
47
+ regionCode: string | null;
48
+ postalCode: string | null;
49
+ displayName: string | null;
50
+ }
51
+ interface JobCompany {
52
+ id: string;
53
+ name: string | null;
54
+ slug: string | null;
55
+ logoUrl: string | null;
56
+ website: string | null;
57
+ }
58
+ interface RemotePermit {
59
+ type: string;
60
+ value: string;
61
+ }
62
+ interface RemoteTimezone {
63
+ type: string;
64
+ value: string;
65
+ plusMinus?: number;
66
+ }
67
+ interface PublicJob {
68
+ id: string;
69
+ object: 'public_job';
70
+ title: string;
71
+ slug: string | null;
72
+ status: string;
73
+ companyId: string | null;
74
+ employmentType: string | null;
75
+ remoteOption: string | null;
76
+ seniority: string | null;
77
+ salaryMin: number | null;
78
+ salaryMax: number | null;
79
+ salaryCurrency: string | null;
80
+ salaryTimeframe: string | null;
81
+ isFeatured: boolean;
82
+ publishedAt: string | null;
83
+ expiresAt: string | null;
84
+ createdAt: string;
85
+ updatedAt: string;
86
+ description: string | null;
87
+ applicationUrl: string | null;
88
+ /** Canonical hierarchical permit selection. */
89
+ remotePermits: RemotePermit[];
90
+ /** Read-only — derived from `remotePermits`. */
91
+ remoteWorldwide: boolean | null;
92
+ /** Canonical hierarchical timezone selection. */
93
+ remoteTimezones: RemoteTimezone[];
94
+ /** Read-only — derived from `remoteTimezones`. */
95
+ remoteAllowedTzOffsets: number[];
96
+ /** Read-only — derived from `remotePermits`. */
97
+ remoteWorkPermitCountryCodes: string[];
98
+ /** Read-only — derived from `remotePermits`. */
99
+ remoteWorkPermitSubdivisionCodes: string[];
100
+ remoteSponsorship: 'yes' | 'no' | 'unknown';
101
+ educationRequirements: EducationRequirement[];
102
+ experienceMonths: number | null;
103
+ experienceInPlaceOfEducation: boolean | null;
104
+ inOfficePeriod: 'per_week' | 'per_month' | 'per_year' | null;
105
+ inOfficeFrequency: number | null;
106
+ company: JobCompany | null;
107
+ officeLocations: OfficeLocation[];
108
+ /** Resolved taxonomy — same `{slug,name}` shape as `PublicJobCard`; names joined server-side. */
109
+ categories: Array<{
110
+ slug: string;
111
+ name: string;
112
+ }>;
113
+ skills: Array<{
114
+ slug: string;
115
+ name: string;
116
+ }>;
117
+ /**
118
+ * Place ancestor chain (country → region → city) for the breadcrumb. Each
119
+ * `slug` is the English SOURCE slug — the key `taxonomy.places.resolve()`
120
+ * accepts. On a localized board it may differ from the canonical
121
+ * board-language URL slug (the board router 308-redirects the source slug to
122
+ * the canonical one); link via `/jobs/locations/:slug`.
123
+ */
124
+ placeHierarchy: Array<{
125
+ slug: string;
126
+ name: string;
127
+ }>;
128
+ links: {
129
+ public: string | null;
130
+ };
131
+ }
132
+ /**
133
+ * The slim listing card (ADR-0037 §1) returned by `jobs.list` / `jobs.search`
134
+ * / `companies.listJobs`. Full enrichment (description, officeLocations, the
135
+ * structured remote blocks, …) lives on `jobs.retrieve` (`PublicJob`).
136
+ * Transcribed from `serializeJobCard`.
137
+ */
138
+ interface PublicJobCard {
139
+ id: string;
140
+ object: 'job_card';
141
+ slug: string;
142
+ title: string;
143
+ publishedAt: string | null;
144
+ employmentType: string | null;
145
+ remoteOption: string | null;
146
+ /** Remote-region label for remote jobs (e.g. "Worldwide"); `null` otherwise. */
147
+ remoteLocationLabel: string | null;
148
+ salaryMin: number | null;
149
+ salaryMax: number | null;
150
+ salaryCurrency: string | null;
151
+ salaryTimeframe: string | null;
152
+ isFeatured: boolean;
153
+ locationLabel: string | null;
154
+ company: {
155
+ slug: string;
156
+ name: string;
157
+ logoUrl: string | null;
158
+ } | null;
159
+ categories: Array<{
160
+ slug: string;
161
+ name: string;
162
+ }>;
163
+ skills: Array<{
164
+ slug: string;
165
+ name: string;
166
+ }>;
167
+ links: {
168
+ public: string | null;
169
+ };
170
+ /**
171
+ * Long-form description (HTML), or `null`. Present ONLY when the list/search
172
+ * was called with `fields: '+description'` (the RSS feed's sparse-fieldset
173
+ * opt-in); `undefined`/absent on the default slim card.
174
+ */
175
+ description?: string | null;
176
+ }
177
+ /** Derived category/skill suggestion on the jobs browse list (ADR-0037 §8). */
178
+ interface RelatedSearch {
179
+ type: 'category' | 'skill';
180
+ slug: string;
181
+ term: string;
182
+ count: number;
183
+ }
184
+ /** The browse list envelope — `PublicJobCard`s + storefront fields + `relatedSearches`. */
185
+ interface JobCardListEnvelope extends ListEnvelope<PublicJobCard> {
186
+ relatedSearches?: RelatedSearch[];
187
+ }
188
+ /** The search envelope — `PublicJobCard`s + storefront fields. */
189
+ type JobCardSearchEnvelope = SearchEnvelope<PublicJobCard>;
190
+ type JobsListQuery = {
191
+ cursor?: string;
192
+ /** 1–100. */
193
+ limit?: number;
194
+ /** Storefront page offset; takes precedence over `cursor`. `offset + limit` ≤ 10,000. */
195
+ offset?: number;
196
+ /** Single or repeated (up to 10) — repeated params are OR-matched. */
197
+ companyId?: string | string[];
198
+ remoteOption?: RemoteOption | RemoteOption[];
199
+ employmentType?: EmploymentType | EmploymentType[];
200
+ seniority?: Seniority | Seniority[];
201
+ /** Place slug for a geo radius search; unresolvable slugs are ignored. */
202
+ location?: string;
203
+ /** Radius in km around `location` (10–250; default 50). */
204
+ radius?: number;
205
+ /** Category slug seed (the `/jobs/[keyword]` page) — server resolves it to the English source name; unresolvable → 404. */
206
+ category?: string;
207
+ /** Skill slug seed (the `/jobs/skills/[skill]` page) — server-resolved; unresolvable → 404. */
208
+ skill?: string;
209
+ /** Sparse fieldset (Medusa-style `+field`). Only `'+description'` is supported — adds `description` to each card. */
210
+ fields?: string;
211
+ };
212
+ type JobsSimilarQuery = {
213
+ /** How many similar jobs to return (1–20; default 5). */
214
+ limit?: number;
215
+ };
216
+ interface JobsSearchBody {
217
+ /** Free-text query, up to 200 characters. */
218
+ query?: string;
219
+ filters?: {
220
+ /** Up to 10 values each. */
221
+ companyId?: string[];
222
+ remoteOption?: RemoteOption[];
223
+ employmentType?: EmploymentType[];
224
+ seniority?: Seniority[];
225
+ /** ISO 8601 datetime bounds. */
226
+ publishedAt?: {
227
+ gte?: string;
228
+ lte?: string;
229
+ };
230
+ /** Place slug for a geo radius search. */
231
+ location?: string;
232
+ /** Radius in km around `location` (10–250; default 50). */
233
+ radius?: number;
234
+ };
235
+ cursor?: string;
236
+ /** 1–100. */
237
+ limit?: number;
238
+ /** Storefront page offset; takes precedence over `cursor`. */
239
+ offset?: number;
240
+ }
241
+
1
242
  type Awaitable<T> = T | Promise<T>;
2
243
  /**
3
244
  * Async token storage. The SDK reads the access token from storage on
@@ -104,6 +345,27 @@ interface PublicBoard {
104
345
  theme: PublicBoardTheme | null;
105
346
  }
106
347
 
348
+ /**
349
+ * The public SEO-infra payload (`board.seo()`). The four values a headless
350
+ * frontend rebuilds `robots.txt` / `ads.txt` / `indexnow-key.txt` (+ the Google
351
+ * site-verification `<meta>`) from — byte-identically to the hosted board.
352
+ */
353
+ interface BoardSeo {
354
+ object: 'board_seo';
355
+ /** Verbatim `ads.txt` content, or `null` when not configured (the handler 404s). */
356
+ adsTxt: string | null;
357
+ /** IndexNow key-file content, or `null` when not configured (the handler 404s). */
358
+ indexNowKey: string | null;
359
+ /** Google site-verification token for the `<meta>` tag, or `null`. */
360
+ googleSiteVerification: string | null;
361
+ /**
362
+ * The board's canonical base URL (honours a configured custom domain). Build
363
+ * the `robots.txt` `Sitemap:` line (`${canonicalBase}/sitemap.xml`) and
364
+ * canonical links from it — NOT the request origin.
365
+ */
366
+ canonicalBase: string;
367
+ }
368
+
107
369
  /**
108
370
  * Error raised for every non-2xx Board API response.
109
371
  *
@@ -151,7 +413,7 @@ declare function isConflict(e: unknown): e is BoardApiError;
151
413
  * constant because the package is platform-neutral and cannot read
152
414
  * package.json at runtime.
153
415
  */
154
- declare const SDK_VERSION = "1.0.0";
416
+ declare const SDK_VERSION = "1.2.0";
155
417
 
156
418
  interface BoardUser {
157
419
  id: string;
@@ -202,6 +464,19 @@ interface ResetPasswordBody {
202
464
  password: string;
203
465
  }
204
466
 
467
+ /**
468
+ * The result of resolving a path against the board's configured redirects
469
+ * (`board.redirects.resolve()`). A headless frontend 308s to `target`, or 404s
470
+ * when it's `null`.
471
+ */
472
+ interface RedirectResolution {
473
+ object: 'redirect_resolution';
474
+ /** The path that was resolved. */
475
+ path: string;
476
+ /** The redirect target (board-relative), or `null` when no redirect matches. */
477
+ target: string | null;
478
+ }
479
+
205
480
  /** Author shape embedded on posts (no `object` discriminator). */
206
481
  interface BlogAuthorEmbed {
207
482
  id: string;
@@ -268,27 +543,6 @@ interface BlogSearchBody {
268
543
  limit?: number;
269
544
  }
270
545
 
271
- /**
272
- * Stripe-shaped success envelopes (`01-conventions.md` §5.1). The
273
- * server MAY add top-level fields; consumers MUST ignore unknown
274
- * fields.
275
- */
276
- interface ListEnvelope<T> {
277
- object: 'list';
278
- url: string;
279
- hasMore: boolean;
280
- /** `null` when `hasMore` is false — always present, never undefined. */
281
- nextCursor: string | null;
282
- data: T[];
283
- }
284
- interface SearchEnvelope<T> {
285
- object: 'search_result';
286
- url: string;
287
- hasMore: boolean;
288
- nextCursor: string | null;
289
- data: T[];
290
- }
291
-
292
546
  interface PublicCompany {
293
547
  id: string;
294
548
  object: 'public_company';
@@ -321,110 +575,6 @@ interface CompaniesSearchBody {
321
575
  limit?: number;
322
576
  }
323
577
 
324
- type RemoteOption = 'on_site' | 'hybrid' | 'remote';
325
- type EmploymentType = 'full_time' | 'part_time' | 'contract' | 'internship' | 'temporary' | 'volunteer' | 'other';
326
- type Seniority = 'entry_level' | 'associate' | 'mid_level' | 'senior' | 'lead' | 'principal' | 'director' | 'executive';
327
- type EducationRequirement = 'high_school' | 'associate_degree' | 'bachelor_degree' | 'professional_certificate' | 'postgraduate_degree' | 'no_requirements';
328
- interface OfficeLocation {
329
- countryCode: string | null;
330
- country: string | null;
331
- locality: string | null;
332
- city: string | null;
333
- region: string | null;
334
- regionCode: string | null;
335
- postalCode: string | null;
336
- displayName: string | null;
337
- }
338
- interface JobCompany {
339
- id: string;
340
- name: string | null;
341
- slug: string | null;
342
- logoUrl: string | null;
343
- website: string | null;
344
- }
345
- interface RemotePermit {
346
- type: string;
347
- value: string;
348
- }
349
- interface RemoteTimezone {
350
- type: string;
351
- value: string;
352
- plusMinus?: number;
353
- }
354
- interface PublicJob {
355
- id: string;
356
- object: 'public_job';
357
- title: string;
358
- slug: string | null;
359
- status: string;
360
- companyId: string | null;
361
- employmentType: string | null;
362
- remoteOption: string | null;
363
- seniority: string | null;
364
- salaryMin: number | null;
365
- salaryMax: number | null;
366
- salaryCurrency: string | null;
367
- salaryTimeframe: string | null;
368
- isFeatured: boolean;
369
- publishedAt: string | null;
370
- expiresAt: string | null;
371
- createdAt: string;
372
- updatedAt: string;
373
- description: string | null;
374
- applicationUrl: string | null;
375
- /** Canonical hierarchical permit selection. */
376
- remotePermits: RemotePermit[];
377
- /** Read-only — derived from `remotePermits`. */
378
- remoteWorldwide: boolean | null;
379
- /** Canonical hierarchical timezone selection. */
380
- remoteTimezones: RemoteTimezone[];
381
- /** Read-only — derived from `remoteTimezones`. */
382
- remoteAllowedTzOffsets: number[];
383
- /** Read-only — derived from `remotePermits`. */
384
- remoteWorkPermitCountryCodes: string[];
385
- /** Read-only — derived from `remotePermits`. */
386
- remoteWorkPermitSubdivisionCodes: string[];
387
- remoteSponsorship: 'yes' | 'no' | 'unknown';
388
- educationRequirements: EducationRequirement[];
389
- experienceMonths: number | null;
390
- experienceInPlaceOfEducation: boolean | null;
391
- inOfficePeriod: 'per_week' | 'per_month' | 'per_year' | null;
392
- inOfficeFrequency: number | null;
393
- company: JobCompany | null;
394
- officeLocations: OfficeLocation[];
395
- links: {
396
- public: string | null;
397
- };
398
- }
399
- type JobsListQuery = {
400
- cursor?: string;
401
- /** 1–100. */
402
- limit?: number;
403
- companyId?: string;
404
- remoteOption?: RemoteOption;
405
- employmentType?: EmploymentType;
406
- seniority?: Seniority;
407
- };
408
- interface JobsSearchBody {
409
- /** Free-text query, up to 200 characters. */
410
- query?: string;
411
- filters?: {
412
- /** Up to 10 values each. */
413
- companyId?: string[];
414
- remoteOption?: RemoteOption[];
415
- employmentType?: EmploymentType[];
416
- seniority?: Seniority[];
417
- /** ISO 8601 datetime bounds. */
418
- publishedAt?: {
419
- gte?: string;
420
- lte?: string;
421
- };
422
- };
423
- cursor?: string;
424
- /** 1–100. */
425
- limit?: number;
426
- }
427
-
428
578
  /**
429
579
  * The embedded `job` is the SAME `public_job` shape the anonymous jobs
430
580
  * list emits — saved rows and search rows render with one component.
@@ -446,6 +596,53 @@ interface SaveJobBody {
446
596
  jobId: string;
447
597
  }
448
598
 
599
+ interface TaxonomyGeo {
600
+ lat: number | null;
601
+ lng: number | null;
602
+ countryCode: string | null;
603
+ regionCode: string | null;
604
+ region: string | null;
605
+ city: string | null;
606
+ locality: string | null;
607
+ placeType: string | null;
608
+ }
609
+ /**
610
+ * Page-meta for a resolved taxonomy slug. `sourceSlug` is the immutable
611
+ * English search key; `canonicalSlug` is the board-language URL; `redirectTo`
612
+ * is the canonical slug to 308 to when the inbound slug isn't canonical (the
613
+ * host app emits the redirect — the SDK never navigates). `geo` is set for
614
+ * place resolutions only.
615
+ */
616
+ interface TaxonomyResolution {
617
+ object: 'taxonomy_resolution';
618
+ type: 'category' | 'skill' | 'place';
619
+ sourceSlug: string;
620
+ canonicalSlug: string;
621
+ displayName: string;
622
+ redirectTo: string | null;
623
+ geo: TaxonomyGeo | null;
624
+ }
625
+ /**
626
+ * A place in the board's locations directory (`taxonomy.places.list()` →
627
+ * `GET /places`), the data the `/jobs/locations/` index renders. `jobCount` is
628
+ * subtree-summed (a parent counts its descendants); `id`/`parentId` carry the
629
+ * hierarchy so consumers rebuild the same nested tree the hosted index shows.
630
+ */
631
+ interface PublicPlace {
632
+ object: 'place';
633
+ /** Stable place identity (locations-tree edge endpoint). */
634
+ id: string;
635
+ /** Parent place's `id`; `null` for a root place. */
636
+ parentId: string | null;
637
+ /** Public slug (links to `/jobs/locations/:slug`); `null` if unslugged. */
638
+ slug: string | null;
639
+ name: string;
640
+ placeType: string;
641
+ countryCode: string | null;
642
+ regionCode: string | null;
643
+ jobCount: number;
644
+ }
645
+
449
646
  interface CreateBoardClientOptions {
450
647
  baseUrl: string;
451
648
  /** Board identifier: `pk_…` key (provisioned default) | `boards_…` ID | slug. */
@@ -489,16 +686,26 @@ declare function createBoardClient(options: CreateBoardClientOptions): {
489
686
  * const { name, theme } = await board.context();
490
687
  */
491
688
  context(options?: FetchOptions): Promise<PublicBoard>;
689
+ /**
690
+ * Board SEO infra — `ads.txt`, IndexNow key, Google site-verification, and
691
+ * the canonical base URL. Rebuild `robots.txt` / `ads.txt` /
692
+ * `indexnow-key.txt` from it.
693
+ *
694
+ * @example
695
+ * const { adsTxt, canonicalBase } = await board.seo();
696
+ */
697
+ seo(options?: FetchOptions): Promise<BoardSeo>;
492
698
  jobs: {
493
- list(query?: JobsListQuery, options?: FetchOptions): Promise<ListEnvelope<PublicJob>>;
699
+ list(query?: JobsListQuery, options?: FetchOptions): Promise<JobCardListEnvelope>;
494
700
  retrieve(jobSlug: string, query?: Record<string, never>, options?: FetchOptions): Promise<PublicJob>;
495
- search(body: JobsSearchBody, query?: Record<string, never>, options?: FetchOptions): Promise<SearchEnvelope<PublicJob>>;
701
+ search(body: JobsSearchBody, query?: Record<string, never>, options?: FetchOptions): Promise<JobCardSearchEnvelope>;
702
+ similar(jobSlug: string, query?: JobsSimilarQuery, options?: FetchOptions): Promise<ListEnvelope<PublicJobCard>>;
496
703
  };
497
704
  companies: {
498
705
  list(query?: CompaniesListQuery, options?: FetchOptions): Promise<ListEnvelope<PublicCompany>>;
499
706
  retrieve(companySlug: string, query?: Record<string, never>, options?: FetchOptions): Promise<PublicCompany>;
500
707
  search(body: CompaniesSearchBody, query?: Record<string, never>, options?: FetchOptions): Promise<SearchEnvelope<PublicCompany>>;
501
- listJobs(companySlug: string, query?: CompanyJobsListQuery, options?: FetchOptions): Promise<ListEnvelope<PublicJob>>;
708
+ listJobs(companySlug: string, query?: CompanyJobsListQuery, options?: FetchOptions): Promise<JobCardListEnvelope>;
502
709
  };
503
710
  blog: {
504
711
  posts: {
@@ -532,7 +739,22 @@ declare function createBoardClient(options: CreateBoardClientOptions): {
532
739
  unsave(jobId: string, query?: Record<string, never>, options?: FetchOptions): Promise<void>;
533
740
  };
534
741
  };
742
+ taxonomy: {
743
+ categories: {
744
+ resolve(slug: string, options?: FetchOptions): Promise<TaxonomyResolution>;
745
+ };
746
+ skills: {
747
+ resolve(slug: string, options?: FetchOptions): Promise<TaxonomyResolution>;
748
+ };
749
+ places: {
750
+ list(options?: FetchOptions): Promise<ListEnvelope<PublicPlace>>;
751
+ resolve(slug: string, options?: FetchOptions): Promise<TaxonomyResolution>;
752
+ };
753
+ };
754
+ redirects: {
755
+ resolve(path: string, options?: FetchOptions): Promise<RedirectResolution>;
756
+ };
535
757
  };
536
758
  type BoardSdk = ReturnType<typeof createBoardClient>;
537
759
 
538
- export { ACCESS_TOKEN_KEY, type Awaitable, type BlogAuthorEmbed, type BlogPostsListQuery, type BlogSearchBody, type BlogTagEmbed, BoardApiError, type BoardAuthSession, BoardClient, type BoardRequest, type BoardSdk, type BoardUser, type CompaniesListQuery, type CompaniesSearchBody, type CompanyJobsListQuery, type CreateBoardClientOptions, type CustomStorage, type EducationRequirement, type EmploymentType, type FetchOptions, type ForgotPasswordBody, type JobCompany, type JobsListQuery, type JobsSearchBody, type ListEnvelope, type Logger, type LoginBody, type LogoutBody, type OfficeLocation, type PublicBlogAuthor, type PublicBlogPost, type PublicBlogPostSummary, type PublicBlogTag, type PublicBoard, type PublicBoardAnalytics, type PublicBoardFeatures, type PublicBoardTheme, type PublicCompany, type PublicJob, REFRESH_TOKEN_KEY, type RefreshBody, type RegisterBody, type RemoteOption, type RemotePermit, type RemoteTimezone, type ResetPasswordBody, SDK_VERSION, type SaveJobBody, type SavedJob, type SavedJobsListQuery, type SearchEnvelope, type Seniority, type StorageMode, type VerifyEmailBody, createBoardClient, isBoardApiError, isConflict, isForbidden, isNotFound, isRateLimited, isUnauthorized, isValidationError };
760
+ export { ACCESS_TOKEN_KEY, type Awaitable, type BlogAuthorEmbed, type BlogPostsListQuery, type BlogSearchBody, type BlogTagEmbed, BoardApiError, type BoardAuthSession, BoardClient, type BoardRequest, type BoardSdk, type BoardSeo, type BoardUser, type CompaniesListQuery, type CompaniesSearchBody, type CompanyJobsListQuery, type CreateBoardClientOptions, type CustomStorage, type EducationRequirement, type EmploymentType, type FetchOptions, type ForgotPasswordBody, type JobCardListEnvelope, type JobCardSearchEnvelope, type JobCompany, type JobsListQuery, type JobsSearchBody, type ListEnvelope, type Logger, type LoginBody, type LogoutBody, type OfficeLocation, type PublicBlogAuthor, type PublicBlogPost, type PublicBlogPostSummary, type PublicBlogTag, type PublicBoard, type PublicBoardAnalytics, type PublicBoardFeatures, type PublicBoardTheme, type PublicCompany, type PublicJob, type PublicJobCard, type PublicPlace, REFRESH_TOKEN_KEY, type RedirectResolution, type RefreshBody, type RegisterBody, type RelatedSearch, type RemoteOption, type RemotePermit, type RemoteTimezone, type ResetPasswordBody, SDK_VERSION, type SaveJobBody, type SavedJob, type SavedJobsListQuery, type SearchEnvelope, type Seniority, type StorageMode, type StorefrontPagination, type TaxonomyGeo, type TaxonomyResolution, type VerifyEmailBody, createBoardClient, isBoardApiError, isConflict, isForbidden, isNotFound, isRateLimited, isUnauthorized, isValidationError };
package/dist/index.js CHANGED
@@ -143,7 +143,7 @@ async function clearSession(storage) {
143
143
  }
144
144
 
145
145
  // src/version.ts
146
- var SDK_VERSION = "1.0.0";
146
+ var SDK_VERSION = "1.2.0";
147
147
 
148
148
  // src/client.ts
149
149
  function isRawBody(body) {
@@ -544,6 +544,23 @@ function jobsNamespace(client) {
544
544
  body,
545
545
  query
546
546
  });
547
+ },
548
+ /**
549
+ * List jobs similar to one job — the same ranking that powers the
550
+ * on-page similar-jobs rail. Returns up to `limit` slim cards (default
551
+ * 5), excluding the job itself and any role at the same company.
552
+ *
553
+ * @example
554
+ * const { data } = await board.jobs.similar('senior-chef', { limit: 5 });
555
+ */
556
+ similar(jobSlug, query, options) {
557
+ return client.fetch(
558
+ `/jobs/${encodeURIComponent(jobSlug)}/similar`,
559
+ {
560
+ ...options,
561
+ query
562
+ }
563
+ );
547
564
  }
548
565
  };
549
566
  }
@@ -608,6 +625,55 @@ function meNamespace(client) {
608
625
  };
609
626
  }
610
627
 
628
+ // src/namespaces/redirects.ts
629
+ function redirectsNamespace(client) {
630
+ return {
631
+ /**
632
+ * Resolve a board-relative path against the board's configured redirects —
633
+ * the same resolution the hosted board's catch-all performs. 308 to
634
+ * `target`, or 404 when `target` is `null`.
635
+ *
636
+ * @example
637
+ * const { target } = await board.redirects.resolve('/old-jobs')
638
+ */
639
+ resolve(path, options) {
640
+ return client.fetch("/redirects/resolve", {
641
+ ...options,
642
+ query: { path }
643
+ });
644
+ }
645
+ };
646
+ }
647
+
648
+ // src/namespaces/taxonomy.ts
649
+ function taxonomyResolver(client, kind) {
650
+ return {
651
+ /**
652
+ * Resolve a (board-language or English) taxonomy slug to its page-meta.
653
+ * Rejects with a `not_found` `BoardApiError` when the slug doesn't resolve.
654
+ */
655
+ resolve(slug, options) {
656
+ return client.fetch(
657
+ `/${kind}/${encodeURIComponent(slug)}`,
658
+ options
659
+ );
660
+ }
661
+ };
662
+ }
663
+ function taxonomyNamespace(client) {
664
+ return {
665
+ categories: taxonomyResolver(client, "categories"),
666
+ skills: taxonomyResolver(client, "skills"),
667
+ places: {
668
+ ...taxonomyResolver(client, "places"),
669
+ /** List every place used by a published job, with its live job count. */
670
+ list(options) {
671
+ return client.fetch("/places", options);
672
+ }
673
+ }
674
+ };
675
+ }
676
+
611
677
  // src/index.ts
612
678
  function createBoardClient(options) {
613
679
  const client = new BoardClient({
@@ -635,10 +701,23 @@ function createBoardClient(options) {
635
701
  context(options2) {
636
702
  return client.fetch("", options2);
637
703
  },
704
+ /**
705
+ * Board SEO infra — `ads.txt`, IndexNow key, Google site-verification, and
706
+ * the canonical base URL. Rebuild `robots.txt` / `ads.txt` /
707
+ * `indexnow-key.txt` from it.
708
+ *
709
+ * @example
710
+ * const { adsTxt, canonicalBase } = await board.seo();
711
+ */
712
+ seo(options2) {
713
+ return client.fetch("/seo", options2);
714
+ },
638
715
  jobs: jobsNamespace(client),
639
716
  companies: companiesNamespace(client),
640
717
  blog: blogNamespace(client),
641
718
  auth: authNamespace(client),
642
- me: meNamespace(client)
719
+ me: meNamespace(client),
720
+ taxonomy: taxonomyNamespace(client),
721
+ redirects: redirectsNamespace(client)
643
722
  };
644
723
  }
package/dist/index.mjs CHANGED
@@ -105,7 +105,7 @@ async function clearSession(storage) {
105
105
  }
106
106
 
107
107
  // src/version.ts
108
- var SDK_VERSION = "1.0.0";
108
+ var SDK_VERSION = "1.2.0";
109
109
 
110
110
  // src/client.ts
111
111
  function isRawBody(body) {
@@ -506,6 +506,23 @@ function jobsNamespace(client) {
506
506
  body,
507
507
  query
508
508
  });
509
+ },
510
+ /**
511
+ * List jobs similar to one job — the same ranking that powers the
512
+ * on-page similar-jobs rail. Returns up to `limit` slim cards (default
513
+ * 5), excluding the job itself and any role at the same company.
514
+ *
515
+ * @example
516
+ * const { data } = await board.jobs.similar('senior-chef', { limit: 5 });
517
+ */
518
+ similar(jobSlug, query, options) {
519
+ return client.fetch(
520
+ `/jobs/${encodeURIComponent(jobSlug)}/similar`,
521
+ {
522
+ ...options,
523
+ query
524
+ }
525
+ );
509
526
  }
510
527
  };
511
528
  }
@@ -570,6 +587,55 @@ function meNamespace(client) {
570
587
  };
571
588
  }
572
589
 
590
+ // src/namespaces/redirects.ts
591
+ function redirectsNamespace(client) {
592
+ return {
593
+ /**
594
+ * Resolve a board-relative path against the board's configured redirects —
595
+ * the same resolution the hosted board's catch-all performs. 308 to
596
+ * `target`, or 404 when `target` is `null`.
597
+ *
598
+ * @example
599
+ * const { target } = await board.redirects.resolve('/old-jobs')
600
+ */
601
+ resolve(path, options) {
602
+ return client.fetch("/redirects/resolve", {
603
+ ...options,
604
+ query: { path }
605
+ });
606
+ }
607
+ };
608
+ }
609
+
610
+ // src/namespaces/taxonomy.ts
611
+ function taxonomyResolver(client, kind) {
612
+ return {
613
+ /**
614
+ * Resolve a (board-language or English) taxonomy slug to its page-meta.
615
+ * Rejects with a `not_found` `BoardApiError` when the slug doesn't resolve.
616
+ */
617
+ resolve(slug, options) {
618
+ return client.fetch(
619
+ `/${kind}/${encodeURIComponent(slug)}`,
620
+ options
621
+ );
622
+ }
623
+ };
624
+ }
625
+ function taxonomyNamespace(client) {
626
+ return {
627
+ categories: taxonomyResolver(client, "categories"),
628
+ skills: taxonomyResolver(client, "skills"),
629
+ places: {
630
+ ...taxonomyResolver(client, "places"),
631
+ /** List every place used by a published job, with its live job count. */
632
+ list(options) {
633
+ return client.fetch("/places", options);
634
+ }
635
+ }
636
+ };
637
+ }
638
+
573
639
  // src/index.ts
574
640
  function createBoardClient(options) {
575
641
  const client = new BoardClient({
@@ -597,11 +663,24 @@ function createBoardClient(options) {
597
663
  context(options2) {
598
664
  return client.fetch("", options2);
599
665
  },
666
+ /**
667
+ * Board SEO infra — `ads.txt`, IndexNow key, Google site-verification, and
668
+ * the canonical base URL. Rebuild `robots.txt` / `ads.txt` /
669
+ * `indexnow-key.txt` from it.
670
+ *
671
+ * @example
672
+ * const { adsTxt, canonicalBase } = await board.seo();
673
+ */
674
+ seo(options2) {
675
+ return client.fetch("/seo", options2);
676
+ },
600
677
  jobs: jobsNamespace(client),
601
678
  companies: companiesNamespace(client),
602
679
  blog: blogNamespace(client),
603
680
  auth: authNamespace(client),
604
- me: meNamespace(client)
681
+ me: meNamespace(client),
682
+ taxonomy: taxonomyNamespace(client),
683
+ redirects: redirectsNamespace(client)
605
684
  };
606
685
  }
607
686
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cavuno/board",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Typed isomorphic client for the Cavuno Board API",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -33,17 +33,18 @@
33
33
  "publishConfig": {
34
34
  "access": "public"
35
35
  },
36
- "devDependencies": {
37
- "@types/node": "24.9.1",
38
- "tsup": "^8.4.0",
39
- "vitest": "^2.1.9",
40
- "@kit/tsconfig": "0.1.0"
41
- },
42
36
  "scripts": {
43
37
  "build": "tsup",
44
38
  "clean": "git clean -xdf .turbo dist node_modules",
45
39
  "typecheck": "tsgo --noEmit",
46
40
  "test": "vitest run",
47
- "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')}\""
41
+ "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"
43
+ },
44
+ "devDependencies": {
45
+ "@kit/tsconfig": "workspace:*",
46
+ "@types/node": "catalog:",
47
+ "tsup": "^8.4.0",
48
+ "vitest": "^2.1.9"
48
49
  }
49
- }
50
+ }