@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.
package/dist/index.js ADDED
@@ -0,0 +1,644 @@
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/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ACCESS_TOKEN_KEY: () => ACCESS_TOKEN_KEY,
24
+ BoardApiError: () => BoardApiError,
25
+ BoardClient: () => BoardClient,
26
+ REFRESH_TOKEN_KEY: () => REFRESH_TOKEN_KEY,
27
+ SDK_VERSION: () => SDK_VERSION,
28
+ createBoardClient: () => createBoardClient,
29
+ isBoardApiError: () => isBoardApiError,
30
+ isConflict: () => isConflict,
31
+ isForbidden: () => isForbidden,
32
+ isNotFound: () => isNotFound,
33
+ isRateLimited: () => isRateLimited,
34
+ isUnauthorized: () => isUnauthorized,
35
+ isValidationError: () => isValidationError
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // src/errors.ts
40
+ var BoardApiError = class extends Error {
41
+ status;
42
+ /** `<domain>_<snake_reason>` code from the v1 error envelope. */
43
+ code;
44
+ /** Structured, per-code details — shape varies by `code`. */
45
+ details;
46
+ requestId;
47
+ /** The parsed response body, untouched. */
48
+ raw;
49
+ constructor(input) {
50
+ super(input.message);
51
+ this.name = "BoardApiError";
52
+ this.status = input.status;
53
+ this.code = input.code;
54
+ this.details = input.details;
55
+ this.requestId = input.requestId;
56
+ this.raw = input.raw;
57
+ }
58
+ };
59
+ function isBoardApiError(e) {
60
+ return e instanceof BoardApiError;
61
+ }
62
+ function isNotFound(e) {
63
+ return isBoardApiError(e) && e.status === 404;
64
+ }
65
+ function isUnauthorized(e) {
66
+ return isBoardApiError(e) && e.status === 401;
67
+ }
68
+ function isForbidden(e) {
69
+ return isBoardApiError(e) && e.status === 403;
70
+ }
71
+ function isValidationError(e) {
72
+ return isBoardApiError(e) && e.status === 400 && e.code === "validation_bad_request";
73
+ }
74
+ function isRateLimited(e) {
75
+ return isBoardApiError(e) && e.status === 429;
76
+ }
77
+ function isConflict(e) {
78
+ return isBoardApiError(e) && e.status === 409;
79
+ }
80
+
81
+ // src/query.ts
82
+ function toSearchParams(query) {
83
+ const params = new URLSearchParams();
84
+ if (!query) return params;
85
+ for (const [key, value] of Object.entries(query)) {
86
+ if (value === void 0 || value === null) continue;
87
+ if (Array.isArray(value)) {
88
+ for (const item of value) {
89
+ if (item === void 0 || item === null) continue;
90
+ params.append(key, String(item));
91
+ }
92
+ } else {
93
+ params.append(key, String(value));
94
+ }
95
+ }
96
+ return params;
97
+ }
98
+
99
+ // src/storage.ts
100
+ var ACCESS_TOKEN_KEY = "cavuno_board_access_token";
101
+ var REFRESH_TOKEN_KEY = "cavuno_board_refresh_token";
102
+ function isBrowser() {
103
+ return typeof globalThis.document !== "undefined";
104
+ }
105
+ function memoryStorage() {
106
+ const backing = /* @__PURE__ */ new Map();
107
+ return {
108
+ getItem: (key) => backing.get(key) ?? null,
109
+ setItem: (key, value) => {
110
+ backing.set(key, value);
111
+ },
112
+ removeItem: (key) => {
113
+ backing.delete(key);
114
+ }
115
+ };
116
+ }
117
+ var NOSTORE = {
118
+ getItem: () => null,
119
+ setItem: () => {
120
+ },
121
+ removeItem: () => {
122
+ }
123
+ };
124
+ function resolveStorage(mode) {
125
+ const resolved = mode ?? (isBrowser() ? "memory" : "nostore");
126
+ if (typeof resolved === "object") return resolved;
127
+ switch (resolved) {
128
+ case "memory":
129
+ return memoryStorage();
130
+ case "nostore":
131
+ return NOSTORE;
132
+ default:
133
+ throw new Error(`Unknown storage mode '${String(resolved)}'`);
134
+ }
135
+ }
136
+ async function writeSession(storage, session) {
137
+ await storage.setItem(ACCESS_TOKEN_KEY, session.accessToken);
138
+ await storage.setItem(REFRESH_TOKEN_KEY, session.refreshToken);
139
+ }
140
+ async function clearSession(storage) {
141
+ await storage.removeItem(ACCESS_TOKEN_KEY);
142
+ await storage.removeItem(REFRESH_TOKEN_KEY);
143
+ }
144
+
145
+ // src/version.ts
146
+ var SDK_VERSION = "1.0.0";
147
+
148
+ // src/client.ts
149
+ function isRawBody(body) {
150
+ return typeof body === "string" || body instanceof URLSearchParams || typeof FormData !== "undefined" && body instanceof FormData || typeof Blob !== "undefined" && body instanceof Blob || body instanceof ArrayBuffer || typeof ReadableStream !== "undefined" && body instanceof ReadableStream;
151
+ }
152
+ var BoardClient = class {
153
+ storage;
154
+ basePath;
155
+ options;
156
+ constructor(options) {
157
+ this.options = options;
158
+ this.storage = options.storage;
159
+ const baseUrl = options.baseUrl.replace(/\/+$/, "");
160
+ this.basePath = `${baseUrl}/v1/boards/${encodeURIComponent(options.board)}`;
161
+ }
162
+ /**
163
+ * The full request pipeline. Public and first-class: custom endpoints
164
+ * work without an SDK release, and still get the board-identifier
165
+ * base path, default headers, bearer token, and both hooks.
166
+ *
167
+ * @example
168
+ * const stats = await board.client.fetch<MyShape>('/custom/stats');
169
+ */
170
+ async fetch(path, init = {}) {
171
+ const { body, query, headers: callHeaders, ...passthrough } = init;
172
+ const method = (init.method ?? "GET").toUpperCase();
173
+ let url = `${this.basePath}${path}`;
174
+ const params = toSearchParams(query);
175
+ const search = params.toString();
176
+ if (search) url += `?${search}`;
177
+ const jsonBody = body !== void 0 && !isRawBody(body);
178
+ const serializedBody = body === void 0 ? void 0 : jsonBody ? JSON.stringify(body) : body;
179
+ const headers = new Headers({ accept: "application/json" });
180
+ if (jsonBody) headers.set("content-type", "application/json");
181
+ for (const [key, value] of Object.entries(
182
+ this.options.globalHeaders ?? {}
183
+ )) {
184
+ headers.set(key, value);
185
+ }
186
+ const token = await this.storage.getItem(ACCESS_TOKEN_KEY);
187
+ if (token !== null) headers.set("authorization", `Bearer ${token}`);
188
+ if (callHeaders) {
189
+ new Headers(callHeaders).forEach((value, key) => {
190
+ headers.set(key, value);
191
+ });
192
+ }
193
+ if ((method !== "GET" || headers.has("authorization")) && !headers.has("x-cavuno-sdk")) {
194
+ headers.set("x-cavuno-sdk", `board@${SDK_VERSION}`);
195
+ }
196
+ let req = {
197
+ url,
198
+ init: { ...passthrough, method, headers, body: serializedBody }
199
+ };
200
+ if (this.options.onRequest) req = await this.options.onRequest(req);
201
+ this.options.logger?.debug(`${method} ${req.url}`);
202
+ const res = await globalThis.fetch(req.url, req.init);
203
+ this.options.logger?.debug(`${res.status} ${method} ${req.url}`);
204
+ if (this.options.onResponse) await this.options.onResponse(res.clone(), req);
205
+ if (res.status === 204) return void 0;
206
+ if (res.ok) return await res.json();
207
+ let parsed;
208
+ try {
209
+ parsed = await res.json();
210
+ } catch {
211
+ parsed = void 0;
212
+ }
213
+ const envelope = parsed?.error;
214
+ const error = envelope && typeof envelope.code === "string" && typeof envelope.message === "string" ? new BoardApiError({
215
+ status: res.status,
216
+ code: envelope.code,
217
+ message: envelope.message,
218
+ details: envelope.details,
219
+ requestId: envelope.requestId,
220
+ raw: parsed
221
+ }) : new BoardApiError({
222
+ status: res.status,
223
+ code: "unknown_error",
224
+ message: res.statusText || "Request failed",
225
+ raw: parsed
226
+ });
227
+ this.options.logger?.error(
228
+ `${error.status} ${error.code} ${method} ${req.url}`
229
+ );
230
+ throw error;
231
+ }
232
+ };
233
+
234
+ // src/namespaces/auth.ts
235
+ function authNamespace(client) {
236
+ async function persist(session) {
237
+ await writeSession(client.storage, session);
238
+ return session;
239
+ }
240
+ async function resolveRefreshToken(body) {
241
+ if (body?.refreshToken) return body.refreshToken;
242
+ const stored = await client.storage.getItem(REFRESH_TOKEN_KEY);
243
+ if (stored === null) {
244
+ throw new Error(
245
+ "No refresh token available \u2014 pass { refreshToken } or configure auth.storage"
246
+ );
247
+ }
248
+ return stored;
249
+ }
250
+ return {
251
+ /**
252
+ * Register a board user (emailpass). Persists the returned token
253
+ * pair to storage and returns the session.
254
+ *
255
+ * @example
256
+ * await board.auth.register({
257
+ * role: 'candidate',
258
+ * method: 'emailpass',
259
+ * email: 'a@b.com',
260
+ * password: 'hunter22',
261
+ * displayName: 'Ada',
262
+ * });
263
+ */
264
+ async register(body, options) {
265
+ const session = await client.fetch("/auth/register", {
266
+ ...options,
267
+ method: "POST",
268
+ body
269
+ });
270
+ return persist(session);
271
+ },
272
+ /**
273
+ * Log in with email + password. Persists the returned token pair to
274
+ * storage and returns the session. The SDK never navigates — the
275
+ * host app owns any redirect/verification UX.
276
+ *
277
+ * @example
278
+ * const { boardUser } = await board.auth.login({
279
+ * email: 'a@b.com',
280
+ * password: 'hunter22',
281
+ * });
282
+ */
283
+ async login(body, options) {
284
+ const session = await client.fetch("/auth/login", {
285
+ ...options,
286
+ method: "POST",
287
+ body
288
+ });
289
+ return persist(session);
290
+ },
291
+ /**
292
+ * Rotate the refresh token for a new bearer pair. Refresh tokens
293
+ * are single-use; the rotated pair is persisted to storage. There
294
+ * is no automatic refresh on 401 — call this explicitly.
295
+ *
296
+ * @example
297
+ * await board.auth.refresh(); // uses the stored refresh token
298
+ */
299
+ async refresh(body, options) {
300
+ const refreshToken = await resolveRefreshToken(body);
301
+ const session = await client.fetch("/auth/refresh", {
302
+ ...options,
303
+ method: "POST",
304
+ body: { refreshToken }
305
+ });
306
+ return persist(session);
307
+ },
308
+ /**
309
+ * Revoke the refresh token and clear stored tokens. Idempotent
310
+ * server-side (204 even for unknown tokens). If the request itself
311
+ * fails (network error, 5xx), stored tokens are deliberately kept —
312
+ * clearing them would leave a live refresh token server-side while
313
+ * the client believes it's logged out. Catch and retry, or clear
314
+ * storage yourself to force a local logout.
315
+ *
316
+ * @example
317
+ * await board.auth.logout();
318
+ */
319
+ async logout(body, options) {
320
+ const refreshToken = await resolveRefreshToken(body);
321
+ await client.fetch("/auth/logout", {
322
+ ...options,
323
+ method: "POST",
324
+ body: { refreshToken }
325
+ });
326
+ await clearSession(client.storage);
327
+ },
328
+ /**
329
+ * Confirm an email-verification token (sent by register).
330
+ *
331
+ * @example
332
+ * await board.auth.verifyEmail({ token });
333
+ */
334
+ verifyEmail(body, options) {
335
+ return client.fetch("/auth/verify-email", {
336
+ ...options,
337
+ method: "POST",
338
+ body
339
+ });
340
+ },
341
+ /**
342
+ * Request a password-reset email. Always resolves (204) whether or
343
+ * not the email exists — no account oracle.
344
+ *
345
+ * @example
346
+ * await board.auth.forgotPassword({ email: 'a@b.com' });
347
+ */
348
+ forgotPassword(body, options) {
349
+ return client.fetch("/auth/forgot-password", {
350
+ ...options,
351
+ method: "POST",
352
+ body
353
+ });
354
+ },
355
+ /**
356
+ * Set a new password with a reset token. The token is single-use;
357
+ * all existing sessions are invalidated server-side.
358
+ *
359
+ * @example
360
+ * await board.auth.resetPassword({ token, password: 'newpass99' });
361
+ */
362
+ resetPassword(body, options) {
363
+ return client.fetch("/auth/reset-password", {
364
+ ...options,
365
+ method: "POST",
366
+ body
367
+ });
368
+ }
369
+ };
370
+ }
371
+
372
+ // src/namespaces/blog.ts
373
+ function blogNamespace(client) {
374
+ return {
375
+ posts: {
376
+ /**
377
+ * List published posts (summaries — no `html`).
378
+ *
379
+ * @example
380
+ * const { data } = await board.blog.posts.list({ tagSlug: 'news' });
381
+ */
382
+ list(query, options) {
383
+ return client.fetch(
384
+ "/blog/posts",
385
+ { ...options, query }
386
+ );
387
+ },
388
+ /**
389
+ * Retrieve one post by slug, including its `html` body.
390
+ *
391
+ * @example
392
+ * const post = await board.blog.posts.retrieve('hello-world');
393
+ */
394
+ retrieve(postSlug, query, options) {
395
+ return client.fetch(
396
+ `/blog/posts/${encodeURIComponent(postSlug)}`,
397
+ { ...options, query }
398
+ );
399
+ }
400
+ },
401
+ tags: {
402
+ /** @example const { data } = await board.blog.tags.list(); */
403
+ list(query, options) {
404
+ return client.fetch("/blog/tags", {
405
+ ...options,
406
+ query
407
+ });
408
+ },
409
+ /** @example const tag = await board.blog.tags.retrieve('news'); */
410
+ retrieve(tagSlug, query, options) {
411
+ return client.fetch(
412
+ `/blog/tags/${encodeURIComponent(tagSlug)}`,
413
+ { ...options, query }
414
+ );
415
+ }
416
+ },
417
+ authors: {
418
+ /** @example const { data } = await board.blog.authors.list(); */
419
+ list(query, options) {
420
+ return client.fetch("/blog/authors", {
421
+ ...options,
422
+ query
423
+ });
424
+ },
425
+ /** @example const author = await board.blog.authors.retrieve('jane'); */
426
+ retrieve(authorSlug, query, options) {
427
+ return client.fetch(
428
+ `/blog/authors/${encodeURIComponent(authorSlug)}`,
429
+ { ...options, query }
430
+ );
431
+ }
432
+ },
433
+ /**
434
+ * Free-text post search (returns post summaries).
435
+ *
436
+ * @example
437
+ * const { data } = await board.blog.search({ query: 'launch' });
438
+ */
439
+ search(body, query, options) {
440
+ return client.fetch(
441
+ "/blog/search",
442
+ { ...options, method: "POST", body, query }
443
+ );
444
+ }
445
+ };
446
+ }
447
+
448
+ // src/namespaces/companies.ts
449
+ function companiesNamespace(client) {
450
+ return {
451
+ /**
452
+ * List companies on the board.
453
+ *
454
+ * @example
455
+ * const { data } = await board.companies.list({ limit: 20 });
456
+ */
457
+ list(query, options) {
458
+ return client.fetch("/companies", {
459
+ ...options,
460
+ query
461
+ });
462
+ },
463
+ /**
464
+ * Retrieve one company by slug.
465
+ *
466
+ * @example
467
+ * const company = await board.companies.retrieve('acme');
468
+ */
469
+ retrieve(companySlug, query, options) {
470
+ return client.fetch(
471
+ `/companies/${encodeURIComponent(companySlug)}`,
472
+ { ...options, query }
473
+ );
474
+ },
475
+ /**
476
+ * Free-text company search.
477
+ *
478
+ * @example
479
+ * const { data } = await board.companies.search({ query: 'acme' });
480
+ */
481
+ search(body, query, options) {
482
+ return client.fetch("/companies/search", {
483
+ ...options,
484
+ method: "POST",
485
+ body,
486
+ query
487
+ });
488
+ },
489
+ /**
490
+ * List one company's published jobs.
491
+ *
492
+ * @example
493
+ * const { data } = await board.companies.listJobs('acme', { limit: 10 });
494
+ */
495
+ listJobs(companySlug, query, options) {
496
+ return client.fetch(
497
+ `/companies/${encodeURIComponent(companySlug)}/jobs`,
498
+ { ...options, query }
499
+ );
500
+ }
501
+ };
502
+ }
503
+
504
+ // src/namespaces/jobs.ts
505
+ function jobsNamespace(client) {
506
+ return {
507
+ /**
508
+ * List published jobs.
509
+ *
510
+ * @example
511
+ * const { data, nextCursor } = await board.jobs.list({ limit: 20 });
512
+ */
513
+ list(query, options) {
514
+ return client.fetch("/jobs", {
515
+ ...options,
516
+ query
517
+ });
518
+ },
519
+ /**
520
+ * Retrieve one published job by slug.
521
+ *
522
+ * @example
523
+ * const job = await board.jobs.retrieve('senior-chef');
524
+ */
525
+ retrieve(jobSlug, query, options) {
526
+ return client.fetch(`/jobs/${encodeURIComponent(jobSlug)}`, {
527
+ ...options,
528
+ query
529
+ });
530
+ },
531
+ /**
532
+ * Free-text + faceted job search.
533
+ *
534
+ * @example
535
+ * const { data } = await board.jobs.search({
536
+ * query: 'chef',
537
+ * filters: { seniority: ['senior'] },
538
+ * });
539
+ */
540
+ search(body, query, options) {
541
+ return client.fetch("/jobs/search", {
542
+ ...options,
543
+ method: "POST",
544
+ body,
545
+ query
546
+ });
547
+ }
548
+ };
549
+ }
550
+
551
+ // src/namespaces/me.ts
552
+ function meNamespace(client) {
553
+ return {
554
+ /**
555
+ * Retrieve the authenticated board user.
556
+ *
557
+ * @example
558
+ * const me = await board.me.retrieve();
559
+ */
560
+ // `query?: Record<string, never>` = no query params today; the slot
561
+ // holds the locked (id, query?, options?) shape and widens to a real
562
+ // query type without breaking callers when the API grows one.
563
+ retrieve(query, options) {
564
+ return client.fetch("/me", { ...options, query });
565
+ },
566
+ savedJobs: {
567
+ /**
568
+ * List the authenticated user's saved jobs (each embeds the full
569
+ * `public_job`).
570
+ *
571
+ * @example
572
+ * const { data } = await board.me.savedJobs.list({ limit: 20 });
573
+ */
574
+ list(query, options) {
575
+ return client.fetch("/me/saved-jobs", {
576
+ ...options,
577
+ query
578
+ });
579
+ },
580
+ /**
581
+ * Save a job. Converges — saving an already-saved job returns the
582
+ * identical row.
583
+ *
584
+ * @example
585
+ * await board.me.savedJobs.save({ jobId: job.id });
586
+ */
587
+ save(body, query, options) {
588
+ return client.fetch("/me/saved-jobs", {
589
+ ...options,
590
+ method: "POST",
591
+ body,
592
+ query
593
+ });
594
+ },
595
+ /**
596
+ * Unsave a job. Idempotent — unknown IDs still 204.
597
+ *
598
+ * @example
599
+ * await board.me.savedJobs.unsave(job.id);
600
+ */
601
+ unsave(jobId, query, options) {
602
+ return client.fetch(
603
+ `/me/saved-jobs/${encodeURIComponent(jobId)}`,
604
+ { ...options, method: "DELETE", query }
605
+ );
606
+ }
607
+ }
608
+ };
609
+ }
610
+
611
+ // src/index.ts
612
+ function createBoardClient(options) {
613
+ const client = new BoardClient({
614
+ baseUrl: options.baseUrl,
615
+ board: options.board,
616
+ storage: resolveStorage(options.auth?.storage),
617
+ globalHeaders: options.globalHeaders,
618
+ onRequest: options.onRequest,
619
+ onResponse: options.onResponse,
620
+ logger: options.logger
621
+ });
622
+ return {
623
+ /**
624
+ * Escape hatch: raw typed request through the full pipeline (board
625
+ * base path, default headers, bearer token, hooks). Custom
626
+ * endpoints work without an SDK release.
627
+ */
628
+ client,
629
+ /**
630
+ * Board context — identity, features, analytics, theme.
631
+ *
632
+ * @example
633
+ * const { name, theme } = await board.context();
634
+ */
635
+ context(options2) {
636
+ return client.fetch("", options2);
637
+ },
638
+ jobs: jobsNamespace(client),
639
+ companies: companiesNamespace(client),
640
+ blog: blogNamespace(client),
641
+ auth: authNamespace(client),
642
+ me: meNamespace(client)
643
+ };
644
+ }