@boldvideo/bold-js 1.15.2 → 1.17.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 +46 -0
- package/README.md +86 -2
- package/dist/index.cjs +212 -8
- package/dist/index.d.ts +163 -2
- package/dist/index.js +209 -6
- package/docs/plans/2026-01-22-docs-enhance-llms-txt-for-ai-consumption-plan.md +393 -0
- package/docs/plans/2026-01-23-feat-viewers-api-sdk-implementation-plan.md +380 -0
- package/llms.txt +83 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# @boldvideo/bold-js
|
|
2
2
|
|
|
3
|
+
## 1.17.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 563730d: Add full options support to `videos.list()` for tag, collectionId, viewerId, and page filtering
|
|
8
|
+
|
|
9
|
+
**New features:**
|
|
10
|
+
|
|
11
|
+
- Filter videos by `tag` and `collectionId` on both endpoints
|
|
12
|
+
- Include watch progress with `viewerId` parameter
|
|
13
|
+
- Access paginated `/videos` endpoint using `page` parameter
|
|
14
|
+
|
|
15
|
+
**Usage:**
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// Latest videos with filters
|
|
19
|
+
await bold.videos.list({ limit: 20, tag: "sales", collectionId: "col_123" });
|
|
20
|
+
|
|
21
|
+
// Include viewer watch progress
|
|
22
|
+
await bold.videos.list({ limit: 20, viewerId: "viewer_123" });
|
|
23
|
+
|
|
24
|
+
// Paginated index (uses /videos endpoint)
|
|
25
|
+
await bold.videos.list({ page: 2, tag: "sales" });
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Breaking:** None. Existing `bold.videos.list()` and `bold.videos.list(12)` calls work unchanged.
|
|
29
|
+
|
|
30
|
+
## 1.16.0
|
|
31
|
+
|
|
32
|
+
### Minor Changes
|
|
33
|
+
|
|
34
|
+
- 06c8abc: Add Viewers API for managing external users and tracking video progress
|
|
35
|
+
|
|
36
|
+
New methods on the `viewers` namespace:
|
|
37
|
+
|
|
38
|
+
- `viewers.list()` - List all viewers
|
|
39
|
+
- `viewers.get(id)` - Get a viewer by ID
|
|
40
|
+
- `viewers.lookup({ externalId } | { email })` - Find viewer by external ID or email
|
|
41
|
+
- `viewers.create(data)` - Create a new viewer
|
|
42
|
+
- `viewers.update(id, data)` - Update a viewer
|
|
43
|
+
- `viewers.listProgress(viewerId, options?)` - List progress for a viewer
|
|
44
|
+
- `viewers.getProgress(viewerId, videoId)` - Get progress for a video
|
|
45
|
+
- `viewers.saveProgress(viewerId, videoId, data)` - Save/update progress
|
|
46
|
+
|
|
47
|
+
Also fixes `camelizeKeys` to preserve user-defined trait keys (e.g., `company_name` stays as-is instead of becoming `companyName`).
|
|
48
|
+
|
|
3
49
|
## 1.15.2
|
|
4
50
|
|
|
5
51
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -59,9 +59,27 @@ console.log(recs.guidance);
|
|
|
59
59
|
### Videos
|
|
60
60
|
|
|
61
61
|
```typescript
|
|
62
|
-
// List latest videos
|
|
62
|
+
// List latest videos (default: 12)
|
|
63
63
|
const videos = await bold.videos.list();
|
|
64
64
|
|
|
65
|
+
// With limit (backwards compatible)
|
|
66
|
+
const videos = await bold.videos.list(20);
|
|
67
|
+
|
|
68
|
+
// With filters
|
|
69
|
+
const videos = await bold.videos.list({
|
|
70
|
+
limit: 20,
|
|
71
|
+
tag: 'sales',
|
|
72
|
+
collectionId: 'col_123',
|
|
73
|
+
viewerId: 'viewer_123' // Include watch progress
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Paginated index (uses /videos endpoint)
|
|
77
|
+
const videos = await bold.videos.list({
|
|
78
|
+
page: 2,
|
|
79
|
+
tag: 'sales',
|
|
80
|
+
collectionId: 'col_123'
|
|
81
|
+
});
|
|
82
|
+
|
|
65
83
|
// Get a single video by ID or slug
|
|
66
84
|
const video = await bold.videos.get('video-id');
|
|
67
85
|
const videoBySlug = await bold.videos.get('my-video-slug');
|
|
@@ -101,6 +119,65 @@ settings.menuItems.forEach(item => {
|
|
|
101
119
|
|
|
102
120
|
---
|
|
103
121
|
|
|
122
|
+
## Viewers API
|
|
123
|
+
|
|
124
|
+
Manage external users and track their video watch progress. Ideal for course platforms integrating with Bold Video.
|
|
125
|
+
|
|
126
|
+
### Viewer Management
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// Create a viewer (e.g., when user signs up)
|
|
130
|
+
const { data: viewer } = await bold.viewers.create({
|
|
131
|
+
name: 'John Doe',
|
|
132
|
+
externalId: 'user_123', // Your platform's user ID
|
|
133
|
+
email: 'john@example.com',
|
|
134
|
+
traits: { plan: 'pro', company_name: 'Acme Inc' }
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Find viewer by external ID (common for syncing users)
|
|
138
|
+
const { data: viewer } = await bold.viewers.lookup({ externalId: 'user_123' });
|
|
139
|
+
|
|
140
|
+
// Or find by email
|
|
141
|
+
const { data: viewer } = await bold.viewers.lookup({ email: 'john@example.com' });
|
|
142
|
+
|
|
143
|
+
// Update viewer
|
|
144
|
+
await bold.viewers.update(viewer.id, {
|
|
145
|
+
traits: { plan: 'enterprise' } // Note: traits are replaced, not merged
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// List all viewers
|
|
149
|
+
const { data: viewers } = await bold.viewers.list();
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Progress Tracking
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// Save progress as video plays (call every 5-10 seconds)
|
|
156
|
+
await bold.viewers.saveProgress(viewerId, videoId, {
|
|
157
|
+
currentTime: 120, // seconds
|
|
158
|
+
duration: 600 // total video duration
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Mark video complete by setting currentTime = duration
|
|
162
|
+
await bold.viewers.saveProgress(viewerId, videoId, {
|
|
163
|
+
currentTime: 600,
|
|
164
|
+
duration: 600
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Get progress for a specific video
|
|
168
|
+
const { data: progress } = await bold.viewers.getProgress(viewerId, videoId);
|
|
169
|
+
console.log(`${progress.percentage}% complete`);
|
|
170
|
+
|
|
171
|
+
// List all progress for a viewer (e.g., for a course dashboard)
|
|
172
|
+
const { data: progress, meta } = await bold.viewers.listProgress(viewerId, {
|
|
173
|
+
collectionId: 'course-collection-id', // Filter to a course
|
|
174
|
+
completed: false // Only in-progress videos
|
|
175
|
+
});
|
|
176
|
+
console.log(`Completed ${meta.completed} of ${meta.total} videos`);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
104
181
|
## AI Methods
|
|
105
182
|
|
|
106
183
|
All AI methods support both streaming (default) and non-streaming modes.
|
|
@@ -294,7 +371,14 @@ import type {
|
|
|
294
371
|
Recommendation,
|
|
295
372
|
Conversation,
|
|
296
373
|
ConversationMessage,
|
|
297
|
-
Source
|
|
374
|
+
Source,
|
|
375
|
+
Viewer,
|
|
376
|
+
ViewerProgress,
|
|
377
|
+
ViewerLookupParams,
|
|
378
|
+
ListProgressOptions,
|
|
379
|
+
ListVideosOptions,
|
|
380
|
+
ListVideosLatestOptions,
|
|
381
|
+
ListVideosIndexOptions
|
|
298
382
|
} from '@boldvideo/bold-js';
|
|
299
383
|
```
|
|
300
384
|
|
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
|
|
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,12 +54,25 @@ function camelizeKeys(input) {
|
|
|
52
54
|
const out = {};
|
|
53
55
|
for (const [rawKey, value] of Object.entries(input)) {
|
|
54
56
|
const key = snakeToCamel(rawKey);
|
|
55
|
-
|
|
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
|
}
|
|
59
65
|
|
|
60
66
|
// src/lib/fetchers.ts
|
|
67
|
+
function toQuery(params) {
|
|
68
|
+
const qs = new URLSearchParams();
|
|
69
|
+
for (const [k, v] of Object.entries(params)) {
|
|
70
|
+
if (v !== void 0 && v !== null)
|
|
71
|
+
qs.set(k, String(v));
|
|
72
|
+
}
|
|
73
|
+
const s = qs.toString();
|
|
74
|
+
return s ? `?${s}` : "";
|
|
75
|
+
}
|
|
61
76
|
async function get(client, url) {
|
|
62
77
|
try {
|
|
63
78
|
const res = await client.get(url);
|
|
@@ -84,14 +99,44 @@ function fetchSettings(client) {
|
|
|
84
99
|
};
|
|
85
100
|
}
|
|
86
101
|
function fetchVideos(client) {
|
|
87
|
-
return async (
|
|
102
|
+
return async (arg = 12) => {
|
|
88
103
|
try {
|
|
104
|
+
if (typeof arg === "number") {
|
|
105
|
+
return await get(
|
|
106
|
+
client,
|
|
107
|
+
`videos/latest${toQuery({ limit: arg })}`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const opts = arg;
|
|
111
|
+
const hasPage = "page" in opts && opts.page !== void 0;
|
|
112
|
+
if (hasPage && ("limit" in opts || "viewerId" in opts)) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
"videos.list(): cannot use `page` with `limit` or `viewerId` (these belong to different endpoints)"
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (hasPage) {
|
|
118
|
+
const { page, tag: tag2, collectionId: collectionId2 } = opts;
|
|
119
|
+
return await get(
|
|
120
|
+
client,
|
|
121
|
+
`videos${toQuery({
|
|
122
|
+
page,
|
|
123
|
+
tag: tag2,
|
|
124
|
+
collection_id: collectionId2
|
|
125
|
+
})}`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const { limit, tag, collectionId, viewerId } = opts;
|
|
89
129
|
return await get(
|
|
90
130
|
client,
|
|
91
|
-
`videos/latest
|
|
131
|
+
`videos/latest${toQuery({
|
|
132
|
+
limit: limit ?? 12,
|
|
133
|
+
tag,
|
|
134
|
+
collection_id: collectionId,
|
|
135
|
+
viewer_id: viewerId
|
|
136
|
+
})}`
|
|
92
137
|
);
|
|
93
138
|
} catch (error) {
|
|
94
|
-
console.error(`Error fetching videos
|
|
139
|
+
console.error(`Error fetching videos`, error);
|
|
95
140
|
throw error;
|
|
96
141
|
}
|
|
97
142
|
};
|
|
@@ -137,6 +182,154 @@ function fetchPlaylist(client) {
|
|
|
137
182
|
};
|
|
138
183
|
}
|
|
139
184
|
|
|
185
|
+
// src/lib/viewers.ts
|
|
186
|
+
var import_axios = require("axios");
|
|
187
|
+
var VIEWER_CAMELIZE_OPTIONS = { preserveKeys: ["traits"] };
|
|
188
|
+
var ViewerAPIError = class extends Error {
|
|
189
|
+
constructor(method, url, error) {
|
|
190
|
+
var __super = (...args) => {
|
|
191
|
+
super(...args);
|
|
192
|
+
};
|
|
193
|
+
if (error instanceof import_axios.AxiosError) {
|
|
194
|
+
const status = error.response?.status;
|
|
195
|
+
const message = error.response?.data?.error || error.message;
|
|
196
|
+
__super(`${method} ${url} failed (${status}): ${message}`);
|
|
197
|
+
this.status = status;
|
|
198
|
+
this.originalError = error;
|
|
199
|
+
} else if (error instanceof Error) {
|
|
200
|
+
__super(`${method} ${url} failed: ${error.message}`);
|
|
201
|
+
this.originalError = error;
|
|
202
|
+
} else {
|
|
203
|
+
__super(`${method} ${url} failed: ${String(error)}`);
|
|
204
|
+
}
|
|
205
|
+
this.name = "ViewerAPIError";
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
async function get2(client, url, options) {
|
|
209
|
+
try {
|
|
210
|
+
const res = await client.get(url);
|
|
211
|
+
return camelizeKeys(res.data, options);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
throw new ViewerAPIError("GET", url, error);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function post(client, url, data, options) {
|
|
217
|
+
try {
|
|
218
|
+
const res = await client.post(url, data);
|
|
219
|
+
return camelizeKeys(res.data, options);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
throw new ViewerAPIError("POST", url, error);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function patch(client, url, data, options) {
|
|
225
|
+
try {
|
|
226
|
+
const res = await client.patch(url, data);
|
|
227
|
+
return camelizeKeys(res.data, options);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
throw new ViewerAPIError("PATCH", url, error);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function fetchViewers(client) {
|
|
233
|
+
return async () => {
|
|
234
|
+
return get2(client, "viewers", VIEWER_CAMELIZE_OPTIONS);
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function fetchViewer(client) {
|
|
238
|
+
return async (id) => {
|
|
239
|
+
if (!id)
|
|
240
|
+
throw new Error("Viewer ID is required");
|
|
241
|
+
return get2(client, `viewers/${id}`, VIEWER_CAMELIZE_OPTIONS);
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function lookupViewer(client) {
|
|
245
|
+
return async (params) => {
|
|
246
|
+
const qs = new URLSearchParams();
|
|
247
|
+
if ("externalId" in params && params.externalId) {
|
|
248
|
+
qs.set("external_id", params.externalId);
|
|
249
|
+
}
|
|
250
|
+
if ("email" in params && params.email) {
|
|
251
|
+
qs.set("email", params.email);
|
|
252
|
+
}
|
|
253
|
+
if (!qs.toString()) {
|
|
254
|
+
throw new Error("Either externalId or email is required");
|
|
255
|
+
}
|
|
256
|
+
return get2(client, `viewers/lookup?${qs.toString()}`, VIEWER_CAMELIZE_OPTIONS);
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function createViewer(client) {
|
|
260
|
+
return async (data) => {
|
|
261
|
+
if (!data.name)
|
|
262
|
+
throw new Error("Viewer name is required");
|
|
263
|
+
return post(client, "viewers", {
|
|
264
|
+
viewer: {
|
|
265
|
+
name: data.name,
|
|
266
|
+
email: data.email,
|
|
267
|
+
external_id: data.externalId,
|
|
268
|
+
traits: data.traits
|
|
269
|
+
}
|
|
270
|
+
}, VIEWER_CAMELIZE_OPTIONS);
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function updateViewer(client) {
|
|
274
|
+
return async (id, data) => {
|
|
275
|
+
if (!id)
|
|
276
|
+
throw new Error("Viewer ID is required");
|
|
277
|
+
const body = {};
|
|
278
|
+
if (data.name !== void 0)
|
|
279
|
+
body.name = data.name;
|
|
280
|
+
if (data.email !== void 0)
|
|
281
|
+
body.email = data.email;
|
|
282
|
+
if (data.externalId !== void 0)
|
|
283
|
+
body.external_id = data.externalId;
|
|
284
|
+
if (data.traits !== void 0)
|
|
285
|
+
body.traits = data.traits;
|
|
286
|
+
return patch(client, `viewers/${id}`, { viewer: body }, VIEWER_CAMELIZE_OPTIONS);
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function fetchViewerProgress(client) {
|
|
290
|
+
return async (viewerId, options) => {
|
|
291
|
+
if (!viewerId)
|
|
292
|
+
throw new Error("Viewer ID is required");
|
|
293
|
+
const params = new URLSearchParams();
|
|
294
|
+
if (options?.completed !== void 0)
|
|
295
|
+
params.set("completed", String(options.completed));
|
|
296
|
+
if (options?.collectionId)
|
|
297
|
+
params.set("collection_id", options.collectionId);
|
|
298
|
+
const query = params.toString();
|
|
299
|
+
const url = query ? `viewers/${viewerId}/progress?${query}` : `viewers/${viewerId}/progress`;
|
|
300
|
+
return get2(client, url);
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function fetchProgress(client) {
|
|
304
|
+
return async (viewerId, videoId) => {
|
|
305
|
+
if (!viewerId)
|
|
306
|
+
throw new Error("Viewer ID is required");
|
|
307
|
+
if (!videoId)
|
|
308
|
+
throw new Error("Video ID is required");
|
|
309
|
+
return get2(client, `viewers/${viewerId}/progress/${videoId}`);
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function saveProgress(client) {
|
|
313
|
+
return async (viewerId, videoId, data) => {
|
|
314
|
+
if (!viewerId)
|
|
315
|
+
throw new Error("Viewer ID is required");
|
|
316
|
+
if (!videoId)
|
|
317
|
+
throw new Error("Video ID is required");
|
|
318
|
+
if (!Number.isFinite(data.currentTime) || data.currentTime < 0) {
|
|
319
|
+
throw new Error("currentTime must be a non-negative number");
|
|
320
|
+
}
|
|
321
|
+
if (!Number.isFinite(data.duration) || data.duration <= 0) {
|
|
322
|
+
throw new Error("duration must be a positive number");
|
|
323
|
+
}
|
|
324
|
+
return post(client, `viewers/${viewerId}/progress/${videoId}`, {
|
|
325
|
+
progress: {
|
|
326
|
+
current_time: data.currentTime,
|
|
327
|
+
duration: data.duration
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
140
333
|
// src/util/throttle.ts
|
|
141
334
|
var throttle = (fn, delay) => {
|
|
142
335
|
let wait = false;
|
|
@@ -438,7 +631,7 @@ function createClient(apiKey, options = {}) {
|
|
|
438
631
|
};
|
|
439
632
|
let apiClient;
|
|
440
633
|
try {
|
|
441
|
-
apiClient =
|
|
634
|
+
apiClient = import_axios2.default.create(apiClientOptions);
|
|
442
635
|
} catch (error) {
|
|
443
636
|
console.error("Error creating API client", error);
|
|
444
637
|
throw error;
|
|
@@ -459,6 +652,16 @@ function createClient(apiKey, options = {}) {
|
|
|
459
652
|
list: fetchPlaylists(apiClient),
|
|
460
653
|
get: fetchPlaylist(apiClient)
|
|
461
654
|
},
|
|
655
|
+
viewers: {
|
|
656
|
+
list: fetchViewers(apiClient),
|
|
657
|
+
get: fetchViewer(apiClient),
|
|
658
|
+
lookup: lookupViewer(apiClient),
|
|
659
|
+
create: createViewer(apiClient),
|
|
660
|
+
update: updateViewer(apiClient),
|
|
661
|
+
listProgress: fetchViewerProgress(apiClient),
|
|
662
|
+
getProgress: fetchProgress(apiClient),
|
|
663
|
+
saveProgress: saveProgress(apiClient)
|
|
664
|
+
},
|
|
462
665
|
ai: createAI(aiConfig),
|
|
463
666
|
trackEvent: trackEvent(apiClient, userId, { debug }),
|
|
464
667
|
trackPageView: trackPageView(apiClient, userId, { debug })
|
|
@@ -468,5 +671,6 @@ function createClient(apiKey, options = {}) {
|
|
|
468
671
|
0 && (module.exports = {
|
|
469
672
|
DEFAULT_API_BASE_URL,
|
|
470
673
|
DEFAULT_INTERNAL_API_BASE_URL,
|
|
674
|
+
ViewerAPIError,
|
|
471
675
|
createClient
|
|
472
676
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -390,6 +390,127 @@ 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
|
+
};
|
|
476
|
+
/**
|
|
477
|
+
* Options for listing videos from /videos/latest endpoint
|
|
478
|
+
*/
|
|
479
|
+
type ListVideosLatestOptions = {
|
|
480
|
+
/** Max videos to return (default: 12) */
|
|
481
|
+
limit?: number;
|
|
482
|
+
/** Filter by tag */
|
|
483
|
+
tag?: string;
|
|
484
|
+
/** Filter to videos in a specific collection */
|
|
485
|
+
collectionId?: string;
|
|
486
|
+
/** Viewer UUID for watch progress */
|
|
487
|
+
viewerId?: string;
|
|
488
|
+
};
|
|
489
|
+
/**
|
|
490
|
+
* Options for listing videos from /videos (index) endpoint with pagination
|
|
491
|
+
*/
|
|
492
|
+
type ListVideosIndexOptions = {
|
|
493
|
+
/** Page number for pagination */
|
|
494
|
+
page?: number;
|
|
495
|
+
/** Filter by tag */
|
|
496
|
+
tag?: string;
|
|
497
|
+
/** Filter to videos in a specific collection */
|
|
498
|
+
collectionId?: string;
|
|
499
|
+
};
|
|
500
|
+
/**
|
|
501
|
+
* Combined options for bold.videos.list()
|
|
502
|
+
*
|
|
503
|
+
* If `page` is provided, uses /videos (index) endpoint.
|
|
504
|
+
* Otherwise, uses /videos/latest endpoint.
|
|
505
|
+
*
|
|
506
|
+
* Note: `page` and `limit`/`viewerId` are mutually exclusive.
|
|
507
|
+
*/
|
|
508
|
+
type ListVideosOptions = (ListVideosLatestOptions & {
|
|
509
|
+
page?: never;
|
|
510
|
+
}) | (ListVideosIndexOptions & {
|
|
511
|
+
limit?: never;
|
|
512
|
+
viewerId?: never;
|
|
513
|
+
});
|
|
393
514
|
|
|
394
515
|
/**
|
|
395
516
|
* AI client interface for type-safe method overloading
|
|
@@ -503,6 +624,19 @@ interface AIClient {
|
|
|
503
624
|
getConversation(conversationId: string): Promise<Conversation>;
|
|
504
625
|
}
|
|
505
626
|
|
|
627
|
+
declare class ViewerAPIError extends Error {
|
|
628
|
+
readonly status?: number;
|
|
629
|
+
readonly originalError?: Error;
|
|
630
|
+
constructor(method: string, url: string, error: unknown);
|
|
631
|
+
}
|
|
632
|
+
type ViewerLookupParams = {
|
|
633
|
+
externalId: string;
|
|
634
|
+
email?: never;
|
|
635
|
+
} | {
|
|
636
|
+
email: string;
|
|
637
|
+
externalId?: never;
|
|
638
|
+
};
|
|
639
|
+
|
|
506
640
|
type ClientOptions = {
|
|
507
641
|
baseURL?: string;
|
|
508
642
|
debug?: boolean;
|
|
@@ -513,7 +647,7 @@ declare function createClient(apiKey: string, options?: ClientOptions): {
|
|
|
513
647
|
data: Settings;
|
|
514
648
|
}>;
|
|
515
649
|
videos: {
|
|
516
|
-
list: (
|
|
650
|
+
list: (arg?: number | ListVideosOptions) => Promise<{
|
|
517
651
|
data: Video[];
|
|
518
652
|
}>;
|
|
519
653
|
get: (id: string) => Promise<{
|
|
@@ -531,6 +665,33 @@ declare function createClient(apiKey: string, options?: ClientOptions): {
|
|
|
531
665
|
data: Playlist;
|
|
532
666
|
}>;
|
|
533
667
|
};
|
|
668
|
+
viewers: {
|
|
669
|
+
list: () => Promise<{
|
|
670
|
+
data: Viewer[];
|
|
671
|
+
}>;
|
|
672
|
+
get: (id: string) => Promise<{
|
|
673
|
+
data: Viewer;
|
|
674
|
+
}>;
|
|
675
|
+
lookup: (params: ViewerLookupParams) => Promise<{
|
|
676
|
+
data: Viewer;
|
|
677
|
+
}>;
|
|
678
|
+
create: (data: CreateViewerData) => Promise<{
|
|
679
|
+
data: Viewer;
|
|
680
|
+
}>;
|
|
681
|
+
update: (id: string, data: UpdateViewerData) => Promise<{
|
|
682
|
+
data: Viewer;
|
|
683
|
+
}>;
|
|
684
|
+
listProgress: (viewerId: string, options?: ListProgressOptions | undefined) => Promise<{
|
|
685
|
+
data: ViewerProgress[];
|
|
686
|
+
meta: ProgressListMeta;
|
|
687
|
+
}>;
|
|
688
|
+
getProgress: (viewerId: string, videoId: string) => Promise<{
|
|
689
|
+
data: ViewerProgress;
|
|
690
|
+
}>;
|
|
691
|
+
saveProgress: (viewerId: string, videoId: string, data: SaveProgressData) => Promise<{
|
|
692
|
+
data: ViewerProgress;
|
|
693
|
+
}>;
|
|
694
|
+
};
|
|
534
695
|
ai: AIClient;
|
|
535
696
|
trackEvent: (video: any, event: Event) => void;
|
|
536
697
|
trackPageView: (title: string) => void;
|
|
@@ -545,4 +706,4 @@ declare const DEFAULT_API_BASE_URL = "https://app.boldvideo.io/api/v1/";
|
|
|
545
706
|
*/
|
|
546
707
|
declare const DEFAULT_INTERNAL_API_BASE_URL = "https://app.boldvideo.io/i/v1/";
|
|
547
708
|
|
|
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 };
|
|
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 };
|