@nitida/asset-client 0.16.1 → 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: {
@@ -173,10 +173,9 @@ declare function resolveSlots(slotKeys: string[], opts?: ResolveSlotOptions): Pr
173
173
  * On-the-fly transform URL builder.
174
174
  *
175
175
  * Mirrors the server's DSL canonicalizer byte-for-byte so a URL generated
176
- * here hashes to the same R2 cache key as the server's canonical form.
176
+ * here hashes to the same cache key as the server's canonical form.
177
177
  *
178
- * Canonicalization rules (keep in sync with
179
- * `apps/asset-manager/src/features/assets/transform.dsl.ts`):
178
+ * Canonicalization rules (kept in sync with the server):
180
179
  * - Drop entries whose value is `undefined`
181
180
  * - Sort keys alphabetically
182
181
  * - Numbers rendered without leading zeros or trailing dots
@@ -191,7 +190,7 @@ declare function resolveSlots(slotKeys: string[], opts?: ResolveSlotOptions): Pr
191
190
  */
192
191
 
193
192
  /**
194
- * Widths the CDN edge whitelists (DoS guard — see `apps/cdn-proxy` WHITELIST_WIDTHS).
193
+ * Widths the CDN edge whitelists (DoS guard).
195
194
  * Requesting any OTHER width returns HTTP 400 at the edge (unsigned URLs). These are the
196
195
  * 1× base ladder values; DPR ×2/×3 multiples are applied + whitelisted server-side. Import
197
196
  * this instead of hardcoding magic widths so an unsupported size is caught in review/IDE.
@@ -221,9 +220,9 @@ type TransformOptions = {
221
220
  height?: number;
222
221
  /** Resize fit mode. Default `cover` server-side. */
223
222
  fit?: TransformFit;
224
- /** Crop gravity. `auto` uses sharp's `attention` strategy. */
223
+ /** Crop gravity. `auto` picks the region with the most visual salience. */
225
224
  gravity?: TransformGravity;
226
- /** Output format. `auto` → policy decides (see asset-manager bench). */
225
+ /** Output format. `auto` → the platform's policy decides. */
227
226
  format?: TransformFormat;
228
227
  /** Output quality. `auto` → format-specific default. */
229
228
  quality?: "auto" | number;
@@ -234,16 +233,15 @@ type TransformOptions = {
234
233
  *
235
234
  * - `removebg`: remove the background; output is a transparent PNG
236
235
  * of the foreground subject. Forces `format=png` regardless of
237
- * other format hints. Runs U²-Net ONNX locally (or BRIA via
238
- * Replicate when `BG_REMOVAL_BACKEND=replicate`). Single cache
239
- * miss per (sha, dsl) tuple; subsequent identical DSLs serve
240
- * from R2 — no inference, no per-image cost.
236
+ * other format hints. A single cache miss per (sha, dsl) tuple;
237
+ * subsequent identical DSLs serve from cache — no inference, no
238
+ * per-image cost.
241
239
  *
242
- * - `genfill`: aspect-extension outpaint via Flux-Fill Pro on
243
- * Replicate. Requires BOTH `width` and `height` — the server
240
+ * - `genfill`: aspect-extension outpaint. Requires BOTH `width`
241
+ * and `height` — the server
244
242
  * fits the source centered into the target canvas and outpaints
245
243
  * the gutters. Output is PNG (forced) at exactly target dims.
246
- * ~$0.05/image first time; same R2 cache as removebg after.
244
+ * ~$0.05/image first time; same cache as removebg after.
247
245
  * Primary use case: building OG cards (1200×630) from portrait
248
246
  * listing photos without awkward edge mirroring.
249
247
  */
@@ -264,7 +262,7 @@ type TransformOptions = {
264
262
  * the escape hatch for SIGNED URLs that need an off-ladder custom width.
265
263
  *
266
264
  * The edge whitelist only rejects off-ladder widths on UNSIGNED URLs; a valid
267
- * `?sig=` earns the whitelist bypass at the worker (the asset-manager still
265
+ * `?sig=` earns the whitelist bypass at the edge (the server still
268
266
  * does the real HMAC check). So a custom width is ONLY safe when the URL is
269
267
  * signed — hence this type is accepted exclusively by the signing helpers
270
268
  * ({@link getSignedTransformUrl} / `aq.transform(asset, opts, { sign: true })`),
@@ -305,8 +303,8 @@ declare function serializeTransform(opts: SignedTransformOptions): string;
305
303
  * Build a transform URL for a VIDEO asset. Same DSL shape as image
306
304
  * transforms; the server branches on the asset's `kind` column. Video
307
305
  * URLs use `.mp4` (default) or `.webm` extension and on cache miss the
308
- * server returns 202 Accepted while a Cloud Run Job encodes the clip;
309
- * subsequent GETs return 302 to the cached R2 object.
306
+ * server returns 202 Accepted while a background job encodes the clip;
307
+ * subsequent GETs return 302 to the cached object.
310
308
  *
311
309
  * <video src={aq.transformVideo(asset, { width: 1080, height: 1920 })}
312
310
  * autoPlay muted loop playsInline />
@@ -338,15 +336,30 @@ declare function getVideoTransformUrl(asset: Pick<AssetDTO, "sha">, opts: Transf
338
336
  * if (prefersNativeHls(video)) {
339
337
  * video.src = src;
340
338
  * } else {
341
- * // Defaults open at a fixed low rung measure instead of guessing.
342
- * const hls = new Hls({ startLevel: -1, testBandwidth: true, abrEwmaDefaultEstimate: 1_000_000 });
339
+ * const hls = new Hls({ capLevelToPlayerSize: false, abrEwmaDefaultEstimate: 5_000_000 });
343
340
  * hls.loadSource(src);
344
341
  * hls.attachMedia(video);
345
342
  * }
346
343
  * ```
347
344
  *
348
- * On first request the server returns 202 Accepted while a Cloud Run
349
- * Job transcodes the ladder (typically 1-3 min for a 90 s source);
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
+ *
361
+ * On first request the server returns 202 Accepted while a background
362
+ * job transcodes the ladder (typically 1-3 min for a 90 s source);
350
363
  * subsequent requests get 302 to the cached master.m3u8. Keep the
351
364
  * progressive MP4 as a fallback source for that window.
352
365
  *
@@ -418,8 +431,8 @@ declare function getTransformSrcSet(asset: Pick<AssetDTO, "sha">, widths: number
418
431
  * USED for, removing the implicit landscape-only assumption that bit us
419
432
  * with vertical product photos.
420
433
  */
421
- type VariantPreset = "thumb" | "sm" | "md" | "lg" | "xl" | "original" | "poster" | "video" | "aiproxy" | "mp3";
422
- /** 1-char alias used in R2 keys / wire `presets` string. */
434
+ type VariantPreset = "thumb" | "sm" | "md" | "lg" | "xl" | "original" | "poster" | "video" | "aiproxy" | "hls" | "mp3";
435
+ /** 1-char alias used in storage keys / wire `presets` string. */
423
436
  declare const PRESET_SHORT: Record<VariantPreset, string>;
424
437
  declare const PRESET_LONG: Record<string, VariantPreset>;
425
438
  /** Variant extension by preset. Image variants are always WebP, video MP4. */
@@ -435,11 +448,11 @@ type AssetVariant = {
435
448
  preset: VariantPreset;
436
449
  /** Public CDN URL of this variant. */
437
450
  url: string;
438
- /** Pixel width. Absent for `original`-only assets where sharp was skipped, or for video presets. */
451
+ /** Pixel width. Absent for `original`-only assets where image processing was skipped, or for video presets. */
439
452
  width?: number;
440
453
  /** Pixel height. Same caveat as `width`. */
441
454
  height?: number;
442
- /** Byte size of the variant file on R2. */
455
+ /** Byte size of the stored variant file. */
443
456
  bytes: number;
444
457
  /**
445
458
  * Where the bytes for this variant came from. Useful for quality
@@ -453,9 +466,79 @@ type AssetVariant = {
453
466
  *
454
467
  * Absent on variants written before the trace field existed.
455
468
  */
456
- sourceFrom?: VariantPreset | "upload";
469
+ sourceFrom?: VariantPreset | "upload" | "transform-route" | "hls-transcode" | "cdn-probe";
457
470
  /** ISO timestamp this variant was written. Absent on pre-trace variants. */
458
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;
459
542
  };
460
543
  /**
461
544
  * Compact wire shape — what the server actually sends. Aliases (`w`, `h`,
@@ -559,7 +642,7 @@ declare function getAssetUrl(asset: Pick<AssetDTO, "sha"> & OriginalHints, prese
559
642
  * Reads `dto.presets` — the compact 1-char code string the server sends on EVERY shape, including
560
643
  * the slim list/resolver one that carries no `variants` at all. That is why this exists and why it
561
644
  * stays the right existence check even now that `GET /assets/:id` really does send `variants`
562
- * (it did not until 2026-08-17; doc 240 §4.3).
645
+ * (it did not until the 2026-08-17 deploy).
563
646
  *
564
647
  * @example
565
648
  * ```ts
@@ -588,4 +671,4 @@ declare function getAssetDimensions(asset: Pick<AssetDTO, "w" | "h">): {
588
671
  height: number;
589
672
  } | null;
590
673
 
591
- 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: {
@@ -173,10 +173,9 @@ declare function resolveSlots(slotKeys: string[], opts?: ResolveSlotOptions): Pr
173
173
  * On-the-fly transform URL builder.
174
174
  *
175
175
  * Mirrors the server's DSL canonicalizer byte-for-byte so a URL generated
176
- * here hashes to the same R2 cache key as the server's canonical form.
176
+ * here hashes to the same cache key as the server's canonical form.
177
177
  *
178
- * Canonicalization rules (keep in sync with
179
- * `apps/asset-manager/src/features/assets/transform.dsl.ts`):
178
+ * Canonicalization rules (kept in sync with the server):
180
179
  * - Drop entries whose value is `undefined`
181
180
  * - Sort keys alphabetically
182
181
  * - Numbers rendered without leading zeros or trailing dots
@@ -191,7 +190,7 @@ declare function resolveSlots(slotKeys: string[], opts?: ResolveSlotOptions): Pr
191
190
  */
192
191
 
193
192
  /**
194
- * Widths the CDN edge whitelists (DoS guard — see `apps/cdn-proxy` WHITELIST_WIDTHS).
193
+ * Widths the CDN edge whitelists (DoS guard).
195
194
  * Requesting any OTHER width returns HTTP 400 at the edge (unsigned URLs). These are the
196
195
  * 1× base ladder values; DPR ×2/×3 multiples are applied + whitelisted server-side. Import
197
196
  * this instead of hardcoding magic widths so an unsupported size is caught in review/IDE.
@@ -221,9 +220,9 @@ type TransformOptions = {
221
220
  height?: number;
222
221
  /** Resize fit mode. Default `cover` server-side. */
223
222
  fit?: TransformFit;
224
- /** Crop gravity. `auto` uses sharp's `attention` strategy. */
223
+ /** Crop gravity. `auto` picks the region with the most visual salience. */
225
224
  gravity?: TransformGravity;
226
- /** Output format. `auto` → policy decides (see asset-manager bench). */
225
+ /** Output format. `auto` → the platform's policy decides. */
227
226
  format?: TransformFormat;
228
227
  /** Output quality. `auto` → format-specific default. */
229
228
  quality?: "auto" | number;
@@ -234,16 +233,15 @@ type TransformOptions = {
234
233
  *
235
234
  * - `removebg`: remove the background; output is a transparent PNG
236
235
  * of the foreground subject. Forces `format=png` regardless of
237
- * other format hints. Runs U²-Net ONNX locally (or BRIA via
238
- * Replicate when `BG_REMOVAL_BACKEND=replicate`). Single cache
239
- * miss per (sha, dsl) tuple; subsequent identical DSLs serve
240
- * from R2 — no inference, no per-image cost.
236
+ * other format hints. A single cache miss per (sha, dsl) tuple;
237
+ * subsequent identical DSLs serve from cache — no inference, no
238
+ * per-image cost.
241
239
  *
242
- * - `genfill`: aspect-extension outpaint via Flux-Fill Pro on
243
- * Replicate. Requires BOTH `width` and `height` — the server
240
+ * - `genfill`: aspect-extension outpaint. Requires BOTH `width`
241
+ * and `height` — the server
244
242
  * fits the source centered into the target canvas and outpaints
245
243
  * the gutters. Output is PNG (forced) at exactly target dims.
246
- * ~$0.05/image first time; same R2 cache as removebg after.
244
+ * ~$0.05/image first time; same cache as removebg after.
247
245
  * Primary use case: building OG cards (1200×630) from portrait
248
246
  * listing photos without awkward edge mirroring.
249
247
  */
@@ -264,7 +262,7 @@ type TransformOptions = {
264
262
  * the escape hatch for SIGNED URLs that need an off-ladder custom width.
265
263
  *
266
264
  * The edge whitelist only rejects off-ladder widths on UNSIGNED URLs; a valid
267
- * `?sig=` earns the whitelist bypass at the worker (the asset-manager still
265
+ * `?sig=` earns the whitelist bypass at the edge (the server still
268
266
  * does the real HMAC check). So a custom width is ONLY safe when the URL is
269
267
  * signed — hence this type is accepted exclusively by the signing helpers
270
268
  * ({@link getSignedTransformUrl} / `aq.transform(asset, opts, { sign: true })`),
@@ -305,8 +303,8 @@ declare function serializeTransform(opts: SignedTransformOptions): string;
305
303
  * Build a transform URL for a VIDEO asset. Same DSL shape as image
306
304
  * transforms; the server branches on the asset's `kind` column. Video
307
305
  * URLs use `.mp4` (default) or `.webm` extension and on cache miss the
308
- * server returns 202 Accepted while a Cloud Run Job encodes the clip;
309
- * subsequent GETs return 302 to the cached R2 object.
306
+ * server returns 202 Accepted while a background job encodes the clip;
307
+ * subsequent GETs return 302 to the cached object.
310
308
  *
311
309
  * <video src={aq.transformVideo(asset, { width: 1080, height: 1920 })}
312
310
  * autoPlay muted loop playsInline />
@@ -338,15 +336,30 @@ declare function getVideoTransformUrl(asset: Pick<AssetDTO, "sha">, opts: Transf
338
336
  * if (prefersNativeHls(video)) {
339
337
  * video.src = src;
340
338
  * } else {
341
- * // Defaults open at a fixed low rung measure instead of guessing.
342
- * const hls = new Hls({ startLevel: -1, testBandwidth: true, abrEwmaDefaultEstimate: 1_000_000 });
339
+ * const hls = new Hls({ capLevelToPlayerSize: false, abrEwmaDefaultEstimate: 5_000_000 });
343
340
  * hls.loadSource(src);
344
341
  * hls.attachMedia(video);
345
342
  * }
346
343
  * ```
347
344
  *
348
- * On first request the server returns 202 Accepted while a Cloud Run
349
- * Job transcodes the ladder (typically 1-3 min for a 90 s source);
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
+ *
361
+ * On first request the server returns 202 Accepted while a background
362
+ * job transcodes the ladder (typically 1-3 min for a 90 s source);
350
363
  * subsequent requests get 302 to the cached master.m3u8. Keep the
351
364
  * progressive MP4 as a fallback source for that window.
352
365
  *
@@ -418,8 +431,8 @@ declare function getTransformSrcSet(asset: Pick<AssetDTO, "sha">, widths: number
418
431
  * USED for, removing the implicit landscape-only assumption that bit us
419
432
  * with vertical product photos.
420
433
  */
421
- type VariantPreset = "thumb" | "sm" | "md" | "lg" | "xl" | "original" | "poster" | "video" | "aiproxy" | "mp3";
422
- /** 1-char alias used in R2 keys / wire `presets` string. */
434
+ type VariantPreset = "thumb" | "sm" | "md" | "lg" | "xl" | "original" | "poster" | "video" | "aiproxy" | "hls" | "mp3";
435
+ /** 1-char alias used in storage keys / wire `presets` string. */
423
436
  declare const PRESET_SHORT: Record<VariantPreset, string>;
424
437
  declare const PRESET_LONG: Record<string, VariantPreset>;
425
438
  /** Variant extension by preset. Image variants are always WebP, video MP4. */
@@ -435,11 +448,11 @@ type AssetVariant = {
435
448
  preset: VariantPreset;
436
449
  /** Public CDN URL of this variant. */
437
450
  url: string;
438
- /** Pixel width. Absent for `original`-only assets where sharp was skipped, or for video presets. */
451
+ /** Pixel width. Absent for `original`-only assets where image processing was skipped, or for video presets. */
439
452
  width?: number;
440
453
  /** Pixel height. Same caveat as `width`. */
441
454
  height?: number;
442
- /** Byte size of the variant file on R2. */
455
+ /** Byte size of the stored variant file. */
443
456
  bytes: number;
444
457
  /**
445
458
  * Where the bytes for this variant came from. Useful for quality
@@ -453,9 +466,79 @@ type AssetVariant = {
453
466
  *
454
467
  * Absent on variants written before the trace field existed.
455
468
  */
456
- sourceFrom?: VariantPreset | "upload";
469
+ sourceFrom?: VariantPreset | "upload" | "transform-route" | "hls-transcode" | "cdn-probe";
457
470
  /** ISO timestamp this variant was written. Absent on pre-trace variants. */
458
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;
459
542
  };
460
543
  /**
461
544
  * Compact wire shape — what the server actually sends. Aliases (`w`, `h`,
@@ -559,7 +642,7 @@ declare function getAssetUrl(asset: Pick<AssetDTO, "sha"> & OriginalHints, prese
559
642
  * Reads `dto.presets` — the compact 1-char code string the server sends on EVERY shape, including
560
643
  * the slim list/resolver one that carries no `variants` at all. That is why this exists and why it
561
644
  * stays the right existence check even now that `GET /assets/:id` really does send `variants`
562
- * (it did not until 2026-08-17; doc 240 §4.3).
645
+ * (it did not until the 2026-08-17 deploy).
563
646
  *
564
647
  * @example
565
648
  * ```ts
@@ -588,4 +671,4 @@ declare function getAssetDimensions(asset: Pick<AssetDTO, "w" | "h">): {
588
671
  height: number;
589
672
  } | null;
590
673
 
591
- 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 };