@boldvideo/bold-js 1.17.0 → 1.19.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/CHANGELOG.md CHANGED
@@ -1,5 +1,67 @@
1
1
  # @boldvideo/bold-js
2
2
 
3
+ ## 1.19.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d423437: Redesign Community API types to eliminate N+1 queries
8
+
9
+ **Breaking changes to Post type:**
10
+
11
+ - `viewer` → `author` (creator of the post)
12
+ - `reactionsCount` → `reactions.count`
13
+ - `commentsCount` → `comments.count`
14
+ - `viewerReacted` → `reactions.viewer_has_reacted`
15
+ - `createdAt` → `created_at`
16
+
17
+ **New types:**
18
+
19
+ - `UserSummary` - minimal profile with `id`, `name`, `avatar_url`
20
+ - `ReactionSummary` - `{ count, reacted_by[], viewer_has_reacted? }`
21
+ - `CommentSummary` - `{ count, commented_by[], items? }`
22
+ - `CommentThread` and `Reply` - structured comment threading
23
+ - `PaginationMeta` and `PaginatedResponse<T>` - page-based pagination
24
+
25
+ **Pagination changes:**
26
+
27
+ - `posts.list()` now uses `page`/`pageSize` instead of `limit`/`offset`
28
+ - Returns `PaginatedResponse<Post>` with `data[]` and `meta`
29
+
30
+ **Benefits:**
31
+
32
+ - List endpoint now includes `reacted_by` and `commented_by` arrays (up to 10 users)
33
+ - Enables avatar display without fetching each post individually
34
+ - Consistent object shapes between list and detail views
35
+
36
+ ## 1.18.0
37
+
38
+ ### Minor Changes
39
+
40
+ - 48d8b40: Add Community API for posts, comments, and reactions
41
+
42
+ New `bold.community` namespace with full CRUD operations:
43
+
44
+ **Posts:**
45
+
46
+ - `bold.community.posts.list(opts?)` - List posts with category/limit/offset filters
47
+ - `bold.community.posts.get(id, viewerId?)` - Get single post with comments
48
+ - `bold.community.posts.create(viewerId, data)` - Create a post (markdown supported)
49
+ - `bold.community.posts.update(viewerId, id, data)` - Update post (owner/admin only)
50
+ - `bold.community.posts.delete(viewerId, id)` - Delete post (owner/admin only)
51
+ - `bold.community.posts.react(viewerId, id)` - Toggle reaction (like/unlike)
52
+
53
+ **Comments:**
54
+
55
+ - `bold.community.comments.create(viewerId, postId, data)` - Create comment with optional `parentId` for nested replies
56
+ - `bold.community.comments.delete(viewerId, id)` - Delete comment (owner/admin only)
57
+ - `bold.community.comments.react(viewerId, id)` - Toggle reaction
58
+
59
+ **New Types:**
60
+
61
+ - `Post`, `PostAuthor`, `Comment`, `ReactionResponse`
62
+ - `ListPostsOptions`, `CreatePostData`, `UpdatePostData`, `CreateCommentData`
63
+ - `CommunityAPIError` - Error class for Community API operations
64
+
3
65
  ## 1.17.0
4
66
 
5
67
  ### Minor Changes
package/README.md CHANGED
@@ -178,6 +178,69 @@ console.log(`Completed ${meta.completed} of ${meta.total} videos`);
178
178
 
179
179
  ---
180
180
 
181
+ ## Community API
182
+
183
+ Build community features with posts, comments, and reactions. All write operations require a `viewerId` (the viewer performing the action).
184
+
185
+ ### Posts
186
+
187
+ ```typescript
188
+ // List posts (optionally filter by category)
189
+ const { data: posts } = await bold.community.posts.list({
190
+ category: 'announcements',
191
+ limit: 20,
192
+ offset: 0,
193
+ viewerId: 'viewer-uuid' // Include viewerReacted in response
194
+ });
195
+
196
+ // Get a single post with comments
197
+ const { data: post } = await bold.community.posts.get('post-id', 'viewer-uuid');
198
+
199
+ // Create a post (requires viewerId)
200
+ const { data: newPost } = await bold.community.posts.create('viewer-uuid', {
201
+ content: 'Hello community! **Markdown** supported.',
202
+ category: 'general'
203
+ });
204
+
205
+ // Update a post (owner or admin only)
206
+ await bold.community.posts.update('viewer-uuid', 'post-id', {
207
+ content: 'Updated content'
208
+ });
209
+
210
+ // Delete a post (owner or admin only)
211
+ await bold.community.posts.delete('viewer-uuid', 'post-id');
212
+
213
+ // React to a post (toggle like/unlike)
214
+ const reaction = await bold.community.posts.react('viewer-uuid', 'post-id');
215
+ console.log(reaction.reacted, reaction.reactionsCount);
216
+ ```
217
+
218
+ ### Comments
219
+
220
+ ```typescript
221
+ // Create a comment on a post
222
+ const { data: comment } = await bold.community.comments.create(
223
+ 'viewer-uuid',
224
+ 'post-id',
225
+ { content: 'Great post!' }
226
+ );
227
+
228
+ // Reply to a comment (nested)
229
+ const { data: reply } = await bold.community.comments.create(
230
+ 'viewer-uuid',
231
+ 'post-id',
232
+ { content: 'I agree!', parentId: 'parent-comment-id' }
233
+ );
234
+
235
+ // Delete a comment (owner or admin only)
236
+ await bold.community.comments.delete('viewer-uuid', 'comment-id');
237
+
238
+ // React to a comment (toggle)
239
+ const reaction = await bold.community.comments.react('viewer-uuid', 'comment-id');
240
+ ```
241
+
242
+ ---
243
+
181
244
  ## AI Methods
182
245
 
183
246
  All AI methods support both streaming (default) and non-streaming modes.
@@ -378,7 +441,16 @@ import type {
378
441
  ListProgressOptions,
379
442
  ListVideosOptions,
380
443
  ListVideosLatestOptions,
381
- ListVideosIndexOptions
444
+ ListVideosIndexOptions,
445
+ // Community API
446
+ Post,
447
+ PostAuthor,
448
+ Comment,
449
+ ReactionResponse,
450
+ ListPostsOptions,
451
+ CreatePostData,
452
+ UpdatePostData,
453
+ CreateCommentData
382
454
  } from '@boldvideo/bold-js';
383
455
  ```
384
456
 
package/dist/index.cjs CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
+ CommunityAPIError: () => CommunityAPIError,
33
34
  DEFAULT_API_BASE_URL: () => DEFAULT_API_BASE_URL,
34
35
  DEFAULT_INTERNAL_API_BASE_URL: () => DEFAULT_INTERNAL_API_BASE_URL,
35
36
  ViewerAPIError: () => ViewerAPIError,
@@ -38,7 +39,7 @@ __export(src_exports, {
38
39
  module.exports = __toCommonJS(src_exports);
39
40
 
40
41
  // src/lib/client.ts
41
- var import_axios2 = __toESM(require("axios"), 1);
42
+ var import_axios3 = __toESM(require("axios"), 1);
42
43
 
43
44
  // src/util/camelize.ts
44
45
  var isPlainObject = (value) => value !== null && typeof value === "object" && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
@@ -330,6 +331,188 @@ function saveProgress(client) {
330
331
  };
331
332
  }
332
333
 
334
+ // src/lib/community.ts
335
+ var import_axios2 = require("axios");
336
+ var CommunityAPIError = class extends Error {
337
+ constructor(method, url, error) {
338
+ var __super = (...args) => {
339
+ super(...args);
340
+ };
341
+ if (error instanceof import_axios2.AxiosError) {
342
+ const status = error.response?.status;
343
+ const message = error.response?.data?.error || error.message;
344
+ __super(`${method} ${url} failed (${status}): ${message}`);
345
+ this.status = status;
346
+ this.originalError = error;
347
+ } else if (error instanceof Error) {
348
+ __super(`${method} ${url} failed: ${error.message}`);
349
+ this.originalError = error;
350
+ } else {
351
+ __super(`${method} ${url} failed: ${String(error)}`);
352
+ }
353
+ this.name = "CommunityAPIError";
354
+ }
355
+ };
356
+ function requireViewerId(viewerId) {
357
+ if (!viewerId)
358
+ throw new Error("Viewer ID is required (X-Viewer-ID)");
359
+ }
360
+ function viewerHeaders(viewerId) {
361
+ return viewerId ? { "X-Viewer-ID": viewerId } : void 0;
362
+ }
363
+ function toQuery2(params) {
364
+ const qs = new URLSearchParams();
365
+ for (const [k, v] of Object.entries(params)) {
366
+ if (v !== void 0 && v !== null)
367
+ qs.set(k, String(v));
368
+ }
369
+ const s = qs.toString();
370
+ return s ? `?${s}` : "";
371
+ }
372
+ async function get3(client, url, viewerId) {
373
+ try {
374
+ const res = await client.get(url, { headers: viewerHeaders(viewerId) });
375
+ return camelizeKeys(res.data);
376
+ } catch (error) {
377
+ throw new CommunityAPIError("GET", url, error);
378
+ }
379
+ }
380
+ async function post2(client, url, data, viewerId) {
381
+ try {
382
+ const res = await client.post(url, data, { headers: viewerHeaders(viewerId) });
383
+ return camelizeKeys(res.data);
384
+ } catch (error) {
385
+ throw new CommunityAPIError("POST", url, error);
386
+ }
387
+ }
388
+ async function put(client, url, data, viewerId) {
389
+ try {
390
+ const res = await client.put(url, data, { headers: viewerHeaders(viewerId) });
391
+ return camelizeKeys(res.data);
392
+ } catch (error) {
393
+ throw new CommunityAPIError("PUT", url, error);
394
+ }
395
+ }
396
+ async function del(client, url, viewerId) {
397
+ try {
398
+ const res = await client.delete(url, { headers: viewerHeaders(viewerId) });
399
+ return camelizeKeys(res.data);
400
+ } catch (error) {
401
+ throw new CommunityAPIError("DELETE", url, error);
402
+ }
403
+ }
404
+ function listPosts(client) {
405
+ return async (opts = {}) => {
406
+ return get3(
407
+ client,
408
+ `community/posts${toQuery2({
409
+ category: opts.category,
410
+ page: opts.page,
411
+ page_size: opts.pageSize
412
+ })}`,
413
+ opts.viewerId
414
+ );
415
+ };
416
+ }
417
+ function getPost(client) {
418
+ return async (id, viewerId) => {
419
+ if (!id)
420
+ throw new Error("Post ID is required");
421
+ return get3(client, `community/posts/${id}`, viewerId);
422
+ };
423
+ }
424
+ function createPost(client) {
425
+ return async (viewerId, data) => {
426
+ requireViewerId(viewerId);
427
+ if (!data?.content)
428
+ throw new Error("Post content is required");
429
+ return post2(
430
+ client,
431
+ "community/posts",
432
+ { post: { content: data.content, category: data.category } },
433
+ viewerId
434
+ );
435
+ };
436
+ }
437
+ function updatePost(client) {
438
+ return async (viewerId, id, data) => {
439
+ requireViewerId(viewerId);
440
+ if (!id)
441
+ throw new Error("Post ID is required");
442
+ const body = {};
443
+ if (data.content !== void 0)
444
+ body.content = data.content;
445
+ if (data.category !== void 0)
446
+ body.category = data.category;
447
+ return put(
448
+ client,
449
+ `community/posts/${id}`,
450
+ { post: body },
451
+ viewerId
452
+ );
453
+ };
454
+ }
455
+ function deletePost(client) {
456
+ return async (viewerId, id) => {
457
+ requireViewerId(viewerId);
458
+ if (!id)
459
+ throw new Error("Post ID is required");
460
+ return del(client, `community/posts/${id}`, viewerId);
461
+ };
462
+ }
463
+ function reactToPost(client) {
464
+ return async (viewerId, id) => {
465
+ requireViewerId(viewerId);
466
+ if (!id)
467
+ throw new Error("Post ID is required");
468
+ return post2(
469
+ client,
470
+ `community/posts/${id}/react`,
471
+ void 0,
472
+ viewerId
473
+ );
474
+ };
475
+ }
476
+ function createComment(client) {
477
+ return async (viewerId, postId, data) => {
478
+ requireViewerId(viewerId);
479
+ if (!postId)
480
+ throw new Error("Post ID is required");
481
+ if (!data?.content)
482
+ throw new Error("Comment content is required");
483
+ const body = { content: data.content };
484
+ if (data.parentId)
485
+ body.parent_id = data.parentId;
486
+ return post2(
487
+ client,
488
+ `community/posts/${postId}/comments`,
489
+ { comment: body },
490
+ viewerId
491
+ );
492
+ };
493
+ }
494
+ function deleteComment(client) {
495
+ return async (viewerId, id) => {
496
+ requireViewerId(viewerId);
497
+ if (!id)
498
+ throw new Error("Comment ID is required");
499
+ return del(client, `community/comments/${id}`, viewerId);
500
+ };
501
+ }
502
+ function reactToComment(client) {
503
+ return async (viewerId, id) => {
504
+ requireViewerId(viewerId);
505
+ if (!id)
506
+ throw new Error("Comment ID is required");
507
+ return post2(
508
+ client,
509
+ `community/comments/${id}/react`,
510
+ void 0,
511
+ viewerId
512
+ );
513
+ };
514
+ }
515
+
333
516
  // src/util/throttle.ts
334
517
  var throttle = (fn, delay) => {
335
518
  let wait = false;
@@ -631,7 +814,7 @@ function createClient(apiKey, options = {}) {
631
814
  };
632
815
  let apiClient;
633
816
  try {
634
- apiClient = import_axios2.default.create(apiClientOptions);
817
+ apiClient = import_axios3.default.create(apiClientOptions);
635
818
  } catch (error) {
636
819
  console.error("Error creating API client", error);
637
820
  throw error;
@@ -663,12 +846,28 @@ function createClient(apiKey, options = {}) {
663
846
  saveProgress: saveProgress(apiClient)
664
847
  },
665
848
  ai: createAI(aiConfig),
849
+ community: {
850
+ posts: {
851
+ list: listPosts(apiClient),
852
+ get: getPost(apiClient),
853
+ create: createPost(apiClient),
854
+ update: updatePost(apiClient),
855
+ delete: deletePost(apiClient),
856
+ react: reactToPost(apiClient)
857
+ },
858
+ comments: {
859
+ create: createComment(apiClient),
860
+ delete: deleteComment(apiClient),
861
+ react: reactToComment(apiClient)
862
+ }
863
+ },
666
864
  trackEvent: trackEvent(apiClient, userId, { debug }),
667
865
  trackPageView: trackPageView(apiClient, userId, { debug })
668
866
  };
669
867
  }
670
868
  // Annotate the CommonJS export names for ESM import in node:
671
869
  0 && (module.exports = {
870
+ CommunityAPIError,
672
871
  DEFAULT_API_BASE_URL,
673
872
  DEFAULT_INTERNAL_API_BASE_URL,
674
873
  ViewerAPIError,
package/dist/index.d.ts CHANGED
@@ -511,6 +511,173 @@ type ListVideosOptions = (ListVideosLatestOptions & {
511
511
  limit?: never;
512
512
  viewerId?: never;
513
513
  });
514
+ /**
515
+ * Minimal profile object for list contexts
516
+ */
517
+ type UserSummary = {
518
+ id: string;
519
+ name: string;
520
+ avatar_url: string | null;
521
+ };
522
+ /**
523
+ * Author information for posts and comments (extended for admin context)
524
+ */
525
+ type PostAuthor = UserSummary & {
526
+ email?: string;
527
+ isAdmin?: boolean;
528
+ };
529
+ /**
530
+ * Reaction summary with users who reacted
531
+ */
532
+ type ReactionSummary = {
533
+ count: number;
534
+ /** Users who reacted (capped to 10 from API) */
535
+ reacted_by: UserSummary[];
536
+ /** Present when viewer context exists */
537
+ viewer_has_reacted?: boolean;
538
+ };
539
+ /**
540
+ * Comment summary with users who commented
541
+ */
542
+ type CommentSummary = {
543
+ count: number;
544
+ /** Users who commented (capped to 10 from API) */
545
+ commented_by: UserSummary[];
546
+ /** Full comment threads (present in detail only) */
547
+ items?: CommentThread[];
548
+ };
549
+ /**
550
+ * A reply to a comment
551
+ */
552
+ type Reply = {
553
+ id: string;
554
+ content: string;
555
+ created_at: string;
556
+ author: UserSummary;
557
+ parent_comment_id: string;
558
+ };
559
+ /**
560
+ * A top-level comment with replies
561
+ */
562
+ type CommentThread = {
563
+ id: string;
564
+ content: string;
565
+ created_at: string;
566
+ author: UserSummary;
567
+ replies: Reply[];
568
+ };
569
+ /**
570
+ * @deprecated Use CommentThread instead
571
+ * A comment on a community post (legacy format)
572
+ */
573
+ type Comment = {
574
+ id: string;
575
+ content: string;
576
+ /** Nesting depth (0 = top-level) */
577
+ depth: number;
578
+ reactionsCount: number;
579
+ /** Whether current viewer has reacted (only present when X-Viewer-ID header sent) */
580
+ viewerReacted?: boolean;
581
+ /** @deprecated Use author instead */
582
+ viewer: PostAuthor;
583
+ author: PostAuthor;
584
+ /** Nested replies */
585
+ replies: Comment[];
586
+ createdAt: string;
587
+ updatedAt: string;
588
+ };
589
+ /**
590
+ * A community post (list view)
591
+ */
592
+ type Post = {
593
+ id: string;
594
+ content: string;
595
+ category?: string;
596
+ pinned?: boolean;
597
+ pinnedAt?: string | null;
598
+ created_at: string;
599
+ author: PostAuthor;
600
+ reactions: ReactionSummary;
601
+ comments: CommentSummary;
602
+ /** @deprecated Use created_at */
603
+ createdAt?: string;
604
+ /** @deprecated Use reactions.count */
605
+ reactionsCount?: number;
606
+ /** @deprecated Use comments.count */
607
+ commentsCount?: number;
608
+ /** @deprecated Use reactions.viewer_has_reacted */
609
+ viewerReacted?: boolean;
610
+ /** @deprecated Use author */
611
+ viewer?: PostAuthor;
612
+ updatedAt?: string;
613
+ };
614
+ /**
615
+ * Response from react endpoints (toggle)
616
+ */
617
+ type ReactionResponse = {
618
+ reacted: boolean;
619
+ reactionsCount: number;
620
+ };
621
+ /**
622
+ * Pagination metadata from API
623
+ */
624
+ type PaginationMeta = {
625
+ /** Current page (1-indexed) */
626
+ page: number;
627
+ /** Items per page */
628
+ page_size: number;
629
+ /** Total items across all pages */
630
+ total_entries: number;
631
+ /** Total number of pages */
632
+ total_pages: number;
633
+ };
634
+ /**
635
+ * Paginated response wrapper
636
+ */
637
+ type PaginatedResponse<T> = {
638
+ data: T[];
639
+ meta: PaginationMeta;
640
+ };
641
+ /**
642
+ * Options for listing community posts
643
+ */
644
+ type ListPostsOptions = {
645
+ /** Filter by category (e.g., "announcements", "general") */
646
+ category?: string;
647
+ /** Page number (1-indexed, default: 1) */
648
+ page?: number;
649
+ /** Items per page (default: 20, max: 100) */
650
+ pageSize?: number;
651
+ /** Viewer ID to include viewer_has_reacted in response */
652
+ viewerId?: string;
653
+ };
654
+ /**
655
+ * Data for creating a community post
656
+ */
657
+ type CreatePostData = {
658
+ /** Post content (supports markdown) */
659
+ content: string;
660
+ /** Category for the post */
661
+ category?: string;
662
+ };
663
+ /**
664
+ * Data for updating a community post
665
+ */
666
+ type UpdatePostData = {
667
+ /** Post content (supports markdown) */
668
+ content?: string;
669
+ /** Category for the post */
670
+ category?: string;
671
+ };
672
+ /**
673
+ * Data for creating a comment
674
+ */
675
+ type CreateCommentData = {
676
+ /** Comment content */
677
+ content: string;
678
+ /** Parent comment ID for replies */
679
+ parentId?: string;
680
+ };
514
681
 
515
682
  /**
516
683
  * AI client interface for type-safe method overloading
@@ -693,6 +860,33 @@ declare function createClient(apiKey: string, options?: ClientOptions): {
693
860
  }>;
694
861
  };
695
862
  ai: AIClient;
863
+ community: {
864
+ posts: {
865
+ list: (opts?: ListPostsOptions) => Promise<PaginatedResponse<Post>>;
866
+ get: (id: string, viewerId?: string | undefined) => Promise<{
867
+ data: Post;
868
+ }>;
869
+ create: (viewerId: string, data: CreatePostData) => Promise<{
870
+ data: Post;
871
+ }>;
872
+ update: (viewerId: string, id: string, data: UpdatePostData) => Promise<{
873
+ data: Post;
874
+ }>;
875
+ delete: (viewerId: string, id: string) => Promise<{
876
+ data?: unknown;
877
+ }>;
878
+ react: (viewerId: string, id: string) => Promise<ReactionResponse>;
879
+ };
880
+ comments: {
881
+ create: (viewerId: string, postId: string, data: CreateCommentData) => Promise<{
882
+ data: Comment;
883
+ }>;
884
+ delete: (viewerId: string, id: string) => Promise<{
885
+ data?: unknown;
886
+ }>;
887
+ react: (viewerId: string, id: string) => Promise<ReactionResponse>;
888
+ };
889
+ };
696
890
  trackEvent: (video: any, event: Event) => void;
697
891
  trackPageView: (title: string) => void;
698
892
  };
@@ -706,4 +900,13 @@ declare const DEFAULT_API_BASE_URL = "https://app.boldvideo.io/api/v1/";
706
900
  */
707
901
  declare const DEFAULT_INTERNAL_API_BASE_URL = "https://app.boldvideo.io/i/v1/";
708
902
 
709
- export { AIContextMessage, AIEvent, AIResponse, AIUsage, Account, AccountAI, AnalyticsProvider, AskOptions, AssistantConfig, ChatOptions, Citation, ClientOptions, Conversation, ConversationMessage, ConversationMetadata, CreateViewerData, CustomRedirect, DEFAULT_API_BASE_URL, DEFAULT_INTERNAL_API_BASE_URL, ListProgressOptions, ListVideosIndexOptions, ListVideosLatestOptions, ListVideosOptions, MenuItem, Playlist, Portal, PortalDisplay, PortalHero, PortalLayout, PortalNavigation, PortalTheme, ProgressListMeta, RecommendOptions, RecommendResponse, Recommendation, RecommendationVideo, RecommendationsOptions, RecommendationsResponse, SaveProgressData, SearchOptions, Segment, Settings, Source, ThemeColors, ThemeConfig, UpdateViewerData, Video, VideoAttachment, VideoDownloadUrls, VideoMetadata, VideoSubtitles, VideoTranscript, Viewer, ViewerAPIError, ViewerLookupParams, ViewerProgress, createClient };
903
+ /**
904
+ * Error thrown by Community API operations
905
+ */
906
+ declare class CommunityAPIError extends Error {
907
+ readonly status?: number;
908
+ readonly originalError?: Error;
909
+ constructor(method: string, url: string, error: unknown);
910
+ }
911
+
912
+ export { AIContextMessage, AIEvent, AIResponse, AIUsage, Account, AccountAI, AnalyticsProvider, AskOptions, AssistantConfig, ChatOptions, Citation, ClientOptions, Comment, CommentSummary, CommentThread, CommunityAPIError, Conversation, ConversationMessage, ConversationMetadata, CreateCommentData, CreatePostData, CreateViewerData, CustomRedirect, DEFAULT_API_BASE_URL, DEFAULT_INTERNAL_API_BASE_URL, ListPostsOptions, ListProgressOptions, ListVideosIndexOptions, ListVideosLatestOptions, ListVideosOptions, MenuItem, PaginatedResponse, PaginationMeta, Playlist, Portal, PortalDisplay, PortalHero, PortalLayout, PortalNavigation, PortalTheme, Post, PostAuthor, ProgressListMeta, ReactionResponse, ReactionSummary, RecommendOptions, RecommendResponse, Recommendation, RecommendationVideo, RecommendationsOptions, RecommendationsResponse, Reply, SaveProgressData, SearchOptions, Segment, Settings, Source, ThemeColors, ThemeConfig, UpdatePostData, UpdateViewerData, UserSummary, Video, VideoAttachment, VideoDownloadUrls, VideoMetadata, VideoSubtitles, VideoTranscript, Viewer, ViewerAPIError, ViewerLookupParams, ViewerProgress, createClient };
package/dist/index.js CHANGED
@@ -291,6 +291,188 @@ function saveProgress(client) {
291
291
  };
292
292
  }
293
293
 
294
+ // src/lib/community.ts
295
+ import { AxiosError as AxiosError2 } from "axios";
296
+ var CommunityAPIError = class extends Error {
297
+ constructor(method, url, error) {
298
+ var __super = (...args) => {
299
+ super(...args);
300
+ };
301
+ if (error instanceof AxiosError2) {
302
+ const status = error.response?.status;
303
+ const message = error.response?.data?.error || error.message;
304
+ __super(`${method} ${url} failed (${status}): ${message}`);
305
+ this.status = status;
306
+ this.originalError = error;
307
+ } else if (error instanceof Error) {
308
+ __super(`${method} ${url} failed: ${error.message}`);
309
+ this.originalError = error;
310
+ } else {
311
+ __super(`${method} ${url} failed: ${String(error)}`);
312
+ }
313
+ this.name = "CommunityAPIError";
314
+ }
315
+ };
316
+ function requireViewerId(viewerId) {
317
+ if (!viewerId)
318
+ throw new Error("Viewer ID is required (X-Viewer-ID)");
319
+ }
320
+ function viewerHeaders(viewerId) {
321
+ return viewerId ? { "X-Viewer-ID": viewerId } : void 0;
322
+ }
323
+ function toQuery2(params) {
324
+ const qs = new URLSearchParams();
325
+ for (const [k, v] of Object.entries(params)) {
326
+ if (v !== void 0 && v !== null)
327
+ qs.set(k, String(v));
328
+ }
329
+ const s = qs.toString();
330
+ return s ? `?${s}` : "";
331
+ }
332
+ async function get3(client, url, viewerId) {
333
+ try {
334
+ const res = await client.get(url, { headers: viewerHeaders(viewerId) });
335
+ return camelizeKeys(res.data);
336
+ } catch (error) {
337
+ throw new CommunityAPIError("GET", url, error);
338
+ }
339
+ }
340
+ async function post2(client, url, data, viewerId) {
341
+ try {
342
+ const res = await client.post(url, data, { headers: viewerHeaders(viewerId) });
343
+ return camelizeKeys(res.data);
344
+ } catch (error) {
345
+ throw new CommunityAPIError("POST", url, error);
346
+ }
347
+ }
348
+ async function put(client, url, data, viewerId) {
349
+ try {
350
+ const res = await client.put(url, data, { headers: viewerHeaders(viewerId) });
351
+ return camelizeKeys(res.data);
352
+ } catch (error) {
353
+ throw new CommunityAPIError("PUT", url, error);
354
+ }
355
+ }
356
+ async function del(client, url, viewerId) {
357
+ try {
358
+ const res = await client.delete(url, { headers: viewerHeaders(viewerId) });
359
+ return camelizeKeys(res.data);
360
+ } catch (error) {
361
+ throw new CommunityAPIError("DELETE", url, error);
362
+ }
363
+ }
364
+ function listPosts(client) {
365
+ return async (opts = {}) => {
366
+ return get3(
367
+ client,
368
+ `community/posts${toQuery2({
369
+ category: opts.category,
370
+ page: opts.page,
371
+ page_size: opts.pageSize
372
+ })}`,
373
+ opts.viewerId
374
+ );
375
+ };
376
+ }
377
+ function getPost(client) {
378
+ return async (id, viewerId) => {
379
+ if (!id)
380
+ throw new Error("Post ID is required");
381
+ return get3(client, `community/posts/${id}`, viewerId);
382
+ };
383
+ }
384
+ function createPost(client) {
385
+ return async (viewerId, data) => {
386
+ requireViewerId(viewerId);
387
+ if (!data?.content)
388
+ throw new Error("Post content is required");
389
+ return post2(
390
+ client,
391
+ "community/posts",
392
+ { post: { content: data.content, category: data.category } },
393
+ viewerId
394
+ );
395
+ };
396
+ }
397
+ function updatePost(client) {
398
+ return async (viewerId, id, data) => {
399
+ requireViewerId(viewerId);
400
+ if (!id)
401
+ throw new Error("Post ID is required");
402
+ const body = {};
403
+ if (data.content !== void 0)
404
+ body.content = data.content;
405
+ if (data.category !== void 0)
406
+ body.category = data.category;
407
+ return put(
408
+ client,
409
+ `community/posts/${id}`,
410
+ { post: body },
411
+ viewerId
412
+ );
413
+ };
414
+ }
415
+ function deletePost(client) {
416
+ return async (viewerId, id) => {
417
+ requireViewerId(viewerId);
418
+ if (!id)
419
+ throw new Error("Post ID is required");
420
+ return del(client, `community/posts/${id}`, viewerId);
421
+ };
422
+ }
423
+ function reactToPost(client) {
424
+ return async (viewerId, id) => {
425
+ requireViewerId(viewerId);
426
+ if (!id)
427
+ throw new Error("Post ID is required");
428
+ return post2(
429
+ client,
430
+ `community/posts/${id}/react`,
431
+ void 0,
432
+ viewerId
433
+ );
434
+ };
435
+ }
436
+ function createComment(client) {
437
+ return async (viewerId, postId, data) => {
438
+ requireViewerId(viewerId);
439
+ if (!postId)
440
+ throw new Error("Post ID is required");
441
+ if (!data?.content)
442
+ throw new Error("Comment content is required");
443
+ const body = { content: data.content };
444
+ if (data.parentId)
445
+ body.parent_id = data.parentId;
446
+ return post2(
447
+ client,
448
+ `community/posts/${postId}/comments`,
449
+ { comment: body },
450
+ viewerId
451
+ );
452
+ };
453
+ }
454
+ function deleteComment(client) {
455
+ return async (viewerId, id) => {
456
+ requireViewerId(viewerId);
457
+ if (!id)
458
+ throw new Error("Comment ID is required");
459
+ return del(client, `community/comments/${id}`, viewerId);
460
+ };
461
+ }
462
+ function reactToComment(client) {
463
+ return async (viewerId, id) => {
464
+ requireViewerId(viewerId);
465
+ if (!id)
466
+ throw new Error("Comment ID is required");
467
+ return post2(
468
+ client,
469
+ `community/comments/${id}/react`,
470
+ void 0,
471
+ viewerId
472
+ );
473
+ };
474
+ }
475
+
294
476
  // src/util/throttle.ts
295
477
  var throttle = (fn, delay) => {
296
478
  let wait = false;
@@ -624,11 +806,27 @@ function createClient(apiKey, options = {}) {
624
806
  saveProgress: saveProgress(apiClient)
625
807
  },
626
808
  ai: createAI(aiConfig),
809
+ community: {
810
+ posts: {
811
+ list: listPosts(apiClient),
812
+ get: getPost(apiClient),
813
+ create: createPost(apiClient),
814
+ update: updatePost(apiClient),
815
+ delete: deletePost(apiClient),
816
+ react: reactToPost(apiClient)
817
+ },
818
+ comments: {
819
+ create: createComment(apiClient),
820
+ delete: deleteComment(apiClient),
821
+ react: reactToComment(apiClient)
822
+ }
823
+ },
627
824
  trackEvent: trackEvent(apiClient, userId, { debug }),
628
825
  trackPageView: trackPageView(apiClient, userId, { debug })
629
826
  };
630
827
  }
631
828
  export {
829
+ CommunityAPIError,
632
830
  DEFAULT_API_BASE_URL,
633
831
  DEFAULT_INTERNAL_API_BASE_URL,
634
832
  ViewerAPIError,
package/llms.txt CHANGED
@@ -111,6 +111,43 @@ const { data: progress, meta } = await bold.viewers.listProgress(viewer.id, { co
111
111
  console.log(`Completed ${meta.completed} of ${meta.total} videos`);
112
112
  ```
113
113
 
114
+ ## Community API
115
+
116
+ Build community features with posts, comments, and reactions. Write operations require `viewerId`.
117
+
118
+ ### Posts
119
+
120
+ - `bold.community.posts.list(opts?)` - List posts with pagination (returns `PaginatedResponse<Post>`)
121
+ - `bold.community.posts.get(id, viewerId?)` - Get post with full comments
122
+ - `bold.community.posts.create(viewerId, data)` - Create a post
123
+ - `bold.community.posts.update(viewerId, id, data)` - Update post (owner/admin)
124
+ - `bold.community.posts.delete(viewerId, id)` - Delete post (owner/admin)
125
+ - `bold.community.posts.react(viewerId, id)` - Toggle reaction
126
+
127
+ ### Comments
128
+
129
+ - `bold.community.comments.create(viewerId, postId, data)` - Create comment
130
+ - `bold.community.comments.delete(viewerId, id)` - Delete comment (owner/admin)
131
+ - `bold.community.comments.react(viewerId, id)` - Toggle reaction
132
+
133
+ ```typescript
134
+ // Example: List posts with pagination
135
+ const response = await bold.community.posts.list({ page: 1, pageSize: 20, viewerId });
136
+ // response.data = Post[]
137
+ // response.meta = { page, page_size, total_entries, total_pages }
138
+
139
+ // Access users who reacted/commented (for avatar display)
140
+ const post = response.data[0];
141
+ post.reactions.reacted_by; // UserSummary[] (up to 10)
142
+ post.comments.commented_by; // UserSummary[] (up to 10)
143
+ post.reactions.viewer_has_reacted; // boolean (when viewerId provided)
144
+
145
+ // Create and interact
146
+ await bold.community.posts.create(viewerId, { content: 'Hello!', category: 'general' });
147
+ await bold.community.posts.react(viewerId, post.id);
148
+ await bold.community.comments.create(viewerId, post.id, { content: 'Great post!' });
149
+ ```
150
+
114
151
  ## AI Methods
115
152
 
116
153
  All AI methods return `AsyncIterable<AIEvent>` (streaming) or `Promise<AIResponse>` (non-streaming).
@@ -275,9 +312,94 @@ type AIEvent =
275
312
  }
276
313
  ```
277
314
 
315
+ ### UserSummary
316
+
317
+ ```typescript
318
+ {
319
+ id: string;
320
+ name: string;
321
+ avatar_url: string | null;
322
+ }
323
+ ```
324
+
325
+ ### Post
326
+
327
+ ```typescript
328
+ {
329
+ id: string;
330
+ content: string; // Markdown content
331
+ created_at: string;
332
+ author: PostAuthor; // Creator of the post
333
+ reactions: ReactionSummary; // { count, reacted_by, viewer_has_reacted? }
334
+ comments: CommentSummary; // { count, commented_by, items? }
335
+ category?: string;
336
+ pinned?: boolean;
337
+ pinnedAt?: string | null;
338
+ updatedAt?: string;
339
+ }
340
+ ```
341
+
342
+ ### ReactionSummary
343
+
344
+ ```typescript
345
+ {
346
+ count: number;
347
+ reacted_by: UserSummary[]; // Capped to 10 from API
348
+ viewer_has_reacted?: boolean; // Present when viewerId provided
349
+ }
350
+ ```
351
+
352
+ ### CommentSummary
353
+
354
+ ```typescript
355
+ {
356
+ count: number;
357
+ commented_by: UserSummary[]; // Capped to 10 from API
358
+ items?: CommentThread[]; // Full threads (detail view only)
359
+ }
360
+ ```
361
+
362
+ ### CommentThread
363
+
364
+ ```typescript
365
+ {
366
+ id: string;
367
+ content: string;
368
+ created_at: string;
369
+ author: UserSummary;
370
+ replies: Reply[]; // Nested replies
371
+ }
372
+ ```
373
+
374
+ ### Reply
375
+
376
+ ```typescript
377
+ {
378
+ id: string;
379
+ content: string;
380
+ created_at: string;
381
+ author: UserSummary;
382
+ parent_comment_id: string;
383
+ }
384
+ ```
385
+
386
+ ### PaginatedResponse<T>
387
+
388
+ ```typescript
389
+ {
390
+ data: T[];
391
+ meta: {
392
+ page: number; // Current page (1-indexed)
393
+ page_size: number;
394
+ total_entries: number;
395
+ total_pages: number;
396
+ };
397
+ }
398
+ ```
399
+
278
400
  ### Other types exported
279
401
 
280
- `Playlist`, `Settings`, `Portal`, `MenuItem`, `AIResponse`, `AIUsage`, `AIContextMessage`, `Conversation`, `ConversationMessage`, `RecommendationsResponse`, `Viewer`, `ViewerProgress`, `ViewerLookupParams`, `ListProgressOptions`, `ListVideosOptions`, `ListVideosLatestOptions`, `ListVideosIndexOptions`
402
+ `Playlist`, `Settings`, `Portal`, `MenuItem`, `AIResponse`, `AIUsage`, `AIContextMessage`, `Conversation`, `ConversationMessage`, `RecommendationsResponse`, `Viewer`, `ViewerProgress`, `ViewerLookupParams`, `ListProgressOptions`, `ListVideosOptions`, `ListVideosLatestOptions`, `ListVideosIndexOptions`, `Post`, `PostAuthor`, `UserSummary`, `ReactionSummary`, `CommentSummary`, `CommentThread`, `Reply`, `Comment`, `ReactionResponse`, `ListPostsOptions`, `CreatePostData`, `UpdatePostData`, `CreateCommentData`, `PaginationMeta`, `PaginatedResponse`, `CommunityAPIError`
281
403
 
282
404
  ## Error Handling
283
405
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@boldvideo/bold-js",
3
3
  "license": "MIT",
4
- "version": "1.17.0",
4
+ "version": "1.19.0",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
7
7
  "types": "dist/index.d.ts",