@nitida/asset-client 0.16.2 → 0.16.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/dist/index.d.cts CHANGED
@@ -51,14 +51,14 @@ declare function getTextColorForBackground(swatch: PaletteSwatch | string | null
51
51
  /** Luminancia relativa WCAG de un hex. 0 = negro, 1 = blanco. */
52
52
  declare function relativeLuminance(hex: string): number;
53
53
  /**
54
- * Razón de contraste WCAG entre dos luminancias: `(L1 + 0.05) / (L2 + 0.05)`,
55
- * con la más clara arriba. 1 = idénticos, 21 = negro contra blanco.
54
+ * WCAG contrast ratio between two luminances: `(L1 + 0.05) / (L2 + 0.05)`,
55
+ * lighter on top. 1 = identical, 21 = black against white.
56
56
  */
57
57
  declare function contrastRatio(l1: number, l2: number): number;
58
58
  /**
59
- * Igual que el color recomendado, pero devuelve también **la razón lograda** y
60
- * si pasa AA para que quien lo use pueda decidir con el número a la vista en
61
- * vez de asumir que alcanzó.
59
+ * Same choice as the recommended text colour, but it also returns **the ratio
60
+ * it achieved** and whether that passes AA so a caller can decide with the
61
+ * number in front of them instead of assuming it was enough.
62
62
  */
63
63
  declare function bestTextContrast(hex: string): {
64
64
  color: "#000000" | "#FFFFFF";
@@ -98,9 +98,9 @@ declare function getPaletteBlurBackground(palette: AssetPalette | null | undefin
98
98
  * Slots give tenants a way to attach stable, human-readable names
99
99
  * ("webapp.wizard.pool-type.icon-1", "storefront.cr.hero-video.landscape_hd_16x9.mp4")
100
100
  * to assets they uploaded. Consumers resolve names → AssetDTOs at
101
- * build / runtime so their source never hardcodes a CDN URL; the
102
- * admin rebinds a slot from `asset-lab-web` and every consumer picks
103
- * up the swap on cache refresh.
101
+ * build / runtime so their source never hardcodes a CDN URL; an
102
+ * admin rebinds a slot in the platform console and every consumer
103
+ * picks up the swap on cache refresh.
104
104
  *
105
105
  * Two layers in this package:
106
106
  * - `resolveSlot` / `resolveSlots` — universal (server, edge,
@@ -137,8 +137,8 @@ type SlotResolution = {
137
137
  *
138
138
  * configureSlotResolver({
139
139
  * endpoint: process.env.AQUIENPZ_URL,
140
- * apiKey: process.env.ASSET_MANAGER_RUNTIME_KEY,
141
- * tenantCode: "realtyone-cr",
140
+ * apiKey: process.env.AQUIENPZ_API_KEY, // amk_rt_* — server-only
141
+ * tenantCode: "acme-co",
142
142
  * });
143
143
  */
144
144
  declare function configureSlotResolver(opts: {
@@ -336,13 +336,28 @@ declare function getVideoTransformUrl(asset: Pick<AssetDTO, "sha">, opts: Transf
336
336
  * if (prefersNativeHls(video)) {
337
337
  * video.src = src;
338
338
  * } else {
339
- * // Defaults open at a fixed low rung measure instead of guessing.
340
- * const hls = new Hls({ startLevel: -1, testBandwidth: true, abrEwmaDefaultEstimate: 1_000_000 });
339
+ * const hls = new Hls({ capLevelToPlayerSize: false, abrEwmaDefaultEstimate: 5_000_000 });
341
340
  * hls.loadSource(src);
342
341
  * hls.attachMedia(video);
343
342
  * }
344
343
  * ```
345
344
  *
345
+ * ⚠️ **Do NOT pass `startLevel: -1` with `testBandwidth: true`.** That pair is
346
+ * documented by hls.js as *"forces the player to download a fragment from the
347
+ * lowest level to establish a bandwidth estimate"* — on a clip short enough to
348
+ * be one segment, the probe IS the whole video, and it plays at the bottom
349
+ * rung from first frame to last. (This doc-comment recommended exactly that
350
+ * until 2026-08-18; a 5.042 s 4K asset was measured being delivered at
351
+ * 426x240 because of it.) Leave `startLevel` unset: hls.js then opens on the
352
+ * FIRST level in the manifest, and the server puts the right one there —
353
+ * a mid rung for long video, the top rung for a clip under 18 s, which is the
354
+ * same rung native HLS opens on per RFC 8216 §6.3.4. The ladder decides; the
355
+ * player should not second-guess it.
356
+ *
357
+ * `capLevelToPlayerSize` is worth disabling explicitly: `@videojs/core`
358
+ * defaults it to `true`, which caps quality to the player's rendered pixel box,
359
+ * so a small inline player is pinned to 240p/360p on any connection.
360
+ *
346
361
  * On first request the server returns 202 Accepted while a background
347
362
  * job transcodes the ladder (typically 1-3 min for a 90 s source);
348
363
  * subsequent requests get 302 to the cached master.m3u8. Keep the
@@ -416,7 +431,7 @@ declare function getTransformSrcSet(asset: Pick<AssetDTO, "sha">, widths: number
416
431
  * USED for, removing the implicit landscape-only assumption that bit us
417
432
  * with vertical product photos.
418
433
  */
419
- type VariantPreset = "thumb" | "sm" | "md" | "lg" | "xl" | "original" | "poster" | "video" | "aiproxy" | "mp3";
434
+ type VariantPreset = "thumb" | "sm" | "md" | "lg" | "xl" | "original" | "poster" | "video" | "aiproxy" | "hls" | "mp3";
420
435
  /** 1-char alias used in storage keys / wire `presets` string. */
421
436
  declare const PRESET_SHORT: Record<VariantPreset, string>;
422
437
  declare const PRESET_LONG: Record<string, VariantPreset>;
@@ -451,9 +466,79 @@ type AssetVariant = {
451
466
  *
452
467
  * Absent on variants written before the trace field existed.
453
468
  */
454
- sourceFrom?: VariantPreset | "upload";
469
+ sourceFrom?: VariantPreset | "upload" | "transform-route" | "hls-transcode" | "cdn-probe";
455
470
  /** ISO timestamp this variant was written. Absent on pre-trace variants. */
456
471
  createdAt?: string;
472
+ /**
473
+ * **`preset: "hls"` only** — the ladder's rungs, in MASTER ORDER.
474
+ *
475
+ * The reason this exists: an entry that only says *there is a ladder* leaves
476
+ * a consumer that plans a composition exactly as blind as no entry at all,
477
+ * because the ceiling of a composition is its weakest ingredient and there is
478
+ * no upscale. Before this field the only way to learn a clip's real rungs was
479
+ * to fetch the master playlist — or worse, download the asset.
480
+ *
481
+ * `rungs[0]` is the rung every client OPENS on (RFC 8216 §6.3.4 for native
482
+ * HLS; hls.js with `startLevel` unset uses "the first level in the
483
+ * manifest"), so the order is a delivery fact — do not sort it in place.
484
+ *
485
+ * Use {@link hlsLadderAlignment} rather than eyeballing `segments`: a ladder
486
+ * can be complete and still unable to adapt.
487
+ */
488
+ rungs?: HlsRung[];
489
+ };
490
+ /**
491
+ * One rung of an adaptive HLS ladder.
492
+ *
493
+ * `segments` / `durationSec` are what make a ladder JUDGEABLE rather than
494
+ * merely present. Measured on a prod ladder of an 87 s 4K source: 240p cut at
495
+ * 14.35 s in 12 segments, 720p at 5.63 s in 12, 2160p at 5.88 s in 14. Cuts
496
+ * that do not line up cannot be swapped, and a swap is what a rendition switch
497
+ * IS — so that ladder looked complete and adapted badly. One segment means zero
498
+ * switch points: whichever rung the player opens on is the rung it finishes on.
499
+ *
500
+ * Both are optional because a rung whose playlist could not be read is recorded
501
+ * WITHOUT them rather than with a zero — unmeasured and none are different
502
+ * facts, and a `0` there would read as the latter.
503
+ */
504
+ type HlsRung = {
505
+ /** Rung directory / identity — `"720p"`. */
506
+ name: string;
507
+ /** `RESOLUTION` from the master playlist. */
508
+ width: number;
509
+ height: number;
510
+ /** `BANDWIDTH` in bits per second. */
511
+ bandwidth: number;
512
+ /** Absolute URL of this rung's media playlist. */
513
+ url: string;
514
+ /** `#EXTINF` count. */
515
+ segments?: number;
516
+ /** Sum of the `#EXTINF` values, seconds. */
517
+ durationSec?: number;
518
+ };
519
+ /**
520
+ * The ladder of an asset, or `null` when it has none / the DTO does not carry
521
+ * `variants` (the slim list shape never does — use `hasPreset(a, "hls")` there).
522
+ */
523
+ declare function getHlsLadder(asset: Pick<AssetDTO, "variants">): AssetVariant | null;
524
+ /**
525
+ * What the recorded rungs actually support — computed here, never stored, so
526
+ * one rule serves every consumer and a change to it does not need a backfill.
527
+ *
528
+ * - `switchable`: more than one rung AND more than one segment. False means the
529
+ * player is pinned to whatever rung it opens on for the entire clip.
530
+ * - `aligned`: every measured rung reports the same segment count. `null` means
531
+ * fewer than two rungs were measured — **unknown, not false.** Treating that
532
+ * as `false` rejects ladders nobody looked at.
533
+ * - `ceilingHeight`: the tallest rung. The ceiling of any composition using it.
534
+ */
535
+ declare function hlsLadderAlignment(rungs: HlsRung[]): {
536
+ rungCount: number;
537
+ ceilingHeight: number;
538
+ floorHeight: number;
539
+ switchable: boolean;
540
+ aligned: boolean | null;
541
+ minSegments: number | null;
457
542
  };
458
543
  /**
459
544
  * Compact wire shape — what the server actually sends. Aliases (`w`, `h`,
@@ -557,7 +642,7 @@ declare function getAssetUrl(asset: Pick<AssetDTO, "sha"> & OriginalHints, prese
557
642
  * Reads `dto.presets` — the compact 1-char code string the server sends on EVERY shape, including
558
643
  * the slim list/resolver one that carries no `variants` at all. That is why this exists and why it
559
644
  * stays the right existence check even now that `GET /assets/:id` really does send `variants`
560
- * (it did not until 2026-08-17; doc 240 §4.3).
645
+ * (it did not until the 2026-08-17 deploy).
561
646
  *
562
647
  * @example
563
648
  * ```ts
@@ -586,4 +671,4 @@ declare function getAssetDimensions(asset: Pick<AssetDTO, "w" | "h">): {
586
671
  height: number;
587
672
  } | null;
588
673
 
589
- export { type AssetDTO, type AssetPalette, type AssetVariant, PRESET_EXT, PRESET_LONG, PRESET_MAX_DIM, PRESET_SHORT, type PaletteSwatch, type ResolveSlotOptions, type SignedTransformOptions, type SlotDTO, type SlotResolution, TRANSFORM_WIDTHS, type TransformEffect, type TransformFit, type TransformFormat, type TransformGravity, type TransformOptions, type TransformWidth, type VariantPreset, bestTextContrast, computeVariantDimensions, configureSlotResolver, contrastRatio, extractAssetSha, getAmbientGradient, getAssetDimensions, getAssetSrcSet, getAssetUrl, getCdnBase, getHlsStreamingUrl, getPaletteBlurBackground, getPaletteCssVars, getSignedTransformUrl, getTenantId, getTextColorForBackground, getTransformSrcSet, getTransformUrl, getVideoTransformUrl, hasPreset, invalidateSlotCache, iteratePaletteSwatches, pickAmbientBackground, relativeLuminance, resolveSlot, resolveSlots, serializeTransform, setCdnBase, setTenantId, signTransformUrl };
674
+ export { type AssetDTO, type AssetPalette, type AssetVariant, type HlsRung, PRESET_EXT, PRESET_LONG, PRESET_MAX_DIM, PRESET_SHORT, type PaletteSwatch, type ResolveSlotOptions, type SignedTransformOptions, type SlotDTO, type SlotResolution, TRANSFORM_WIDTHS, type TransformEffect, type TransformFit, type TransformFormat, type TransformGravity, type TransformOptions, type TransformWidth, type VariantPreset, bestTextContrast, computeVariantDimensions, configureSlotResolver, contrastRatio, extractAssetSha, getAmbientGradient, getAssetDimensions, getAssetSrcSet, getAssetUrl, getCdnBase, getHlsLadder, getHlsStreamingUrl, getPaletteBlurBackground, getPaletteCssVars, getSignedTransformUrl, getTenantId, getTextColorForBackground, getTransformSrcSet, getTransformUrl, getVideoTransformUrl, hasPreset, hlsLadderAlignment, invalidateSlotCache, iteratePaletteSwatches, pickAmbientBackground, relativeLuminance, resolveSlot, resolveSlots, serializeTransform, setCdnBase, setTenantId, signTransformUrl };
package/dist/index.d.ts CHANGED
@@ -51,14 +51,14 @@ declare function getTextColorForBackground(swatch: PaletteSwatch | string | null
51
51
  /** Luminancia relativa WCAG de un hex. 0 = negro, 1 = blanco. */
52
52
  declare function relativeLuminance(hex: string): number;
53
53
  /**
54
- * Razón de contraste WCAG entre dos luminancias: `(L1 + 0.05) / (L2 + 0.05)`,
55
- * con la más clara arriba. 1 = idénticos, 21 = negro contra blanco.
54
+ * WCAG contrast ratio between two luminances: `(L1 + 0.05) / (L2 + 0.05)`,
55
+ * lighter on top. 1 = identical, 21 = black against white.
56
56
  */
57
57
  declare function contrastRatio(l1: number, l2: number): number;
58
58
  /**
59
- * Igual que el color recomendado, pero devuelve también **la razón lograda** y
60
- * si pasa AA para que quien lo use pueda decidir con el número a la vista en
61
- * vez de asumir que alcanzó.
59
+ * Same choice as the recommended text colour, but it also returns **the ratio
60
+ * it achieved** and whether that passes AA so a caller can decide with the
61
+ * number in front of them instead of assuming it was enough.
62
62
  */
63
63
  declare function bestTextContrast(hex: string): {
64
64
  color: "#000000" | "#FFFFFF";
@@ -98,9 +98,9 @@ declare function getPaletteBlurBackground(palette: AssetPalette | null | undefin
98
98
  * Slots give tenants a way to attach stable, human-readable names
99
99
  * ("webapp.wizard.pool-type.icon-1", "storefront.cr.hero-video.landscape_hd_16x9.mp4")
100
100
  * to assets they uploaded. Consumers resolve names → AssetDTOs at
101
- * build / runtime so their source never hardcodes a CDN URL; the
102
- * admin rebinds a slot from `asset-lab-web` and every consumer picks
103
- * up the swap on cache refresh.
101
+ * build / runtime so their source never hardcodes a CDN URL; an
102
+ * admin rebinds a slot in the platform console and every consumer
103
+ * picks up the swap on cache refresh.
104
104
  *
105
105
  * Two layers in this package:
106
106
  * - `resolveSlot` / `resolveSlots` — universal (server, edge,
@@ -137,8 +137,8 @@ type SlotResolution = {
137
137
  *
138
138
  * configureSlotResolver({
139
139
  * endpoint: process.env.AQUIENPZ_URL,
140
- * apiKey: process.env.ASSET_MANAGER_RUNTIME_KEY,
141
- * tenantCode: "realtyone-cr",
140
+ * apiKey: process.env.AQUIENPZ_API_KEY, // amk_rt_* — server-only
141
+ * tenantCode: "acme-co",
142
142
  * });
143
143
  */
144
144
  declare function configureSlotResolver(opts: {
@@ -336,13 +336,28 @@ declare function getVideoTransformUrl(asset: Pick<AssetDTO, "sha">, opts: Transf
336
336
  * if (prefersNativeHls(video)) {
337
337
  * video.src = src;
338
338
  * } else {
339
- * // Defaults open at a fixed low rung measure instead of guessing.
340
- * const hls = new Hls({ startLevel: -1, testBandwidth: true, abrEwmaDefaultEstimate: 1_000_000 });
339
+ * const hls = new Hls({ capLevelToPlayerSize: false, abrEwmaDefaultEstimate: 5_000_000 });
341
340
  * hls.loadSource(src);
342
341
  * hls.attachMedia(video);
343
342
  * }
344
343
  * ```
345
344
  *
345
+ * ⚠️ **Do NOT pass `startLevel: -1` with `testBandwidth: true`.** That pair is
346
+ * documented by hls.js as *"forces the player to download a fragment from the
347
+ * lowest level to establish a bandwidth estimate"* — on a clip short enough to
348
+ * be one segment, the probe IS the whole video, and it plays at the bottom
349
+ * rung from first frame to last. (This doc-comment recommended exactly that
350
+ * until 2026-08-18; a 5.042 s 4K asset was measured being delivered at
351
+ * 426x240 because of it.) Leave `startLevel` unset: hls.js then opens on the
352
+ * FIRST level in the manifest, and the server puts the right one there —
353
+ * a mid rung for long video, the top rung for a clip under 18 s, which is the
354
+ * same rung native HLS opens on per RFC 8216 §6.3.4. The ladder decides; the
355
+ * player should not second-guess it.
356
+ *
357
+ * `capLevelToPlayerSize` is worth disabling explicitly: `@videojs/core`
358
+ * defaults it to `true`, which caps quality to the player's rendered pixel box,
359
+ * so a small inline player is pinned to 240p/360p on any connection.
360
+ *
346
361
  * On first request the server returns 202 Accepted while a background
347
362
  * job transcodes the ladder (typically 1-3 min for a 90 s source);
348
363
  * subsequent requests get 302 to the cached master.m3u8. Keep the
@@ -416,7 +431,7 @@ declare function getTransformSrcSet(asset: Pick<AssetDTO, "sha">, widths: number
416
431
  * USED for, removing the implicit landscape-only assumption that bit us
417
432
  * with vertical product photos.
418
433
  */
419
- type VariantPreset = "thumb" | "sm" | "md" | "lg" | "xl" | "original" | "poster" | "video" | "aiproxy" | "mp3";
434
+ type VariantPreset = "thumb" | "sm" | "md" | "lg" | "xl" | "original" | "poster" | "video" | "aiproxy" | "hls" | "mp3";
420
435
  /** 1-char alias used in storage keys / wire `presets` string. */
421
436
  declare const PRESET_SHORT: Record<VariantPreset, string>;
422
437
  declare const PRESET_LONG: Record<string, VariantPreset>;
@@ -451,9 +466,79 @@ type AssetVariant = {
451
466
  *
452
467
  * Absent on variants written before the trace field existed.
453
468
  */
454
- sourceFrom?: VariantPreset | "upload";
469
+ sourceFrom?: VariantPreset | "upload" | "transform-route" | "hls-transcode" | "cdn-probe";
455
470
  /** ISO timestamp this variant was written. Absent on pre-trace variants. */
456
471
  createdAt?: string;
472
+ /**
473
+ * **`preset: "hls"` only** — the ladder's rungs, in MASTER ORDER.
474
+ *
475
+ * The reason this exists: an entry that only says *there is a ladder* leaves
476
+ * a consumer that plans a composition exactly as blind as no entry at all,
477
+ * because the ceiling of a composition is its weakest ingredient and there is
478
+ * no upscale. Before this field the only way to learn a clip's real rungs was
479
+ * to fetch the master playlist — or worse, download the asset.
480
+ *
481
+ * `rungs[0]` is the rung every client OPENS on (RFC 8216 §6.3.4 for native
482
+ * HLS; hls.js with `startLevel` unset uses "the first level in the
483
+ * manifest"), so the order is a delivery fact — do not sort it in place.
484
+ *
485
+ * Use {@link hlsLadderAlignment} rather than eyeballing `segments`: a ladder
486
+ * can be complete and still unable to adapt.
487
+ */
488
+ rungs?: HlsRung[];
489
+ };
490
+ /**
491
+ * One rung of an adaptive HLS ladder.
492
+ *
493
+ * `segments` / `durationSec` are what make a ladder JUDGEABLE rather than
494
+ * merely present. Measured on a prod ladder of an 87 s 4K source: 240p cut at
495
+ * 14.35 s in 12 segments, 720p at 5.63 s in 12, 2160p at 5.88 s in 14. Cuts
496
+ * that do not line up cannot be swapped, and a swap is what a rendition switch
497
+ * IS — so that ladder looked complete and adapted badly. One segment means zero
498
+ * switch points: whichever rung the player opens on is the rung it finishes on.
499
+ *
500
+ * Both are optional because a rung whose playlist could not be read is recorded
501
+ * WITHOUT them rather than with a zero — unmeasured and none are different
502
+ * facts, and a `0` there would read as the latter.
503
+ */
504
+ type HlsRung = {
505
+ /** Rung directory / identity — `"720p"`. */
506
+ name: string;
507
+ /** `RESOLUTION` from the master playlist. */
508
+ width: number;
509
+ height: number;
510
+ /** `BANDWIDTH` in bits per second. */
511
+ bandwidth: number;
512
+ /** Absolute URL of this rung's media playlist. */
513
+ url: string;
514
+ /** `#EXTINF` count. */
515
+ segments?: number;
516
+ /** Sum of the `#EXTINF` values, seconds. */
517
+ durationSec?: number;
518
+ };
519
+ /**
520
+ * The ladder of an asset, or `null` when it has none / the DTO does not carry
521
+ * `variants` (the slim list shape never does — use `hasPreset(a, "hls")` there).
522
+ */
523
+ declare function getHlsLadder(asset: Pick<AssetDTO, "variants">): AssetVariant | null;
524
+ /**
525
+ * What the recorded rungs actually support — computed here, never stored, so
526
+ * one rule serves every consumer and a change to it does not need a backfill.
527
+ *
528
+ * - `switchable`: more than one rung AND more than one segment. False means the
529
+ * player is pinned to whatever rung it opens on for the entire clip.
530
+ * - `aligned`: every measured rung reports the same segment count. `null` means
531
+ * fewer than two rungs were measured — **unknown, not false.** Treating that
532
+ * as `false` rejects ladders nobody looked at.
533
+ * - `ceilingHeight`: the tallest rung. The ceiling of any composition using it.
534
+ */
535
+ declare function hlsLadderAlignment(rungs: HlsRung[]): {
536
+ rungCount: number;
537
+ ceilingHeight: number;
538
+ floorHeight: number;
539
+ switchable: boolean;
540
+ aligned: boolean | null;
541
+ minSegments: number | null;
457
542
  };
458
543
  /**
459
544
  * Compact wire shape — what the server actually sends. Aliases (`w`, `h`,
@@ -557,7 +642,7 @@ declare function getAssetUrl(asset: Pick<AssetDTO, "sha"> & OriginalHints, prese
557
642
  * Reads `dto.presets` — the compact 1-char code string the server sends on EVERY shape, including
558
643
  * the slim list/resolver one that carries no `variants` at all. That is why this exists and why it
559
644
  * stays the right existence check even now that `GET /assets/:id` really does send `variants`
560
- * (it did not until 2026-08-17; doc 240 §4.3).
645
+ * (it did not until the 2026-08-17 deploy).
561
646
  *
562
647
  * @example
563
648
  * ```ts
@@ -586,4 +671,4 @@ declare function getAssetDimensions(asset: Pick<AssetDTO, "w" | "h">): {
586
671
  height: number;
587
672
  } | null;
588
673
 
589
- export { type AssetDTO, type AssetPalette, type AssetVariant, PRESET_EXT, PRESET_LONG, PRESET_MAX_DIM, PRESET_SHORT, type PaletteSwatch, type ResolveSlotOptions, type SignedTransformOptions, type SlotDTO, type SlotResolution, TRANSFORM_WIDTHS, type TransformEffect, type TransformFit, type TransformFormat, type TransformGravity, type TransformOptions, type TransformWidth, type VariantPreset, bestTextContrast, computeVariantDimensions, configureSlotResolver, contrastRatio, extractAssetSha, getAmbientGradient, getAssetDimensions, getAssetSrcSet, getAssetUrl, getCdnBase, getHlsStreamingUrl, getPaletteBlurBackground, getPaletteCssVars, getSignedTransformUrl, getTenantId, getTextColorForBackground, getTransformSrcSet, getTransformUrl, getVideoTransformUrl, hasPreset, invalidateSlotCache, iteratePaletteSwatches, pickAmbientBackground, relativeLuminance, resolveSlot, resolveSlots, serializeTransform, setCdnBase, setTenantId, signTransformUrl };
674
+ export { type AssetDTO, type AssetPalette, type AssetVariant, type HlsRung, PRESET_EXT, PRESET_LONG, PRESET_MAX_DIM, PRESET_SHORT, type PaletteSwatch, type ResolveSlotOptions, type SignedTransformOptions, type SlotDTO, type SlotResolution, TRANSFORM_WIDTHS, type TransformEffect, type TransformFit, type TransformFormat, type TransformGravity, type TransformOptions, type TransformWidth, type VariantPreset, bestTextContrast, computeVariantDimensions, configureSlotResolver, contrastRatio, extractAssetSha, getAmbientGradient, getAssetDimensions, getAssetSrcSet, getAssetUrl, getCdnBase, getHlsLadder, getHlsStreamingUrl, getPaletteBlurBackground, getPaletteCssVars, getSignedTransformUrl, getTenantId, getTextColorForBackground, getTransformSrcSet, getTransformUrl, getVideoTransformUrl, hasPreset, hlsLadderAlignment, invalidateSlotCache, iteratePaletteSwatches, pickAmbientBackground, relativeLuminance, resolveSlot, resolveSlots, serializeTransform, setCdnBase, setTenantId, signTransformUrl };
package/dist/index.js CHANGED
@@ -336,6 +336,11 @@ var PRESET_SHORT = {
336
336
  poster: "p",
337
337
  video: "v",
338
338
  aiproxy: "a",
339
+ // `h` — free in both directions: no other preset claims it, and no
340
+ // multi-character token that `stripMultiCharTokens` removes donates one
341
+ // (`transform-<hex>` → t/r/a/n/s/f/o/m + a–f; `pr`; `mp3`). So a server that
342
+ // starts emitting `h` cannot make an OLDER client answer `true` to anything.
343
+ hls: "h",
339
344
  // 3 chars, NOT a 1-char alias: the server has no short-form for
340
345
  // audio so its `shortPreset("mp3")` falls through to the literal token,
341
346
  // and the deployed server already writes the `-mp3.mp3` variant + emits
@@ -356,6 +361,9 @@ var PRESET_EXT = {
356
361
  poster: "webp",
357
362
  video: "mp4",
358
363
  aiproxy: "mp4",
364
+ // The ladder's ENTRY file. Never used to build a key — see `getAssetUrl`,
365
+ // which refuses to derive `<sha>-h.m3u8` because no such object exists.
366
+ hls: "m3u8",
359
367
  mp3: "mp3"
360
368
  };
361
369
  var PRESET_MAX_DIM = {
@@ -368,10 +376,30 @@ var PRESET_MAX_DIM = {
368
376
  poster: null,
369
377
  video: null,
370
378
  aiproxy: null,
379
+ // A ladder has no single max side — it has a rung per size. `null` keeps it
380
+ // out of `getAssetSrcSet`, where offering an `.m3u8` as an `<img>` candidate
381
+ // would be nonsense. Its ceiling is `variants.find(v => v.preset === "hls").height`.
382
+ hls: null,
371
383
  // audio has no pixel dimensions; `null` keeps mp3 out of the
372
384
  // dimension-based `getAssetSrcSet` / `computeVariantDimensions` logic.
373
385
  mp3: null
374
386
  };
387
+ function getHlsLadder(asset) {
388
+ return asset.variants?.find((v) => v.preset === "hls") ?? null;
389
+ }
390
+ function hlsLadderAlignment(rungs) {
391
+ const heights = rungs.map((r) => r.height);
392
+ const measured = rungs.map((r) => r.segments).filter((s) => typeof s === "number" && s > 0);
393
+ const minSegments = measured.length > 0 ? Math.min(...measured) : null;
394
+ return {
395
+ rungCount: rungs.length,
396
+ ceilingHeight: heights.length > 0 ? Math.max(...heights) : 0,
397
+ floorHeight: heights.length > 0 ? Math.min(...heights) : 0,
398
+ switchable: rungs.length > 1 && (minSegments ?? 0) > 1,
399
+ aligned: measured.length >= 2 ? new Set(measured).size === 1 : null,
400
+ minSegments
401
+ };
402
+ }
375
403
  var cdnBaseUrl = "https://8ok.uk";
376
404
  function setCdnBase(url) {
377
405
  cdnBaseUrl = url.replace(/\/$/, "");
@@ -404,15 +432,13 @@ var ORIGINAL_EXT_BY_MIME = {
404
432
  "video/mp4": "mp4",
405
433
  "video/webm": "webm",
406
434
  "video/quicktime": "mov",
407
- // Audio — ausentes hasta 2026-08-17, y su ausencia costó un rodeo entero en
408
- // neo (`withRealOriginalExt`), que existe SÓLO porque esta tabla devolvía el
409
- // centinela `bin` para toda nota de voz. Medido en producción: `-o.bin` da
410
- // 404 y `-o.m4a` da 200.
435
+ // Audio — absent until 2026-08-17. While they were missing this table
436
+ // returned the `bin` sentinel for every voice note, so consumers had to
437
+ // hand-roll the extension themselves. Measured: `-o.bin` 404s, `-o.m4a` 200s.
411
438
  //
412
- // ⚠️ Se keyean por el mime COMPLETO, no por el subtipo: `audio/mp4` guarda
413
- // `.m4a` y `video/mp4` guarda `.mp4`. Un `switch` sobre el subtipo `mp4` no
414
- // puede distinguirloses el error que un consumidor cometió y tuvo que
415
- // corregir por su cuenta.
439
+ // ⚠️ These are keyed by the FULL mime, not the subtype: `audio/mp4` is stored
440
+ // as `.m4a` and `video/mp4` as `.mp4`. A `switch` on the `mp4` subtype cannot
441
+ // tell them apart a mistake worth not repeating.
416
442
  "audio/mpeg": "mpga",
417
443
  "audio/mp4": "m4a",
418
444
  "audio/x-m4a": "m4a",
@@ -431,6 +457,11 @@ function getAssetUrl(asset, preset) {
431
457
  const ext = asset.oext || originalExtForMime(asset.mime);
432
458
  return `${cdnBaseUrl}/${variantPrefix()}${asset.sha}-${PRESET_SHORT.original}.${ext}`;
433
459
  }
460
+ if (preset === "hls") {
461
+ const stored = asset.variants?.find((v) => v.preset === "hls")?.url;
462
+ if (stored) return stored;
463
+ return `${cdnBaseUrl}/t/format=hls/${asset.sha}.m3u8`;
464
+ }
434
465
  return `${cdnBaseUrl}/${variantPrefix()}${asset.sha}-${PRESET_SHORT[preset]}.${PRESET_EXT[preset]}`;
435
466
  }
436
467
  function hasPreset(asset, preset) {
@@ -477,6 +508,7 @@ export {
477
508
  getAssetSrcSet,
478
509
  getAssetUrl,
479
510
  getCdnBase,
511
+ getHlsLadder,
480
512
  getHlsStreamingUrl,
481
513
  getPaletteBlurBackground,
482
514
  getPaletteCssVars,
@@ -487,6 +519,7 @@ export {
487
519
  getTransformUrl,
488
520
  getVideoTransformUrl,
489
521
  hasPreset,
522
+ hlsLadderAlignment,
490
523
  invalidateSlotCache,
491
524
  iteratePaletteSwatches,
492
525
  pickAmbientBackground,