@cavuno/board 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,538 @@
1
+ type Awaitable<T> = T | Promise<T>;
2
+ /**
3
+ * Async token storage. The SDK reads the access token from storage on
4
+ * every request (fresh-token rule); auth methods write/clear the pair.
5
+ */
6
+ interface CustomStorage {
7
+ getItem(key: string): Awaitable<string | null>;
8
+ setItem(key: string, value: string): Awaitable<void>;
9
+ removeItem(key: string): Awaitable<void>;
10
+ }
11
+ type StorageMode = 'memory' | 'nostore' | CustomStorage;
12
+ declare const ACCESS_TOKEN_KEY = "cavuno_board_access_token";
13
+ declare const REFRESH_TOKEN_KEY = "cavuno_board_refresh_token";
14
+
15
+ /**
16
+ * The request handed to `onRequest` and `fetch`. `onRequest` may
17
+ * mutate or replace it wholesale (locale headers, URL rewrites);
18
+ * whatever it returns is what gets fetched.
19
+ */
20
+ interface BoardRequest {
21
+ /** Fully built URL, query string included. */
22
+ url: string;
23
+ /** Final RequestInit: method, Headers instance, serialized body, plus passthrough keys (`signal`, `cache`, `next`, `cf`, …). */
24
+ init: RequestInit;
25
+ }
26
+ interface Logger {
27
+ debug(message: string): void;
28
+ error(message: string): void;
29
+ }
30
+ /**
31
+ * Per-call options on every SDK method. Everything besides `body` and
32
+ * `query` is passed through to `fetch` untouched, so framework caching
33
+ * (`next: { tags }`, `cf: {...}`, `cache:`) rides for free.
34
+ */
35
+ type FetchOptions = Omit<RequestInit, 'body'> & {
36
+ body?: unknown;
37
+ query?: Record<string, unknown>;
38
+ };
39
+ interface BoardClientOptions {
40
+ baseUrl: string;
41
+ /** Board identifier: `pk_…` key | `boards_…` ID | slug (ADR-0032). */
42
+ board: string;
43
+ storage: CustomStorage;
44
+ globalHeaders?: Record<string, string>;
45
+ onRequest?: (req: BoardRequest) => Awaitable<BoardRequest>;
46
+ onResponse?: (res: Response, req: BoardRequest) => Awaitable<void>;
47
+ logger?: Logger;
48
+ }
49
+ declare class BoardClient {
50
+ readonly storage: CustomStorage;
51
+ private readonly basePath;
52
+ private readonly options;
53
+ constructor(options: BoardClientOptions);
54
+ /**
55
+ * The full request pipeline. Public and first-class: custom endpoints
56
+ * work without an SDK release, and still get the board-identifier
57
+ * base path, default headers, bearer token, and both hooks.
58
+ *
59
+ * @example
60
+ * const stats = await board.client.fetch<MyShape>('/custom/stats');
61
+ */
62
+ fetch<T>(path: string, init?: FetchOptions): Promise<T>;
63
+ }
64
+
65
+ interface PublicBoardFeatures {
66
+ jobAlerts: boolean;
67
+ candidates: boolean;
68
+ employers: boolean;
69
+ blog: boolean;
70
+ talentDirectory: boolean;
71
+ registrationWall: boolean;
72
+ passwordProtected: boolean;
73
+ publicJobSubmission: boolean;
74
+ candidatePaywall: boolean;
75
+ }
76
+ interface PublicBoardAnalytics {
77
+ ga4MeasurementId: string | null;
78
+ gtmId: string | null;
79
+ metaPixelId: string | null;
80
+ linkedInPartnerId: string | null;
81
+ cookieConsentRequired: boolean;
82
+ }
83
+ interface PublicBoardTheme {
84
+ mode: string;
85
+ schemeId: string;
86
+ typography: {
87
+ fontSans: string;
88
+ fontHeading?: string | null;
89
+ };
90
+ colors: Record<string, unknown>;
91
+ }
92
+ interface PublicBoard {
93
+ object: 'public_board';
94
+ /** Immutable board identifier (`boards_…`, ADR-0032). */
95
+ id: string;
96
+ /** Mutable canonical slug. */
97
+ slug: string;
98
+ name: string;
99
+ language: string;
100
+ logoUrl: string | null;
101
+ primaryDomain: string | null;
102
+ features: PublicBoardFeatures;
103
+ analytics: PublicBoardAnalytics;
104
+ theme: PublicBoardTheme | null;
105
+ }
106
+
107
+ /**
108
+ * Error raised for every non-2xx Board API response.
109
+ *
110
+ * Carries the complete v1 error envelope (`01-conventions.md` §5.2):
111
+ * `{ error: { code, message, details?, requestId } }` — nothing is
112
+ * discarded, so callers never need to string-match messages.
113
+ *
114
+ * @example
115
+ * try {
116
+ * await board.jobs.retrieve('senior-chef');
117
+ * } catch (e) {
118
+ * if (isNotFound(e)) showEmptyState();
119
+ * else throw e;
120
+ * }
121
+ */
122
+ declare class BoardApiError extends Error {
123
+ readonly status: number;
124
+ /** `<domain>_<snake_reason>` code from the v1 error envelope. */
125
+ readonly code: string;
126
+ /** Structured, per-code details — shape varies by `code`. */
127
+ readonly details?: unknown;
128
+ readonly requestId?: string;
129
+ /** The parsed response body, untouched. */
130
+ readonly raw: unknown;
131
+ constructor(input: {
132
+ status: number;
133
+ code: string;
134
+ message: string;
135
+ details?: unknown;
136
+ requestId?: string;
137
+ raw: unknown;
138
+ });
139
+ }
140
+ declare function isBoardApiError(e: unknown): e is BoardApiError;
141
+ declare function isNotFound(e: unknown): e is BoardApiError;
142
+ declare function isUnauthorized(e: unknown): e is BoardApiError;
143
+ declare function isForbidden(e: unknown): e is BoardApiError;
144
+ declare function isValidationError(e: unknown): e is BoardApiError;
145
+ declare function isRateLimited(e: unknown): e is BoardApiError;
146
+ declare function isConflict(e: unknown): e is BoardApiError;
147
+
148
+ /**
149
+ * Kept in lockstep with the package.json `version` field — guarded by
150
+ * version.test.ts, which fails the suite on any drift. A hand-written
151
+ * constant because the package is platform-neutral and cannot read
152
+ * package.json at runtime.
153
+ */
154
+ declare const SDK_VERSION = "1.0.0";
155
+
156
+ interface BoardUser {
157
+ id: string;
158
+ object: 'board_user';
159
+ role: 'candidate' | 'employer';
160
+ email: string;
161
+ displayName: string | null;
162
+ emailVerified: boolean;
163
+ }
164
+ /**
165
+ * Bearer pair returned by register/login/refresh. `expiresAt` is the
166
+ * ACCESS token expiry in epoch ms; the refresh token's own 30-day TTL
167
+ * is intentionally not exposed.
168
+ */
169
+ interface BoardAuthSession {
170
+ object: 'board_auth_session';
171
+ accessToken: string;
172
+ refreshToken: string;
173
+ expiresAt: number;
174
+ boardUser: BoardUser;
175
+ }
176
+ interface RegisterBody {
177
+ role: 'candidate' | 'employer';
178
+ /** Only `emailpass` is supported. */
179
+ method: 'emailpass';
180
+ email: string;
181
+ password: string;
182
+ displayName: string;
183
+ }
184
+ interface LoginBody {
185
+ email: string;
186
+ password: string;
187
+ }
188
+ interface RefreshBody {
189
+ refreshToken: string;
190
+ }
191
+ interface LogoutBody {
192
+ refreshToken: string;
193
+ }
194
+ interface VerifyEmailBody {
195
+ token: string;
196
+ }
197
+ interface ForgotPasswordBody {
198
+ email: string;
199
+ }
200
+ interface ResetPasswordBody {
201
+ token: string;
202
+ password: string;
203
+ }
204
+
205
+ /** Author shape embedded on posts (no `object` discriminator). */
206
+ interface BlogAuthorEmbed {
207
+ id: string;
208
+ name: string;
209
+ slug: string;
210
+ bio: string | null;
211
+ avatarUrl: string | null;
212
+ websiteUrl: string | null;
213
+ twitterUrl: string | null;
214
+ linkedinUrl: string | null;
215
+ githubUrl: string | null;
216
+ }
217
+ /** Tag shape embedded on posts (no `object` discriminator). */
218
+ interface BlogTagEmbed {
219
+ id: string;
220
+ name: string;
221
+ slug: string;
222
+ description: string | null;
223
+ }
224
+ interface PublicBlogAuthor extends BlogAuthorEmbed {
225
+ object: 'public_blog_author';
226
+ }
227
+ interface PublicBlogTag extends BlogTagEmbed {
228
+ object: 'public_blog_tag';
229
+ }
230
+ interface PublicBlogPostSummary {
231
+ id: string;
232
+ object: 'public_blog_post';
233
+ title: string;
234
+ slug: string;
235
+ featured: boolean;
236
+ coverUrl: string | null;
237
+ customExcerpt: string | null;
238
+ readingTimeMin: number | null;
239
+ publishedAt: string | null;
240
+ createdAt: string;
241
+ authors: BlogAuthorEmbed[];
242
+ tags: BlogTagEmbed[];
243
+ }
244
+ /** Detail shape — `html` appears on the single read only, never on summaries. */
245
+ interface PublicBlogPost extends PublicBlogPostSummary {
246
+ html: string | null;
247
+ ogImageUrl: string | null;
248
+ featureImageAlt: string | null;
249
+ featureImageCaption: string | null;
250
+ seoTitle: string | null;
251
+ seoDescription: string | null;
252
+ canonicalUrl: string | null;
253
+ }
254
+ type BlogPostsListQuery = {
255
+ cursor?: string;
256
+ /** 1–100. */
257
+ limit?: number;
258
+ tagSlug?: string;
259
+ authorSlug?: string;
260
+ /** Opt-in only: pass `'true'` to restrict to featured posts. */
261
+ featured?: 'true';
262
+ };
263
+ interface BlogSearchBody {
264
+ /** Free-text query, 1–200 characters. Required. */
265
+ query: string;
266
+ cursor?: string | null;
267
+ /** 1–50. */
268
+ limit?: number;
269
+ }
270
+
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
+ interface PublicCompany {
293
+ id: string;
294
+ object: 'public_company';
295
+ name: string;
296
+ slug: string;
297
+ website: string | null;
298
+ logoUrl: string | null;
299
+ description: string | null;
300
+ jobCount: number;
301
+ publishedJobCount: number;
302
+ links: {
303
+ public: string | null;
304
+ };
305
+ }
306
+ type CompaniesListQuery = {
307
+ cursor?: string;
308
+ /** 1–100. */
309
+ limit?: number;
310
+ };
311
+ type CompanyJobsListQuery = {
312
+ cursor?: string;
313
+ /** 1–100. */
314
+ limit?: number;
315
+ };
316
+ interface CompaniesSearchBody {
317
+ /** Free-text query, up to 200 characters. */
318
+ query?: string;
319
+ cursor?: string;
320
+ /** 1–100. */
321
+ limit?: number;
322
+ }
323
+
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
+ /**
429
+ * The embedded `job` is the SAME `public_job` shape the anonymous jobs
430
+ * list emits — saved rows and search rows render with one component.
431
+ */
432
+ interface SavedJob {
433
+ id: string;
434
+ object: 'saved_job';
435
+ jobId: string;
436
+ /** ISO 8601. */
437
+ savedAt: string;
438
+ job: PublicJob;
439
+ }
440
+ type SavedJobsListQuery = {
441
+ cursor?: string;
442
+ /** 1–100. */
443
+ limit?: number;
444
+ };
445
+ interface SaveJobBody {
446
+ jobId: string;
447
+ }
448
+
449
+ interface CreateBoardClientOptions {
450
+ baseUrl: string;
451
+ /** Board identifier: `pk_…` key (provisioned default) | `boards_…` ID | slug. */
452
+ board: string;
453
+ auth?: {
454
+ /** Default: `'memory'` in the browser, `'nostore'` on the server. */
455
+ storage?: StorageMode;
456
+ };
457
+ globalHeaders?: Record<string, string>;
458
+ onRequest?: (req: BoardRequest) => Awaitable<BoardRequest>;
459
+ onResponse?: (res: Response, req: BoardRequest) => Awaitable<void>;
460
+ logger?: Logger;
461
+ }
462
+ /**
463
+ * Create a Board API client.
464
+ *
465
+ * Isomorphic (browser, Workers, Node ≥ 20), zero dependencies. One
466
+ * module-scoped instance is safe under SSR as long as per-user state
467
+ * stays per-call (`options.headers`) rather than in `auth.storage`.
468
+ *
469
+ * @example
470
+ * import { createBoardClient } from '@cavuno/board';
471
+ *
472
+ * const board = createBoardClient({
473
+ * baseUrl: 'https://api.cavuno.com',
474
+ * board: 'pk_a8f3…',
475
+ * });
476
+ * const { data } = await board.jobs.list({ limit: 20 });
477
+ */
478
+ declare function createBoardClient(options: CreateBoardClientOptions): {
479
+ /**
480
+ * Escape hatch: raw typed request through the full pipeline (board
481
+ * base path, default headers, bearer token, hooks). Custom
482
+ * endpoints work without an SDK release.
483
+ */
484
+ client: BoardClient;
485
+ /**
486
+ * Board context — identity, features, analytics, theme.
487
+ *
488
+ * @example
489
+ * const { name, theme } = await board.context();
490
+ */
491
+ context(options?: FetchOptions): Promise<PublicBoard>;
492
+ jobs: {
493
+ list(query?: JobsListQuery, options?: FetchOptions): Promise<ListEnvelope<PublicJob>>;
494
+ retrieve(jobSlug: string, query?: Record<string, never>, options?: FetchOptions): Promise<PublicJob>;
495
+ search(body: JobsSearchBody, query?: Record<string, never>, options?: FetchOptions): Promise<SearchEnvelope<PublicJob>>;
496
+ };
497
+ companies: {
498
+ list(query?: CompaniesListQuery, options?: FetchOptions): Promise<ListEnvelope<PublicCompany>>;
499
+ retrieve(companySlug: string, query?: Record<string, never>, options?: FetchOptions): Promise<PublicCompany>;
500
+ search(body: CompaniesSearchBody, query?: Record<string, never>, options?: FetchOptions): Promise<SearchEnvelope<PublicCompany>>;
501
+ listJobs(companySlug: string, query?: CompanyJobsListQuery, options?: FetchOptions): Promise<ListEnvelope<PublicJob>>;
502
+ };
503
+ blog: {
504
+ posts: {
505
+ list(query?: BlogPostsListQuery, options?: FetchOptions): Promise<ListEnvelope<PublicBlogPostSummary>>;
506
+ retrieve(postSlug: string, query?: Record<string, never>, options?: FetchOptions): Promise<PublicBlogPost>;
507
+ };
508
+ tags: {
509
+ list(query?: Record<string, never>, options?: FetchOptions): Promise<ListEnvelope<PublicBlogTag>>;
510
+ retrieve(tagSlug: string, query?: Record<string, never>, options?: FetchOptions): Promise<PublicBlogTag>;
511
+ };
512
+ authors: {
513
+ list(query?: Record<string, never>, options?: FetchOptions): Promise<ListEnvelope<PublicBlogAuthor>>;
514
+ retrieve(authorSlug: string, query?: Record<string, never>, options?: FetchOptions): Promise<PublicBlogAuthor>;
515
+ };
516
+ search(body: BlogSearchBody, query?: Record<string, never>, options?: FetchOptions): Promise<SearchEnvelope<PublicBlogPostSummary>>;
517
+ };
518
+ auth: {
519
+ register(body: RegisterBody, options?: FetchOptions): Promise<BoardAuthSession>;
520
+ login(body: LoginBody, options?: FetchOptions): Promise<BoardAuthSession>;
521
+ refresh(body?: RefreshBody, options?: FetchOptions): Promise<BoardAuthSession>;
522
+ logout(body?: LogoutBody, options?: FetchOptions): Promise<void>;
523
+ verifyEmail(body: VerifyEmailBody, options?: FetchOptions): Promise<void>;
524
+ forgotPassword(body: ForgotPasswordBody, options?: FetchOptions): Promise<void>;
525
+ resetPassword(body: ResetPasswordBody, options?: FetchOptions): Promise<void>;
526
+ };
527
+ me: {
528
+ retrieve(query?: Record<string, never>, options?: FetchOptions): Promise<BoardUser>;
529
+ savedJobs: {
530
+ list(query?: SavedJobsListQuery, options?: FetchOptions): Promise<ListEnvelope<SavedJob>>;
531
+ save(body: SaveJobBody, query?: Record<string, never>, options?: FetchOptions): Promise<SavedJob>;
532
+ unsave(jobId: string, query?: Record<string, never>, options?: FetchOptions): Promise<void>;
533
+ };
534
+ };
535
+ };
536
+ type BoardSdk = ReturnType<typeof createBoardClient>;
537
+
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 };