@boldvideo/bold-js 1.15.2 → 1.16.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,24 @@
1
1
  # @boldvideo/bold-js
2
2
 
3
+ ## 1.16.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 06c8abc: Add Viewers API for managing external users and tracking video progress
8
+
9
+ New methods on the `viewers` namespace:
10
+
11
+ - `viewers.list()` - List all viewers
12
+ - `viewers.get(id)` - Get a viewer by ID
13
+ - `viewers.lookup({ externalId } | { email })` - Find viewer by external ID or email
14
+ - `viewers.create(data)` - Create a new viewer
15
+ - `viewers.update(id, data)` - Update a viewer
16
+ - `viewers.listProgress(viewerId, options?)` - List progress for a viewer
17
+ - `viewers.getProgress(viewerId, videoId)` - Get progress for a video
18
+ - `viewers.saveProgress(viewerId, videoId, data)` - Save/update progress
19
+
20
+ Also fixes `camelizeKeys` to preserve user-defined trait keys (e.g., `company_name` stays as-is instead of becoming `companyName`).
21
+
3
22
  ## 1.15.2
4
23
 
5
24
  ### Patch Changes
package/README.md CHANGED
@@ -101,6 +101,65 @@ settings.menuItems.forEach(item => {
101
101
 
102
102
  ---
103
103
 
104
+ ## Viewers API
105
+
106
+ Manage external users and track their video watch progress. Ideal for course platforms integrating with Bold Video.
107
+
108
+ ### Viewer Management
109
+
110
+ ```typescript
111
+ // Create a viewer (e.g., when user signs up)
112
+ const { data: viewer } = await bold.viewers.create({
113
+ name: 'John Doe',
114
+ externalId: 'user_123', // Your platform's user ID
115
+ email: 'john@example.com',
116
+ traits: { plan: 'pro', company_name: 'Acme Inc' }
117
+ });
118
+
119
+ // Find viewer by external ID (common for syncing users)
120
+ const { data: viewer } = await bold.viewers.lookup({ externalId: 'user_123' });
121
+
122
+ // Or find by email
123
+ const { data: viewer } = await bold.viewers.lookup({ email: 'john@example.com' });
124
+
125
+ // Update viewer
126
+ await bold.viewers.update(viewer.id, {
127
+ traits: { plan: 'enterprise' } // Note: traits are replaced, not merged
128
+ });
129
+
130
+ // List all viewers
131
+ const { data: viewers } = await bold.viewers.list();
132
+ ```
133
+
134
+ ### Progress Tracking
135
+
136
+ ```typescript
137
+ // Save progress as video plays (call every 5-10 seconds)
138
+ await bold.viewers.saveProgress(viewerId, videoId, {
139
+ currentTime: 120, // seconds
140
+ duration: 600 // total video duration
141
+ });
142
+
143
+ // Mark video complete by setting currentTime = duration
144
+ await bold.viewers.saveProgress(viewerId, videoId, {
145
+ currentTime: 600,
146
+ duration: 600
147
+ });
148
+
149
+ // Get progress for a specific video
150
+ const { data: progress } = await bold.viewers.getProgress(viewerId, videoId);
151
+ console.log(`${progress.percentage}% complete`);
152
+
153
+ // List all progress for a viewer (e.g., for a course dashboard)
154
+ const { data: progress, meta } = await bold.viewers.listProgress(viewerId, {
155
+ collectionId: 'course-collection-id', // Filter to a course
156
+ completed: false // Only in-progress videos
157
+ });
158
+ console.log(`Completed ${meta.completed} of ${meta.total} videos`);
159
+ ```
160
+
161
+ ---
162
+
104
163
  ## AI Methods
105
164
 
106
165
  All AI methods support both streaming (default) and non-streaming modes.
@@ -294,7 +353,11 @@ import type {
294
353
  Recommendation,
295
354
  Conversation,
296
355
  ConversationMessage,
297
- Source
356
+ Source,
357
+ Viewer,
358
+ ViewerProgress,
359
+ ViewerLookupParams,
360
+ ListProgressOptions
298
361
  } from '@boldvideo/bold-js';
299
362
  ```
300
363
 
package/dist/index.cjs CHANGED
@@ -32,19 +32,21 @@ var src_exports = {};
32
32
  __export(src_exports, {
33
33
  DEFAULT_API_BASE_URL: () => DEFAULT_API_BASE_URL,
34
34
  DEFAULT_INTERNAL_API_BASE_URL: () => DEFAULT_INTERNAL_API_BASE_URL,
35
+ ViewerAPIError: () => ViewerAPIError,
35
36
  createClient: () => createClient
36
37
  });
37
38
  module.exports = __toCommonJS(src_exports);
38
39
 
39
40
  // src/lib/client.ts
40
- var import_axios = __toESM(require("axios"), 1);
41
+ var import_axios2 = __toESM(require("axios"), 1);
41
42
 
42
43
  // src/util/camelize.ts
43
44
  var isPlainObject = (value) => value !== null && typeof value === "object" && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
44
45
  var snakeToCamel = (key) => key.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
45
- function camelizeKeys(input) {
46
+ function camelizeKeys(input, options = {}) {
47
+ const preserve = new Set(options.preserveKeys ?? []);
46
48
  if (Array.isArray(input)) {
47
- return input.map((item) => camelizeKeys(item));
49
+ return input.map((item) => camelizeKeys(item, options));
48
50
  }
49
51
  if (!isPlainObject(input)) {
50
52
  return input;
@@ -52,7 +54,11 @@ function camelizeKeys(input) {
52
54
  const out = {};
53
55
  for (const [rawKey, value] of Object.entries(input)) {
54
56
  const key = snakeToCamel(rawKey);
55
- out[key] = camelizeKeys(value);
57
+ if (preserve.has(rawKey) || preserve.has(key)) {
58
+ out[key] = value;
59
+ continue;
60
+ }
61
+ out[key] = camelizeKeys(value, options);
56
62
  }
57
63
  return out;
58
64
  }
@@ -137,6 +143,154 @@ function fetchPlaylist(client) {
137
143
  };
138
144
  }
139
145
 
146
+ // src/lib/viewers.ts
147
+ var import_axios = require("axios");
148
+ var VIEWER_CAMELIZE_OPTIONS = { preserveKeys: ["traits"] };
149
+ var ViewerAPIError = class extends Error {
150
+ constructor(method, url, error) {
151
+ var __super = (...args) => {
152
+ super(...args);
153
+ };
154
+ if (error instanceof import_axios.AxiosError) {
155
+ const status = error.response?.status;
156
+ const message = error.response?.data?.error || error.message;
157
+ __super(`${method} ${url} failed (${status}): ${message}`);
158
+ this.status = status;
159
+ this.originalError = error;
160
+ } else if (error instanceof Error) {
161
+ __super(`${method} ${url} failed: ${error.message}`);
162
+ this.originalError = error;
163
+ } else {
164
+ __super(`${method} ${url} failed: ${String(error)}`);
165
+ }
166
+ this.name = "ViewerAPIError";
167
+ }
168
+ };
169
+ async function get2(client, url, options) {
170
+ try {
171
+ const res = await client.get(url);
172
+ return camelizeKeys(res.data, options);
173
+ } catch (error) {
174
+ throw new ViewerAPIError("GET", url, error);
175
+ }
176
+ }
177
+ async function post(client, url, data, options) {
178
+ try {
179
+ const res = await client.post(url, data);
180
+ return camelizeKeys(res.data, options);
181
+ } catch (error) {
182
+ throw new ViewerAPIError("POST", url, error);
183
+ }
184
+ }
185
+ async function patch(client, url, data, options) {
186
+ try {
187
+ const res = await client.patch(url, data);
188
+ return camelizeKeys(res.data, options);
189
+ } catch (error) {
190
+ throw new ViewerAPIError("PATCH", url, error);
191
+ }
192
+ }
193
+ function fetchViewers(client) {
194
+ return async () => {
195
+ return get2(client, "viewers", VIEWER_CAMELIZE_OPTIONS);
196
+ };
197
+ }
198
+ function fetchViewer(client) {
199
+ return async (id) => {
200
+ if (!id)
201
+ throw new Error("Viewer ID is required");
202
+ return get2(client, `viewers/${id}`, VIEWER_CAMELIZE_OPTIONS);
203
+ };
204
+ }
205
+ function lookupViewer(client) {
206
+ return async (params) => {
207
+ const qs = new URLSearchParams();
208
+ if ("externalId" in params && params.externalId) {
209
+ qs.set("external_id", params.externalId);
210
+ }
211
+ if ("email" in params && params.email) {
212
+ qs.set("email", params.email);
213
+ }
214
+ if (!qs.toString()) {
215
+ throw new Error("Either externalId or email is required");
216
+ }
217
+ return get2(client, `viewers/lookup?${qs.toString()}`, VIEWER_CAMELIZE_OPTIONS);
218
+ };
219
+ }
220
+ function createViewer(client) {
221
+ return async (data) => {
222
+ if (!data.name)
223
+ throw new Error("Viewer name is required");
224
+ return post(client, "viewers", {
225
+ viewer: {
226
+ name: data.name,
227
+ email: data.email,
228
+ external_id: data.externalId,
229
+ traits: data.traits
230
+ }
231
+ }, VIEWER_CAMELIZE_OPTIONS);
232
+ };
233
+ }
234
+ function updateViewer(client) {
235
+ return async (id, data) => {
236
+ if (!id)
237
+ throw new Error("Viewer ID is required");
238
+ const body = {};
239
+ if (data.name !== void 0)
240
+ body.name = data.name;
241
+ if (data.email !== void 0)
242
+ body.email = data.email;
243
+ if (data.externalId !== void 0)
244
+ body.external_id = data.externalId;
245
+ if (data.traits !== void 0)
246
+ body.traits = data.traits;
247
+ return patch(client, `viewers/${id}`, { viewer: body }, VIEWER_CAMELIZE_OPTIONS);
248
+ };
249
+ }
250
+ function fetchViewerProgress(client) {
251
+ return async (viewerId, options) => {
252
+ if (!viewerId)
253
+ throw new Error("Viewer ID is required");
254
+ const params = new URLSearchParams();
255
+ if (options?.completed !== void 0)
256
+ params.set("completed", String(options.completed));
257
+ if (options?.collectionId)
258
+ params.set("collection_id", options.collectionId);
259
+ const query = params.toString();
260
+ const url = query ? `viewers/${viewerId}/progress?${query}` : `viewers/${viewerId}/progress`;
261
+ return get2(client, url);
262
+ };
263
+ }
264
+ function fetchProgress(client) {
265
+ return async (viewerId, videoId) => {
266
+ if (!viewerId)
267
+ throw new Error("Viewer ID is required");
268
+ if (!videoId)
269
+ throw new Error("Video ID is required");
270
+ return get2(client, `viewers/${viewerId}/progress/${videoId}`);
271
+ };
272
+ }
273
+ function saveProgress(client) {
274
+ return async (viewerId, videoId, data) => {
275
+ if (!viewerId)
276
+ throw new Error("Viewer ID is required");
277
+ if (!videoId)
278
+ throw new Error("Video ID is required");
279
+ if (!Number.isFinite(data.currentTime) || data.currentTime < 0) {
280
+ throw new Error("currentTime must be a non-negative number");
281
+ }
282
+ if (!Number.isFinite(data.duration) || data.duration <= 0) {
283
+ throw new Error("duration must be a positive number");
284
+ }
285
+ return post(client, `viewers/${viewerId}/progress/${videoId}`, {
286
+ progress: {
287
+ current_time: data.currentTime,
288
+ duration: data.duration
289
+ }
290
+ });
291
+ };
292
+ }
293
+
140
294
  // src/util/throttle.ts
141
295
  var throttle = (fn, delay) => {
142
296
  let wait = false;
@@ -438,7 +592,7 @@ function createClient(apiKey, options = {}) {
438
592
  };
439
593
  let apiClient;
440
594
  try {
441
- apiClient = import_axios.default.create(apiClientOptions);
595
+ apiClient = import_axios2.default.create(apiClientOptions);
442
596
  } catch (error) {
443
597
  console.error("Error creating API client", error);
444
598
  throw error;
@@ -459,6 +613,16 @@ function createClient(apiKey, options = {}) {
459
613
  list: fetchPlaylists(apiClient),
460
614
  get: fetchPlaylist(apiClient)
461
615
  },
616
+ viewers: {
617
+ list: fetchViewers(apiClient),
618
+ get: fetchViewer(apiClient),
619
+ lookup: lookupViewer(apiClient),
620
+ create: createViewer(apiClient),
621
+ update: updateViewer(apiClient),
622
+ listProgress: fetchViewerProgress(apiClient),
623
+ getProgress: fetchProgress(apiClient),
624
+ saveProgress: saveProgress(apiClient)
625
+ },
462
626
  ai: createAI(aiConfig),
463
627
  trackEvent: trackEvent(apiClient, userId, { debug }),
464
628
  trackPageView: trackPageView(apiClient, userId, { debug })
@@ -468,5 +632,6 @@ function createClient(apiKey, options = {}) {
468
632
  0 && (module.exports = {
469
633
  DEFAULT_API_BASE_URL,
470
634
  DEFAULT_INTERNAL_API_BASE_URL,
635
+ ViewerAPIError,
471
636
  createClient
472
637
  });
package/dist/index.d.ts CHANGED
@@ -390,6 +390,89 @@ interface Conversation {
390
390
  createdAt: string;
391
391
  updatedAt: string;
392
392
  }
393
+ /**
394
+ * Viewer represents an external user from a course platform
395
+ */
396
+ type Viewer = {
397
+ id: string;
398
+ name: string;
399
+ email?: string;
400
+ externalId?: string;
401
+ /** Key-value metadata. Keys must start with letter/underscore, contain only alphanumeric/underscore */
402
+ traits?: Record<string, unknown>;
403
+ insertedAt: string;
404
+ updatedAt: string;
405
+ };
406
+ /**
407
+ * Progress record for a viewer-video pair
408
+ */
409
+ type ViewerProgress = {
410
+ id: string;
411
+ viewerId: string;
412
+ videoId: string;
413
+ /** Current playback position in seconds */
414
+ currentTime: number;
415
+ /** Total video duration in seconds */
416
+ duration: number;
417
+ /** Calculated: (currentTime / duration) * 100 */
418
+ percentage: number;
419
+ completed: boolean;
420
+ completedAt?: string;
421
+ insertedAt: string;
422
+ updatedAt: string;
423
+ };
424
+ /**
425
+ * Options for listing viewer progress
426
+ */
427
+ type ListProgressOptions = {
428
+ /** Filter by completion status */
429
+ completed?: boolean;
430
+ /** Filter to videos in a specific collection */
431
+ collectionId?: string;
432
+ };
433
+ /**
434
+ * Data for creating a new viewer
435
+ */
436
+ type CreateViewerData = {
437
+ /** Display name (required) */
438
+ name: string;
439
+ /** Email address for lookup */
440
+ email?: string;
441
+ /** Your platform's user ID for lookup */
442
+ externalId?: string;
443
+ /** Key-value metadata. Keys must start with letter/underscore, contain only alphanumeric/underscore */
444
+ traits?: Record<string, unknown>;
445
+ };
446
+ /**
447
+ * Data for updating an existing viewer
448
+ */
449
+ type UpdateViewerData = {
450
+ name?: string;
451
+ email?: string;
452
+ externalId?: string;
453
+ /** Note: traits are replaced entirely, not merged */
454
+ traits?: Record<string, unknown>;
455
+ };
456
+ /**
457
+ * Data for saving video progress
458
+ */
459
+ type SaveProgressData = {
460
+ /** Current playback position in seconds (must be non-negative) */
461
+ currentTime: number;
462
+ /** Total video duration in seconds (must be positive) */
463
+ duration: number;
464
+ };
465
+ /**
466
+ * Metadata returned with progress list
467
+ */
468
+ type ProgressListMeta = {
469
+ /** Total number of progress records */
470
+ total: number;
471
+ /** Number of completed videos */
472
+ completed: number;
473
+ /** Number of in-progress videos */
474
+ inProgress: number;
475
+ };
393
476
 
394
477
  /**
395
478
  * AI client interface for type-safe method overloading
@@ -503,6 +586,19 @@ interface AIClient {
503
586
  getConversation(conversationId: string): Promise<Conversation>;
504
587
  }
505
588
 
589
+ declare class ViewerAPIError extends Error {
590
+ readonly status?: number;
591
+ readonly originalError?: Error;
592
+ constructor(method: string, url: string, error: unknown);
593
+ }
594
+ type ViewerLookupParams = {
595
+ externalId: string;
596
+ email?: never;
597
+ } | {
598
+ email: string;
599
+ externalId?: never;
600
+ };
601
+
506
602
  type ClientOptions = {
507
603
  baseURL?: string;
508
604
  debug?: boolean;
@@ -531,6 +627,33 @@ declare function createClient(apiKey: string, options?: ClientOptions): {
531
627
  data: Playlist;
532
628
  }>;
533
629
  };
630
+ viewers: {
631
+ list: () => Promise<{
632
+ data: Viewer[];
633
+ }>;
634
+ get: (id: string) => Promise<{
635
+ data: Viewer;
636
+ }>;
637
+ lookup: (params: ViewerLookupParams) => Promise<{
638
+ data: Viewer;
639
+ }>;
640
+ create: (data: CreateViewerData) => Promise<{
641
+ data: Viewer;
642
+ }>;
643
+ update: (id: string, data: UpdateViewerData) => Promise<{
644
+ data: Viewer;
645
+ }>;
646
+ listProgress: (viewerId: string, options?: ListProgressOptions | undefined) => Promise<{
647
+ data: ViewerProgress[];
648
+ meta: ProgressListMeta;
649
+ }>;
650
+ getProgress: (viewerId: string, videoId: string) => Promise<{
651
+ data: ViewerProgress;
652
+ }>;
653
+ saveProgress: (viewerId: string, videoId: string, data: SaveProgressData) => Promise<{
654
+ data: ViewerProgress;
655
+ }>;
656
+ };
534
657
  ai: AIClient;
535
658
  trackEvent: (video: any, event: Event) => void;
536
659
  trackPageView: (title: string) => void;
@@ -545,4 +668,4 @@ declare const DEFAULT_API_BASE_URL = "https://app.boldvideo.io/api/v1/";
545
668
  */
546
669
  declare const DEFAULT_INTERNAL_API_BASE_URL = "https://app.boldvideo.io/i/v1/";
547
670
 
548
- export { AIContextMessage, AIEvent, AIResponse, AIUsage, Account, AccountAI, AnalyticsProvider, AskOptions, AssistantConfig, ChatOptions, Citation, ClientOptions, Conversation, ConversationMessage, ConversationMetadata, CustomRedirect, DEFAULT_API_BASE_URL, DEFAULT_INTERNAL_API_BASE_URL, MenuItem, Playlist, Portal, PortalDisplay, PortalHero, PortalLayout, PortalNavigation, PortalTheme, RecommendOptions, RecommendResponse, Recommendation, RecommendationVideo, RecommendationsOptions, RecommendationsResponse, SearchOptions, Segment, Settings, Source, ThemeColors, ThemeConfig, Video, VideoAttachment, VideoDownloadUrls, VideoMetadata, VideoSubtitles, VideoTranscript, createClient };
671
+ 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, 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 };
package/dist/index.js CHANGED
@@ -4,9 +4,10 @@ import axios from "axios";
4
4
  // src/util/camelize.ts
5
5
  var isPlainObject = (value) => value !== null && typeof value === "object" && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
6
6
  var snakeToCamel = (key) => key.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
7
- function camelizeKeys(input) {
7
+ function camelizeKeys(input, options = {}) {
8
+ const preserve = new Set(options.preserveKeys ?? []);
8
9
  if (Array.isArray(input)) {
9
- return input.map((item) => camelizeKeys(item));
10
+ return input.map((item) => camelizeKeys(item, options));
10
11
  }
11
12
  if (!isPlainObject(input)) {
12
13
  return input;
@@ -14,7 +15,11 @@ function camelizeKeys(input) {
14
15
  const out = {};
15
16
  for (const [rawKey, value] of Object.entries(input)) {
16
17
  const key = snakeToCamel(rawKey);
17
- out[key] = camelizeKeys(value);
18
+ if (preserve.has(rawKey) || preserve.has(key)) {
19
+ out[key] = value;
20
+ continue;
21
+ }
22
+ out[key] = camelizeKeys(value, options);
18
23
  }
19
24
  return out;
20
25
  }
@@ -99,6 +104,154 @@ function fetchPlaylist(client) {
99
104
  };
100
105
  }
101
106
 
107
+ // src/lib/viewers.ts
108
+ import { AxiosError } from "axios";
109
+ var VIEWER_CAMELIZE_OPTIONS = { preserveKeys: ["traits"] };
110
+ var ViewerAPIError = class extends Error {
111
+ constructor(method, url, error) {
112
+ var __super = (...args) => {
113
+ super(...args);
114
+ };
115
+ if (error instanceof AxiosError) {
116
+ const status = error.response?.status;
117
+ const message = error.response?.data?.error || error.message;
118
+ __super(`${method} ${url} failed (${status}): ${message}`);
119
+ this.status = status;
120
+ this.originalError = error;
121
+ } else if (error instanceof Error) {
122
+ __super(`${method} ${url} failed: ${error.message}`);
123
+ this.originalError = error;
124
+ } else {
125
+ __super(`${method} ${url} failed: ${String(error)}`);
126
+ }
127
+ this.name = "ViewerAPIError";
128
+ }
129
+ };
130
+ async function get2(client, url, options) {
131
+ try {
132
+ const res = await client.get(url);
133
+ return camelizeKeys(res.data, options);
134
+ } catch (error) {
135
+ throw new ViewerAPIError("GET", url, error);
136
+ }
137
+ }
138
+ async function post(client, url, data, options) {
139
+ try {
140
+ const res = await client.post(url, data);
141
+ return camelizeKeys(res.data, options);
142
+ } catch (error) {
143
+ throw new ViewerAPIError("POST", url, error);
144
+ }
145
+ }
146
+ async function patch(client, url, data, options) {
147
+ try {
148
+ const res = await client.patch(url, data);
149
+ return camelizeKeys(res.data, options);
150
+ } catch (error) {
151
+ throw new ViewerAPIError("PATCH", url, error);
152
+ }
153
+ }
154
+ function fetchViewers(client) {
155
+ return async () => {
156
+ return get2(client, "viewers", VIEWER_CAMELIZE_OPTIONS);
157
+ };
158
+ }
159
+ function fetchViewer(client) {
160
+ return async (id) => {
161
+ if (!id)
162
+ throw new Error("Viewer ID is required");
163
+ return get2(client, `viewers/${id}`, VIEWER_CAMELIZE_OPTIONS);
164
+ };
165
+ }
166
+ function lookupViewer(client) {
167
+ return async (params) => {
168
+ const qs = new URLSearchParams();
169
+ if ("externalId" in params && params.externalId) {
170
+ qs.set("external_id", params.externalId);
171
+ }
172
+ if ("email" in params && params.email) {
173
+ qs.set("email", params.email);
174
+ }
175
+ if (!qs.toString()) {
176
+ throw new Error("Either externalId or email is required");
177
+ }
178
+ return get2(client, `viewers/lookup?${qs.toString()}`, VIEWER_CAMELIZE_OPTIONS);
179
+ };
180
+ }
181
+ function createViewer(client) {
182
+ return async (data) => {
183
+ if (!data.name)
184
+ throw new Error("Viewer name is required");
185
+ return post(client, "viewers", {
186
+ viewer: {
187
+ name: data.name,
188
+ email: data.email,
189
+ external_id: data.externalId,
190
+ traits: data.traits
191
+ }
192
+ }, VIEWER_CAMELIZE_OPTIONS);
193
+ };
194
+ }
195
+ function updateViewer(client) {
196
+ return async (id, data) => {
197
+ if (!id)
198
+ throw new Error("Viewer ID is required");
199
+ const body = {};
200
+ if (data.name !== void 0)
201
+ body.name = data.name;
202
+ if (data.email !== void 0)
203
+ body.email = data.email;
204
+ if (data.externalId !== void 0)
205
+ body.external_id = data.externalId;
206
+ if (data.traits !== void 0)
207
+ body.traits = data.traits;
208
+ return patch(client, `viewers/${id}`, { viewer: body }, VIEWER_CAMELIZE_OPTIONS);
209
+ };
210
+ }
211
+ function fetchViewerProgress(client) {
212
+ return async (viewerId, options) => {
213
+ if (!viewerId)
214
+ throw new Error("Viewer ID is required");
215
+ const params = new URLSearchParams();
216
+ if (options?.completed !== void 0)
217
+ params.set("completed", String(options.completed));
218
+ if (options?.collectionId)
219
+ params.set("collection_id", options.collectionId);
220
+ const query = params.toString();
221
+ const url = query ? `viewers/${viewerId}/progress?${query}` : `viewers/${viewerId}/progress`;
222
+ return get2(client, url);
223
+ };
224
+ }
225
+ function fetchProgress(client) {
226
+ return async (viewerId, videoId) => {
227
+ if (!viewerId)
228
+ throw new Error("Viewer ID is required");
229
+ if (!videoId)
230
+ throw new Error("Video ID is required");
231
+ return get2(client, `viewers/${viewerId}/progress/${videoId}`);
232
+ };
233
+ }
234
+ function saveProgress(client) {
235
+ return async (viewerId, videoId, data) => {
236
+ if (!viewerId)
237
+ throw new Error("Viewer ID is required");
238
+ if (!videoId)
239
+ throw new Error("Video ID is required");
240
+ if (!Number.isFinite(data.currentTime) || data.currentTime < 0) {
241
+ throw new Error("currentTime must be a non-negative number");
242
+ }
243
+ if (!Number.isFinite(data.duration) || data.duration <= 0) {
244
+ throw new Error("duration must be a positive number");
245
+ }
246
+ return post(client, `viewers/${viewerId}/progress/${videoId}`, {
247
+ progress: {
248
+ current_time: data.currentTime,
249
+ duration: data.duration
250
+ }
251
+ });
252
+ };
253
+ }
254
+
102
255
  // src/util/throttle.ts
103
256
  var throttle = (fn, delay) => {
104
257
  let wait = false;
@@ -421,6 +574,16 @@ function createClient(apiKey, options = {}) {
421
574
  list: fetchPlaylists(apiClient),
422
575
  get: fetchPlaylist(apiClient)
423
576
  },
577
+ viewers: {
578
+ list: fetchViewers(apiClient),
579
+ get: fetchViewer(apiClient),
580
+ lookup: lookupViewer(apiClient),
581
+ create: createViewer(apiClient),
582
+ update: updateViewer(apiClient),
583
+ listProgress: fetchViewerProgress(apiClient),
584
+ getProgress: fetchProgress(apiClient),
585
+ saveProgress: saveProgress(apiClient)
586
+ },
424
587
  ai: createAI(aiConfig),
425
588
  trackEvent: trackEvent(apiClient, userId, { debug }),
426
589
  trackPageView: trackPageView(apiClient, userId, { debug })
@@ -429,5 +592,6 @@ function createClient(apiKey, options = {}) {
429
592
  export {
430
593
  DEFAULT_API_BASE_URL,
431
594
  DEFAULT_INTERNAL_API_BASE_URL,
595
+ ViewerAPIError,
432
596
  createClient
433
597
  };