@codenkay/video-nsgplayer-core 0.1.0 → 0.1.3

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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.3
4
+
5
+ - Fix CDN **403 on child HLS playlists/segments** (`stream_N.m3u8`, `.ts`, keys): hls.js now uses an auth-params loader that re-applies signed `token`/`expires` query params on every request, not only the master playlist.
6
+ - Preserve optional `streaming.keyProxyUrlBuilder` through config merge.
7
+
8
+ ## 0.1.2
9
+
10
+ - Fix `InternalError: too much recursion` / stack overflow in `assertNoSecretsInConfig` when `createVideoPlayer` options include an `HTMLVideoElement` mount or credential callbacks.
11
+ - Secret scanning is now plain-object only, iterative, depth-capped, and does not deep-walk `mount` / `auth` / `playback`.
12
+ - Harden `redactObject` against circular graphs.
13
+
14
+ ## 0.1.1
15
+
16
+ - Live-edge and config scan hardening.
17
+
3
18
  ## 0.1.0
4
19
 
5
20
  - Initial release
package/dist/index.cjs CHANGED
@@ -44,15 +44,19 @@ __export(index_exports, {
44
44
  PlaybackSourceManager: () => PlaybackSourceManager,
45
45
  PlayerErrorCode: () => PlayerErrorCode,
46
46
  RefreshManager: () => RefreshManager,
47
+ SECRET_SCAN_MAX_DEPTH: () => SECRET_SCAN_MAX_DEPTH,
47
48
  assertNoSecretsInConfig: () => assertNoSecretsInConfig,
49
+ createAuthParamsLoader: () => createAuthParamsLoader,
48
50
  createPlayerError: () => createPlayerError,
49
51
  createVideoPlayer: () => createVideoPlayer,
50
52
  getLiveEdgeTime: () => getLiveEdgeTime,
51
53
  isAtLiveEdge: () => isAtLiveEdge,
54
+ isPlainObject: () => isPlainObject,
52
55
  mergePlayerConfig: () => mergePlayerConfig,
53
56
  redactObject: () => redactObject,
54
57
  redactSensitive: () => redactSensitive,
55
58
  redactValue: () => redactValue,
59
+ rewriteHlsRequestUrl: () => rewriteHlsRequestUrl,
56
60
  seekToLiveEdge: () => seekToLiveEdge,
57
61
  themeToCssVars: () => themeToCssVars
58
62
  });
@@ -206,7 +210,7 @@ var AuthManager = class {
206
210
 
207
211
  // src/config/defaults.ts
208
212
  var PLAYBACK_RATES = [0.5, 0.75, 1, 1.25, 1.5, 2];
209
- var PACKAGE_VERSION = "0.1.0";
213
+ var PACKAGE_VERSION = "0.1.3";
210
214
  var DEFAULT_PLAYER_BEHAVIOR = {
211
215
  autoplay: true,
212
216
  startMuted: true,
@@ -292,22 +296,83 @@ var FORBIDDEN_SECRET_KEYS = [
292
296
  "basicauthpassword",
293
297
  "password"
294
298
  ];
299
+ var SKIP_DEEP_SCAN_KEYS = /* @__PURE__ */ new Set([
300
+ "mount",
301
+ "auth",
302
+ "playback",
303
+ "gettoken",
304
+ "getsource",
305
+ "refreshsource",
306
+ "keyproxyurlbuilder"
307
+ ]);
308
+ var SECRET_SCAN_MAX_DEPTH = 32;
309
+ function isPlainObject(value) {
310
+ if (value === null || typeof value !== "object") return false;
311
+ if (Array.isArray(value)) return false;
312
+ const proto = Object.getPrototypeOf(value);
313
+ return proto === Object.prototype || proto === null;
314
+ }
315
+ function normalizeConfigKey(key) {
316
+ return key.toLowerCase().replace(/[^a-z0-9]/g, "");
317
+ }
318
+ function isForbiddenSecretKey(key) {
319
+ const lower = normalizeConfigKey(key);
320
+ return FORBIDDEN_SECRET_KEYS.some((k) => lower.includes(k));
321
+ }
322
+ function throwForbidden(path, key) {
323
+ throw new Error(
324
+ `INVALID_CONFIGURATION: Forbidden secret key "${path}${key}" must never be passed to the browser SDK. Keep clientSecret on your server/BFF and use auth.getToken().`
325
+ );
326
+ }
295
327
  function assertNoSecretsInConfig(raw) {
296
- if (!raw || typeof raw !== "object") return;
297
- const walk = (obj, path) => {
298
- for (const [key, value] of Object.entries(obj)) {
299
- const lower = key.toLowerCase().replace(/[^a-z0-9]/g, "");
300
- if (FORBIDDEN_SECRET_KEYS.some((k) => lower.includes(k))) {
301
- throw new Error(
302
- `INVALID_CONFIGURATION: Forbidden secret key "${path}${key}" must never be passed to the browser SDK. Keep clientSecret on your server/BFF and use auth.getToken().`
303
- );
328
+ if (!isPlainObject(raw) && !Array.isArray(raw)) return;
329
+ const seen = /* @__PURE__ */ new WeakSet();
330
+ const stack = [
331
+ { value: raw, path: "", depth: 0 }
332
+ ];
333
+ while (stack.length > 0) {
334
+ const current = stack.pop();
335
+ if (current.depth > SECRET_SCAN_MAX_DEPTH) continue;
336
+ const { value, path, depth } = current;
337
+ if (Array.isArray(value)) {
338
+ if (seen.has(value)) continue;
339
+ seen.add(value);
340
+ for (let i = 0; i < value.length; i += 1) {
341
+ const item = value[i];
342
+ if (isPlainObject(item) || Array.isArray(item)) {
343
+ stack.push({ value: item, path: `${path}[${i}]`, depth: depth + 1 });
344
+ }
345
+ }
346
+ continue;
347
+ }
348
+ if (!isPlainObject(value)) continue;
349
+ if (seen.has(value)) continue;
350
+ seen.add(value);
351
+ for (const [key, child] of Object.entries(value)) {
352
+ const normalized = normalizeConfigKey(key);
353
+ if (isForbiddenSecretKey(key)) {
354
+ throwForbidden(path, key);
355
+ }
356
+ if (normalized === "mount") continue;
357
+ if (SKIP_DEEP_SCAN_KEYS.has(normalized)) {
358
+ if (isPlainObject(child)) {
359
+ for (const nestedKey of Object.keys(child)) {
360
+ if (isForbiddenSecretKey(nestedKey)) {
361
+ throwForbidden(`${path}${key}.`, nestedKey);
362
+ }
363
+ }
364
+ }
365
+ continue;
304
366
  }
305
- if (value && typeof value === "object" && !Array.isArray(value)) {
306
- walk(value, `${path}${key}.`);
367
+ if (isPlainObject(child) || Array.isArray(child)) {
368
+ stack.push({
369
+ value: child,
370
+ path: path ? `${path}${key}.` : `${key}.`,
371
+ depth: depth + 1
372
+ });
307
373
  }
308
374
  }
309
- };
310
- walk(raw, "");
375
+ }
311
376
  }
312
377
  function mergePlayerConfig(partial) {
313
378
  assertNoSecretsInConfig(partial);
@@ -321,7 +386,8 @@ function mergePlayerConfig(partial) {
321
386
  hls: {
322
387
  ...DEFAULT_PLAYER_CONFIG.streaming.hls,
323
388
  ...partial?.streaming?.hls
324
- }
389
+ },
390
+ keyProxyUrlBuilder: partial?.streaming?.keyProxyUrlBuilder
325
391
  },
326
392
  logging: { ...DEFAULT_PLAYER_CONFIG.logging, ...partial?.logging },
327
393
  debug: { ...DEFAULT_PLAYER_CONFIG.debug, ...partial?.debug },
@@ -395,7 +461,14 @@ var EventEmitter = class {
395
461
 
396
462
  // src/security/redaction.ts
397
463
  var SENSITIVE_KEYS = /^(authorization|cookie|set-cookie|x-api-key|token|accessToken|access_token|clientSecret|password)$/i;
398
- function redactValue(key, value) {
464
+ var REDACT_MAX_DEPTH = 32;
465
+ function isPlainObject2(value) {
466
+ if (value === null || typeof value !== "object") return false;
467
+ if (Array.isArray(value)) return false;
468
+ const proto = Object.getPrototypeOf(value);
469
+ return proto === Object.prototype || proto === null;
470
+ }
471
+ function redactValue(key, value, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
399
472
  if (SENSITIVE_KEYS.test(key)) return "[REDACTED]";
400
473
  if (typeof value === "string") {
401
474
  if (/^https?:\/\//i.test(value) && /[?&](token|expires)=/i.test(value)) {
@@ -412,15 +485,25 @@ function redactValue(key, value) {
412
485
  }
413
486
  }
414
487
  }
415
- if (value && typeof value === "object" && !Array.isArray(value)) {
416
- return redactObject(value);
488
+ if (Array.isArray(value)) {
489
+ if (depth >= REDACT_MAX_DEPTH || seen.has(value)) return "[REDACTED_CYCLE]";
490
+ seen.add(value);
491
+ return value.map(
492
+ (item, index) => redactValue(String(index), item, seen, depth + 1)
493
+ );
494
+ }
495
+ if (isPlainObject2(value)) {
496
+ return redactObject(value, seen, depth + 1);
417
497
  }
418
498
  return value;
419
499
  }
420
- function redactObject(obj) {
500
+ function redactObject(obj, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
501
+ if (depth > REDACT_MAX_DEPTH) return { "[REDACTED]": "max_depth" };
502
+ if (seen.has(obj)) return { "[REDACTED]": "cycle" };
503
+ seen.add(obj);
421
504
  const out = {};
422
505
  for (const [key, value] of Object.entries(obj)) {
423
- out[key] = redactValue(key, value);
506
+ out[key] = redactValue(key, value, seen, depth);
424
507
  }
425
508
  return out;
426
509
  }
@@ -480,11 +563,25 @@ var AuthParamsStore = class {
480
563
  });
481
564
  return out;
482
565
  }
566
+ /** True when at least one auth query param is stored. */
567
+ hasParams() {
568
+ return [...this.params.keys()].length > 0;
569
+ }
570
+ toString() {
571
+ return this.params.toString();
572
+ }
483
573
  update(next) {
484
574
  this.params = next instanceof URLSearchParams ? new URLSearchParams(next) : new URLSearchParams(next);
485
575
  }
576
+ /**
577
+ * Merges current auth params onto a target URL, preserving unrelated query keys.
578
+ * Critical for child HLS playlists/segments that omit signed query params.
579
+ */
486
580
  applyToUrl(rawUrl, baseOrigin) {
487
- const target = new URL(rawUrl, baseOrigin ?? (typeof location !== "undefined" ? location.origin : "http://localhost"));
581
+ const target = new URL(
582
+ rawUrl,
583
+ baseOrigin ?? (typeof location !== "undefined" ? location.origin : "http://localhost")
584
+ );
488
585
  this.params.forEach((value, key) => {
489
586
  target.searchParams.set(key, value);
490
587
  });
@@ -719,6 +816,39 @@ function seekToLiveEdge(media, options) {
719
816
  return { moved: true, edge };
720
817
  }
721
818
 
819
+ // src/streaming/authParamsLoader.ts
820
+ var ENC_KEY_PATH_RE = /\/enc\.key$/i;
821
+ function rewriteHlsRequestUrl(rawUrl, store, options = {}, baseOrigin = typeof location !== "undefined" ? location.origin : "http://localhost") {
822
+ if (!rawUrl) return rawUrl;
823
+ const parsed = new URL(rawUrl, baseOrigin);
824
+ const isKey = ENC_KEY_PATH_RE.test(parsed.pathname);
825
+ if (isKey && options.keyProxyUrlBuilder && options.videoId) {
826
+ const snapshot = store.toRecord();
827
+ return options.keyProxyUrlBuilder({
828
+ videoId: options.videoId,
829
+ keyUrl: parsed.toString(),
830
+ token: snapshot.token,
831
+ expires: snapshot.expires
832
+ });
833
+ }
834
+ if (!store.hasParams()) return parsed.toString();
835
+ return store.applyToUrl(rawUrl, baseOrigin);
836
+ }
837
+ function createAuthParamsLoader(HlsClass, store, options = {}) {
838
+ const BaseLoader = HlsClass.DefaultConfig.loader;
839
+ return class AuthParamsLoader extends BaseLoader {
840
+ load(context, config, callbacks) {
841
+ if (context.url) {
842
+ try {
843
+ context.url = rewriteHlsRequestUrl(context.url, store, options);
844
+ } catch {
845
+ }
846
+ }
847
+ super.load(context, config, callbacks);
848
+ }
849
+ };
850
+ }
851
+
722
852
  // src/streaming/HlsEngine.ts
723
853
  var HlsEngine = class {
724
854
  constructor(options) {
@@ -771,15 +901,16 @@ var HlsEngine = class {
771
901
  }
772
902
  }
773
903
  async startWithHlsJs(Hls, playbackUrl) {
774
- const { videoEl, authParams, hlsConfig, callbacks } = this.options;
904
+ const { videoEl, authParams, hlsConfig, callbacks, videoId, keyProxyUrlBuilder } = this.options;
905
+ const loader = createAuthParamsLoader(Hls, authParams, {
906
+ videoId,
907
+ keyProxyUrlBuilder
908
+ });
775
909
  this.hls = new Hls({
776
910
  enableWorker: hlsConfig.enableWorker,
777
911
  lowLatencyMode: hlsConfig.lowLatencyMode,
778
912
  backBufferLength: hlsConfig.backBufferLength,
779
- xhrSetup: (xhr, url) => {
780
- void xhr;
781
- void url;
782
- }
913
+ loader
783
914
  });
784
915
  this.hls.attachMedia(videoEl);
785
916
  this.hls.loadSource(authParams.applyToUrl(playbackUrl));
@@ -890,7 +1021,18 @@ function generateSessionId() {
890
1021
  return `ps_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
891
1022
  }
892
1023
  function createVideoPlayer(options) {
893
- assertNoSecretsInConfig(options);
1024
+ assertNoSecretsInConfig({
1025
+ videoId: options.videoId,
1026
+ sourceUrl: options.sourceUrl,
1027
+ player: options.player,
1028
+ controls: options.controls,
1029
+ theme: options.theme,
1030
+ watermark: options.watermark,
1031
+ streaming: options.streaming,
1032
+ logging: options.logging,
1033
+ debug: options.debug,
1034
+ accessibility: options.accessibility
1035
+ });
894
1036
  let config = mergePlayerConfig({
895
1037
  player: options.player,
896
1038
  controls: options.controls,
@@ -976,6 +1118,8 @@ function createVideoPlayer(options) {
976
1118
  startMuted: config.player.startMuted,
977
1119
  startPosition: config.player.startPosition,
978
1120
  volume: config.player.volume,
1121
+ videoId: options.videoId,
1122
+ keyProxyUrlBuilder: options.streaming?.keyProxyUrlBuilder,
979
1123
  hlsConfig: config.streaming.hls,
980
1124
  logger,
981
1125
  callbacks: {
@@ -1236,7 +1380,8 @@ function createVideoPlayer(options) {
1236
1380
  watermark: { ...config.watermark, ...partial?.watermark },
1237
1381
  streaming: {
1238
1382
  engine: "hls",
1239
- hls: { ...config.streaming.hls, ...partial?.streaming?.hls }
1383
+ hls: { ...config.streaming.hls, ...partial?.streaming?.hls },
1384
+ keyProxyUrlBuilder: partial?.streaming?.keyProxyUrlBuilder ?? config.streaming.keyProxyUrlBuilder
1240
1385
  },
1241
1386
  logging: { ...config.logging, ...partial?.logging },
1242
1387
  debug: { ...config.debug, ...partial?.debug },
@@ -1273,15 +1418,19 @@ function createVideoPlayer(options) {
1273
1418
  PlaybackSourceManager,
1274
1419
  PlayerErrorCode,
1275
1420
  RefreshManager,
1421
+ SECRET_SCAN_MAX_DEPTH,
1276
1422
  assertNoSecretsInConfig,
1423
+ createAuthParamsLoader,
1277
1424
  createPlayerError,
1278
1425
  createVideoPlayer,
1279
1426
  getLiveEdgeTime,
1280
1427
  isAtLiveEdge,
1428
+ isPlainObject,
1281
1429
  mergePlayerConfig,
1282
1430
  redactObject,
1283
1431
  redactSensitive,
1284
1432
  redactValue,
1433
+ rewriteHlsRequestUrl,
1285
1434
  seekToLiveEdge,
1286
1435
  themeToCssVars
1287
1436
  });