@boldvideo/bold-js 1.7.0 → 1.9.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,46 @@
1
1
  # @boldvideo/bold-js
2
2
 
3
+ ## 1.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ff2f021: Transform all API responses to use camelCase (TypeScript/JavaScript convention)
8
+
9
+ **Breaking Changes:**
10
+
11
+ All response types now use camelCase consistently across the entire SDK:
12
+
13
+ - **Videos**: `playback_id` → `playbackId`, `published_at` → `publishedAt`, `stream_url` → `streamUrl`, `meta_data` → `metaData`, `captions_label` → `captionsLabel`, `download_urls` → `downloadUrls`, etc.
14
+ - **Playlists**: `is_private` → `isPrivate`
15
+ - **Settings**: `featured_playlists` → `featuredPlaylists`, `menu_items` → `menuItems`, `theme_config` → `themeConfig`, `ai_avatar` → `aiAvatar`, `logo_url` → `logoUrl`, etc.
16
+ - **Portal/Theme**: `show_chapters` → `showChapters`, `font_body` → `fontBody`, `color_scheme` → `colorScheme`, `css_overrides` → `cssOverrides`, etc.
17
+ - **AI responses**: (already camelCase in v1.8.0)
18
+
19
+ **Internal:**
20
+
21
+ - Applied `camelizeKeys()` transformation in `fetchers.ts` at the transport boundary
22
+ - Updated all type definitions in `types.ts` to use camelCase
23
+
24
+ ## 1.8.0
25
+
26
+ ### Minor Changes
27
+
28
+ - 065d7ea: Transform all API responses to use camelCase (TypeScript/JavaScript convention)
29
+
30
+ **Breaking Changes:**
31
+
32
+ - **All response types now use camelCase** (was snake_case). API responses are transformed at the SDK boundary.
33
+ - `video_id` → `videoId`
34
+ - `timestamp_end` → `timestampEnd`
35
+ - `playback_id` → `playbackId`
36
+ - `input_tokens` → `inputTokens`
37
+ - `conversation_id` → `conversationId`
38
+
39
+ **Internal:**
40
+
41
+ - Added `camelizeKeys` utility for deep snake_case → camelCase transformation
42
+ - Applied transformation in `jsonRequest()` and `parseSSE()` at the transport boundary
43
+
3
44
  ## 1.7.0
4
45
 
5
46
  ### Minor Changes
package/README.md CHANGED
@@ -269,7 +269,33 @@ import type {
269
269
 
270
270
  ---
271
271
 
272
- ## Migration from v1.6.x
272
+ ## Migration from v1.7.x
273
+
274
+ ### Breaking: All response types now use camelCase
275
+
276
+ **All API responses** (videos, playlists, settings, AI) are now transformed to use idiomatic TypeScript/JavaScript naming:
277
+
278
+ ```typescript
279
+ // Before (v1.7.x and earlier)
280
+ video.playback_id
281
+ video.published_at
282
+ video.stream_url
283
+ video.meta_data
284
+ settings.featured_playlists
285
+ settings.menu_items
286
+ settings.theme_config
287
+ playlist.is_private
288
+
289
+ // After (v1.8.0)
290
+ video.playbackId
291
+ video.publishedAt
292
+ video.streamUrl
293
+ video.metaData
294
+ settings.featuredPlaylists
295
+ settings.menuItems
296
+ settings.themeConfig
297
+ playlist.isPrivate
298
+ ```
273
299
 
274
300
  ### Method Changes
275
301
 
package/dist/index.cjs CHANGED
@@ -39,6 +39,24 @@ module.exports = __toCommonJS(src_exports);
39
39
  // src/lib/client.ts
40
40
  var import_axios = __toESM(require("axios"), 1);
41
41
 
42
+ // src/util/camelize.ts
43
+ var isPlainObject = (value) => value !== null && typeof value === "object" && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
44
+ var snakeToCamel = (key) => key.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
45
+ function camelizeKeys(input) {
46
+ if (Array.isArray(input)) {
47
+ return input.map((item) => camelizeKeys(item));
48
+ }
49
+ if (!isPlainObject(input)) {
50
+ return input;
51
+ }
52
+ const out = {};
53
+ for (const [rawKey, value] of Object.entries(input)) {
54
+ const key = snakeToCamel(rawKey);
55
+ out[key] = camelizeKeys(value);
56
+ }
57
+ return out;
58
+ }
59
+
42
60
  // src/lib/fetchers.ts
43
61
  async function get(client, url) {
44
62
  try {
@@ -46,7 +64,7 @@ async function get(client, url) {
46
64
  if (res.status !== 200) {
47
65
  throw new Error(`Unexpected response status: ${res.status}`);
48
66
  }
49
- return res.data;
67
+ return camelizeKeys(res.data);
50
68
  } catch (error) {
51
69
  console.error(`Error fetching data from URL: ${url}`, error);
52
70
  throw error;
@@ -248,7 +266,8 @@ async function* parseSSE(response) {
248
266
  if (!json)
249
267
  continue;
250
268
  try {
251
- const event = JSON.parse(json);
269
+ const raw = JSON.parse(json);
270
+ const event = camelizeKeys(raw);
252
271
  yield event;
253
272
  if (event.type === "message_complete" || event.type === "error") {
254
273
  await reader.cancel();
@@ -298,7 +317,8 @@ async function jsonRequest(path, body, config) {
298
317
  if (!response.ok) {
299
318
  throw new Error(`AI request failed: ${response.status} ${response.statusText}`);
300
319
  }
301
- return response.json();
320
+ const raw = await response.json();
321
+ return camelizeKeys(raw);
302
322
  }
303
323
  function createAI(config) {
304
324
  async function chat(options) {
package/dist/index.d.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  type VideoAttachment = {
2
2
  id: string;
3
3
  title: string;
4
- file_url: string;
5
- file_size?: number;
6
- file_type?: string;
4
+ fileUrl: string;
5
+ fileSize?: number;
6
+ fileType?: string;
7
7
  };
8
8
  type VideoDownloadUrls = {
9
9
  mp4?: string;
10
10
  audio?: string;
11
- legacy_mp4?: string;
11
+ legacyMp4?: string;
12
12
  };
13
13
  type VideoSubtitles = {
14
14
  label: string;
@@ -27,27 +27,27 @@ type VideoMetadata = {
27
27
  };
28
28
  type Video = {
29
29
  captions: string;
30
- captions_label: string;
31
- captions_lang: string;
30
+ captionsLabel: string;
31
+ captionsLang: string;
32
32
  description: string | null;
33
33
  duration: number;
34
34
  id: string;
35
- imported_from: string | null;
36
- legacy_video_url: string | null;
37
- playback_id: string;
38
- published_at: string;
39
- stream_url: string;
35
+ importedFrom: string | null;
36
+ legacyVideoUrl: string | null;
37
+ playbackId: string;
38
+ publishedAt: string;
39
+ streamUrl: string;
40
40
  teaser: string | null;
41
41
  thumbnail: string;
42
42
  title: string;
43
43
  type: string;
44
- meta_data: VideoMetadata;
44
+ metaData: VideoMetadata;
45
45
  chapters?: string;
46
46
  attachments?: VideoAttachment[];
47
47
  cta?: any | null;
48
- download_urls?: VideoDownloadUrls;
49
- internal_id: string;
50
- playback_speed?: number;
48
+ downloadUrls?: VideoDownloadUrls;
49
+ internalId: string;
50
+ playbackSpeed?: number;
51
51
  subtitles?: VideoSubtitles;
52
52
  tags?: string[];
53
53
  transcript?: VideoTranscript;
@@ -55,20 +55,20 @@ type Video = {
55
55
  type Playlist = {
56
56
  description?: string;
57
57
  id: string;
58
- is_private: boolean;
58
+ isPrivate: boolean;
59
59
  title: string;
60
60
  type: string;
61
61
  videos: Video[];
62
62
  };
63
63
  type MenuItem = {
64
64
  icon: string;
65
- is_ext: boolean;
65
+ isExt: boolean;
66
66
  label: string;
67
67
  url: string;
68
68
  };
69
69
  type PortalDisplay = {
70
- show_chapters: boolean;
71
- show_transcripts: boolean;
70
+ showChapters: boolean;
71
+ showTranscripts: boolean;
72
72
  };
73
73
  type AssistantConfig = {
74
74
  headline: string;
@@ -76,36 +76,36 @@ type AssistantConfig = {
76
76
  suggestions: string[];
77
77
  };
78
78
  type PortalLayout = {
79
- assistant_config: AssistantConfig | null;
80
- show_playlists: boolean;
79
+ assistantConfig: AssistantConfig | null;
80
+ showPlaylists: boolean;
81
81
  type: string;
82
- videos_limit: number;
82
+ videosLimit: number;
83
83
  };
84
84
  type PortalNavigation = {
85
- show_ai_search: boolean;
86
- show_header: boolean;
87
- show_search: boolean;
85
+ showAiSearch: boolean;
86
+ showHeader: boolean;
87
+ showSearch: boolean;
88
88
  };
89
89
  type PortalTheme = {
90
90
  background: string;
91
91
  foreground: string;
92
92
  primary: string;
93
- font_body: string;
94
- font_header: string;
95
- logo_url: string;
96
- logo_dark_url: string;
97
- logo_width: number;
98
- logo_height: number;
99
- header_size: string;
93
+ fontBody: string;
94
+ fontHeader: string;
95
+ logoUrl: string;
96
+ logoDarkUrl: string;
97
+ logoWidth: number;
98
+ logoHeight: number;
99
+ headerSize: string;
100
100
  layout: string;
101
101
  radius: string;
102
- color_scheme: "toggle" | "light" | "dark";
102
+ colorScheme: "toggle" | "light" | "dark";
103
103
  light: ThemeColors;
104
104
  dark: ThemeColors;
105
- css_overrides: string | null;
105
+ cssOverrides: string | null;
106
106
  };
107
107
  type Portal = {
108
- color_scheme?: 'toggle' | 'light' | 'dark';
108
+ colorScheme?: 'toggle' | 'light' | 'dark';
109
109
  display: PortalDisplay;
110
110
  layout: PortalLayout;
111
111
  navigation: PortalNavigation;
@@ -115,21 +115,21 @@ type ThemeColors = {
115
115
  accent: string;
116
116
  background: string;
117
117
  foreground: string;
118
- accent_foreground: string;
118
+ accentForeground: string;
119
119
  muted: string;
120
- muted_foreground: string;
120
+ mutedForeground: string;
121
121
  border: string;
122
122
  ring: string;
123
123
  surface: string;
124
124
  };
125
125
  type ThemeConfig = {
126
126
  radius: string;
127
- color_scheme: "toggle" | "light" | "dark";
127
+ colorScheme: "toggle" | "light" | "dark";
128
128
  light: ThemeColors;
129
129
  dark: ThemeColors;
130
130
  };
131
131
  type AccountAI = {
132
- avatar_url: string;
132
+ avatarUrl: string;
133
133
  enabled: boolean;
134
134
  greeting: string;
135
135
  name: string;
@@ -141,7 +141,7 @@ type PersonaEnabled = {
141
141
  enabled: true;
142
142
  name: string;
143
143
  greeting: string;
144
- conversation_starters: string[];
144
+ conversationStarters: string[];
145
145
  };
146
146
  type PersonaDisabled = {
147
147
  enabled: false;
@@ -149,33 +149,33 @@ type PersonaDisabled = {
149
149
  type Persona = PersonaEnabled | PersonaDisabled;
150
150
  type Account = {
151
151
  ai: AccountAI;
152
- ai_search: AccountAISearch;
152
+ aiSearch: AccountAISearch;
153
153
  name: string;
154
154
  persona: Persona;
155
155
  slug: string;
156
156
  };
157
157
  type Settings = {
158
- featured_playlists: Playlist[];
159
- menu_items: MenuItem[];
160
- ai_avatar: string;
161
- ai_name: string;
162
- ai_greeting?: string;
163
- has_ai: boolean;
158
+ featuredPlaylists: Playlist[];
159
+ menuItems: MenuItem[];
160
+ aiAvatar: string;
161
+ aiName: string;
162
+ aiGreeting?: string;
163
+ hasAi: boolean;
164
164
  account: Account;
165
- favicon_url?: string;
166
- logo_dark_url?: string;
167
- logo_url?: string;
168
- meta_data: {
169
- channel_name: string;
165
+ faviconUrl?: string;
166
+ logoDarkUrl?: string;
167
+ logoUrl?: string;
168
+ metaData: {
169
+ channelName: string;
170
170
  description: string;
171
171
  image: string | null;
172
- no_seo: boolean;
173
- social_graph_image_url?: string;
172
+ noSeo: boolean;
173
+ socialGraphImageUrl?: string;
174
174
  title: string;
175
- title_suffix: string;
175
+ titleSuffix: string;
176
176
  };
177
177
  portal: Portal;
178
- theme_config: ThemeConfig;
178
+ themeConfig: ThemeConfig;
179
179
  version: string;
180
180
  };
181
181
  /**
@@ -183,29 +183,28 @@ type Settings = {
183
183
  */
184
184
  interface Source {
185
185
  id: string;
186
- video_id: string;
186
+ videoId: string;
187
187
  title: string;
188
188
  text: string;
189
189
  timestamp: number;
190
- timestamp_end: number;
191
- playback_id: string;
190
+ timestampEnd: number;
191
+ playbackId: string;
192
192
  speaker?: string;
193
193
  }
194
194
  /**
195
195
  * Token usage statistics
196
196
  */
197
197
  interface AIUsage {
198
- input_tokens: number;
199
- output_tokens: number;
198
+ inputTokens: number;
199
+ outputTokens: number;
200
200
  }
201
201
  /**
202
202
  * SSE event types for AI streaming responses
203
203
  */
204
204
  type AIEvent = {
205
205
  type: "message_start";
206
- id: string;
207
- conversation_id?: string;
208
- video_id?: string;
206
+ conversationId?: string;
207
+ videoId?: string;
209
208
  } | {
210
209
  type: "sources";
211
210
  sources: Source[];
@@ -221,8 +220,7 @@ type AIEvent = {
221
220
  recommendations: Recommendation[];
222
221
  } | {
223
222
  type: "message_complete";
224
- id: string;
225
- conversation_id?: string;
223
+ conversationId?: string;
226
224
  content: string;
227
225
  sources: Source[];
228
226
  usage?: AIUsage;
@@ -236,10 +234,13 @@ type AIEvent = {
236
234
  retryable: boolean;
237
235
  };
238
236
  /**
239
- * Non-streaming AI response
237
+ * Non-streaming AI response for /ai/chat, /ai/videos/:id/chat, and /ai/search
240
238
  */
241
239
  interface AIResponse {
242
- id: string;
240
+ conversationId?: string;
241
+ videoId?: string;
242
+ /** @deprecated Use conversationId instead. Will be removed in v2. */
243
+ id?: string;
243
244
  content: string;
244
245
  sources: Source[];
245
246
  usage: AIUsage;
@@ -295,9 +296,9 @@ interface SearchOptions {
295
296
  * A recommended video with relevance score
296
297
  */
297
298
  interface RecommendationVideo {
298
- video_id: string;
299
+ videoId: string;
299
300
  title: string;
300
- playback_id: string;
301
+ playbackId: string;
301
302
  relevance: number;
302
303
  reason: string;
303
304
  }
@@ -328,7 +329,6 @@ type RecommendOptions = RecommendationsOptions;
328
329
  * Non-streaming response for recommendations endpoint
329
330
  */
330
331
  interface RecommendationsResponse {
331
- id: string;
332
332
  recommendations: Recommendation[];
333
333
  guidance: string;
334
334
  sources: Source[];
package/dist/index.js CHANGED
@@ -1,6 +1,24 @@
1
1
  // src/lib/client.ts
2
2
  import axios from "axios";
3
3
 
4
+ // src/util/camelize.ts
5
+ var isPlainObject = (value) => value !== null && typeof value === "object" && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
6
+ var snakeToCamel = (key) => key.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
7
+ function camelizeKeys(input) {
8
+ if (Array.isArray(input)) {
9
+ return input.map((item) => camelizeKeys(item));
10
+ }
11
+ if (!isPlainObject(input)) {
12
+ return input;
13
+ }
14
+ const out = {};
15
+ for (const [rawKey, value] of Object.entries(input)) {
16
+ const key = snakeToCamel(rawKey);
17
+ out[key] = camelizeKeys(value);
18
+ }
19
+ return out;
20
+ }
21
+
4
22
  // src/lib/fetchers.ts
5
23
  async function get(client, url) {
6
24
  try {
@@ -8,7 +26,7 @@ async function get(client, url) {
8
26
  if (res.status !== 200) {
9
27
  throw new Error(`Unexpected response status: ${res.status}`);
10
28
  }
11
- return res.data;
29
+ return camelizeKeys(res.data);
12
30
  } catch (error) {
13
31
  console.error(`Error fetching data from URL: ${url}`, error);
14
32
  throw error;
@@ -210,7 +228,8 @@ async function* parseSSE(response) {
210
228
  if (!json)
211
229
  continue;
212
230
  try {
213
- const event = JSON.parse(json);
231
+ const raw = JSON.parse(json);
232
+ const event = camelizeKeys(raw);
214
233
  yield event;
215
234
  if (event.type === "message_complete" || event.type === "error") {
216
235
  await reader.cancel();
@@ -260,7 +279,8 @@ async function jsonRequest(path, body, config) {
260
279
  if (!response.ok) {
261
280
  throw new Error(`AI request failed: ${response.status} ${response.statusText}`);
262
281
  }
263
- return response.json();
282
+ const raw = await response.json();
283
+ return camelizeKeys(raw);
264
284
  }
265
285
  function createAI(config) {
266
286
  async function chat(options) {
package/llms.txt CHANGED
@@ -110,6 +110,8 @@ Key types exported:
110
110
  - `ChatOptions`, `SearchOptions`
111
111
  - `RecommendationsOptions`, `RecommendationsResponse`, `Recommendation`, `RecommendationVideo`
112
112
 
113
+ All response types use camelCase (e.g., `video.playbackId`, `video.publishedAt`, `settings.featuredPlaylists`, `source.videoId`).
114
+
113
115
  ## Links
114
116
 
115
117
  - [GitHub](https://github.com/boldvideo/bold-js)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@boldvideo/bold-js",
3
3
  "license": "MIT",
4
- "version": "1.7.0",
4
+ "version": "1.9.0",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
7
7
  "types": "dist/index.d.ts",