@nitida/asset-client 0.23.0 → 0.24.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/dist/index.d.cts CHANGED
@@ -304,43 +304,36 @@ type TransformOptions = {
304
304
  /** Output format. `auto` → the platform's policy decides. */
305
305
  format?: TransformFormat;
306
306
  /**
307
- * @deprecated **Do not set this.** Omit the key that is the correct call for
308
- * every surface this platform serves. Kept in the type for one narrow case
309
- * (a hard, MEASURED byte budget), and struck through on purpose so reaching
310
- * for it is a decision, not an accident.
307
+ * **`quality` no existe en este tipo, y eso es deliberado.**
311
308
  *
312
- * There are only two things you can pass, and neither is worth having:
309
+ * No es un dial del encoder: elige QUÉ BYTES decodifica el servidor. Con un
310
+ * número, `/t/` salta a la variante almacenada más chica que cubra el pedido
311
+ * — que ya pasó por una compresión — y la salida es una segunda generación.
312
+ * Sobre un asset CON escalera eso es pérdida pura (medido: +2…+8 % de peso y
313
+ * −0,50…−0,85 dB). Y `"auto"` era un no-op: byte a byte idéntico a omitir la
314
+ * clave, mismo sha256.
313
315
  *
314
- * **`"auto"` is a no-op.** Byte-for-byte identical to omitting the key
315
- * measured 2026-08-31 on a production laddered asset at `width=1920`:
316
- * both answered 136 680 B, sha256 `3a9ba57d…`, `x-transform-source:
317
- * original`. It buys nothing but the illusion of having chosen.
316
+ * El único uso legítimo —un presupuesto de bytes medido, sobre un asset SIN
317
+ * escalera— tiene su propia puerta, que no se puede llamar a ciegas:
318
+ * {@link getByteBudgetTransformUrl}.
318
319
  *
319
- * **A NUMBER costs you image quality.** It is not an encoder knob — it
320
- * selects WHICH BYTES the server decodes. With a number in hand, `/t/`
321
- * short-circuits to the smallest STORED variant that covers the request, and
322
- * that variant already went through one lossy pass, so the output is a second
323
- * generation. `"auto"` is a *string*, fails that `typeof` test, and therefore
324
- * keeps the master path. Measured on two corpora:
320
+ * ## Por qué no alcanzaba con documentarlo
325
321
  *
326
- * - 5 photographs, `width=800` 7–9 % smaller **and worse 5 of 5**, down
327
- * to −3.01 dB PSNR (`/guides/transform-benchmark/`).
328
- * - 5056 px architectural renders at 1280 / 1920 / 3840 — the widths that
329
- * MATCH `md`/`lg`/`xl` exactly, so no downscale hides the first pass
330
- * **+2…+8 % HEAVIER and −0.50…−0.85 dB, 3 of 3**. Not even a byte saving
331
- * to trade for it.
322
+ * Estuvo `@deprecated` con la medición al lado durante exactamente una
323
+ * versión, y eso ya era mejor que nada. Pero un aviso **avisa**; no impide.
324
+ * `getTransformUrl` recibe un `Pick<AssetDTO, "sha">` —un sha y nada más—,
325
+ * así que un `quality: 75` en el call site era **ciego**: nadie ahí, humano o
326
+ * modelo, podía saber si ese asset tenía escalera. La misma línea era
327
+ * correcta o dañina según un dato que no estaba en la llamada.
332
328
  *
333
- * Raising the number does not undo it: doubly-compressed at 80 is worse than
334
- * single-pass at 60, and 31 % heavier.
329
+ * Y hay un llamador que no lee tildados: **un modelo generando código.**
330
+ * `quality` es el parámetro que todo el mundo espera encontrar en una API de
331
+ * imágenes, así que se escribe solo. Next.js llegó a la misma conclusión por
332
+ * el mismo camino y dejó de recomendar `quality` por imagen: la configuración
333
+ * ya decide, y un valor por llamada sólo agrega formas de equivocarse.
335
334
  *
336
- * If you truly have a byte budget, pin it AND upload that asset with
337
- * `presets: ["original"]` — with no ladder there is nothing to short-circuit
338
- * to, and the pin becomes an honest encoder setting again.
339
- *
340
- * The one-line check is on the response: `x-transform-source` names the
341
- * variant the edge decoded. `original` = one pass; `md`/`lg`/`xl` = two.
335
+ * El parámetro no se documenta como peligroso. **No se puede escribir.**
342
336
  */
343
- quality?: "auto" | number;
344
337
  /** Device pixel ratio. Width/height are multiplied by this before resize. */
345
338
  dpr?: 1 | 2 | 3;
346
339
  /**
@@ -404,6 +397,15 @@ type SignedTransformOptions = TransformOptions;
404
397
  type PrivateTransformOptions = Omit<TransformOptions, "width"> & {
405
398
  width?: number;
406
399
  };
400
+ /**
401
+ * `TransformOptions` + el `quality` que el tipo público ya no admite.
402
+ *
403
+ * INTERNO. Existe porque el serializador tiene que poder emitir `quality=` para
404
+ * {@link getByteBudgetTransformUrl}; no porque un llamador deba construirlo.
405
+ */
406
+ type WithPinnedQuality<T> = T & {
407
+ quality: number;
408
+ };
407
409
  /** @deprecated internal alias kept for the builders below. */
408
410
  type AnyWidthTransformOptions = PrivateTransformOptions;
409
411
  /**
@@ -432,7 +434,7 @@ type AnyWidthTransformOptions = PrivateTransformOptions;
432
434
  * to be an aquienpz asset.
433
435
  */
434
436
  declare function extractAssetSha(url: string | null | undefined): string | null;
435
- declare function serializeTransform(opts: AnyWidthTransformOptions): string;
437
+ declare function serializeTransform(opts: AnyWidthTransformOptions | WithPinnedQuality<AnyWidthTransformOptions>): string;
436
438
  /**
437
439
  * Build a transform URL for a VIDEO asset. Same DSL shape as image
438
440
  * transforms; the server branches on the asset's `kind` column. Video
@@ -506,15 +508,64 @@ declare function getHlsStreamingUrl(asset: Pick<AssetDTO, "sha"> & VisibilityHin
506
508
 
507
509
  declare function getTransformUrl(asset: Pick<AssetDTO, "sha"> & VisibilityHint, opts: TransformOptions): string | null;
508
510
  /**
509
- * Build AND sign a transform URL.
511
+ * ¿Este asset tiene variantes de tamaño almacenadas, o sea algo a lo que `/t/`
512
+ * pueda saltar?
513
+ *
514
+ * `null` cuando **no se sabe** — un DTO sin `presets` no dice que no las tenga,
515
+ * dice que no lo trae. Tres estados, tres respuestas: un booleano acá mentiría
516
+ * en un tercio de los casos, y mentiría hacia el lado inseguro.
517
+ *
518
+ * ⚠️ **Se pregunta con `hasPreset`, JAMÁS con `presets.includes("s")`.** La
519
+ * cadena lleva tokens de varias letras —`transform-<hex>`, `upscale_…`, `pr`—
520
+ * que donan `s`, `m`, `l` y `o` de regalo. Ese es el falso positivo que
521
+ * `test/presets-false-positives.test.ts` existe para cazar.
522
+ */
523
+ declare function hasSizeLadder(asset: {
524
+ presets?: string | null;
525
+ }): boolean | null;
526
+ /**
527
+ * ⭐ **La ÚNICA puerta por la que se puede pinnear un `quality` numérico** —
528
+ * y está construida para que no se pueda usar mal.
529
+ *
530
+ * ## El problema que resuelve
531
+ *
532
+ * `getTransformUrl` recibe un `Pick<AssetDTO, "sha">`: **un sha y nada más.**
533
+ * Con eso, un `quality: 75` en el call site es CIEGO — la misma línea es un
534
+ * ajuste honesto del encoder sobre un asset sin escalera, y una segunda
535
+ * compresión silenciosa sobre uno con escalera. Nada en el tipo, en el nombre
536
+ * ni en el editor distinguía los dos casos. Un aviso avisa; esto impide.
537
+ *
538
+ * ## Las dos barreras
539
+ *
540
+ * 1. **En COMPILACIÓN**: el parámetro exige `presets: string`. Un
541
+ * `{ sha }` pelado —la llamada ciega— ya no compila. Para pasar por acá hay
542
+ * que tener el DTO en la mano, y tener el DTO es saber la respuesta.
543
+ * 2. **En EJECUCIÓN**: si el asset tiene cualquier preset de tamaño, **tira**.
544
+ * Si `presets` no vino, **tira** — «no sé» nunca se resuelve como «dale».
510
545
  *
511
- * A signature names the caller it is what a `strict_transforms` tenant
512
- * requires and what the paid-effect (`effect=genfill`) cost guard requires. It
513
- * does NOT widen the ladder; see {@link SignedTransformOptions}.
546
+ * ## Cuándo es legítimo, con el número
514
547
  *
515
- * Returns `null` only when `opts` serialize to an empty DSL (no transform
516
- * requested) same contract as {@link getTransformUrl}.
548
+ * Un presupuesto de bytes que alguien MIDIÓ, sobre un asset subido con
549
+ * `presets: ["original"]`. Ahí no hay a qué saltar y el pin hace lo que su
550
+ * nombre dice. Medido 2026-08-31 contra producción, ancho 1 920, las seis
551
+ * respuestas `x-transform-source: original`:
552
+ *
553
+ * | quality | bytes | vs auto | PSNR |
554
+ * |---|---|---|---|
555
+ * | 40 | 82 112 | **−33,6 %** | 35,32 dB |
556
+ * | 60 | 106 892 | −13,5 % | 36,85 dB |
557
+ * | *(auto)* | 123 614 | — | 37,65 dB |
558
+ * | 90 | 269 026 | +117,6 % | 41,46 dB |
559
+ *
560
+ * Monótona y sin sorpresas: −33,6 % de peso por −2,32 dB. **Eso sí es un
561
+ * intercambio**, y es la razón por la que el parámetro no se borró del todo.
562
+ *
563
+ * @throws si el asset tiene escalera, si no se sabe si la tiene, o si
564
+ * `quality` no está en 1..100.
517
565
  */
566
+ declare function getByteBudgetTransformUrl(asset: Pick<AssetDTO, "sha"> & {
567
+ presets: string;
568
+ } & VisibilityHint, opts: WithPinnedQuality<TransformOptions>): string | null;
518
569
  declare function getSignedTransformUrl(asset: Pick<AssetDTO, "sha"> & VisibilityHint, opts: SignedTransformOptions, signingKey: string, signOpts: SignTransformOptions): Promise<string> | null;
519
570
  /**
520
571
  * The longest life a signed transform URL may claim: **7 days**.
@@ -1136,4 +1187,4 @@ declare function getAssetDimensions(asset: Pick<AssetDTO, "w" | "h">): {
1136
1187
  height: number;
1137
1188
  } | null;
1138
1189
 
1139
- export { type AssetDTO, type AssetPalette, type AssetVariant, type HlsRung, MAX_SIGNED_TRANSFORM_TTL_SECONDS, MAX_SIGNED_URL_TTL_SECONDS, MIN_SIGNED_TRANSFORM_TTL_SECONDS, PRESET_EXT, PRESET_LONG, PRESET_MAX_DIM, PRESET_SHORT, type PaletteSwatch, type PrivateTransformOptions, REQUESTABLE_PRESETS, type RequestablePreset, type ResolveSlotOptions, type SignAccessOptions, type SignTransformOptions, type SignedTransformOptions, type SlotDTO, type SlotResolution, TRANSFORM_WIDTHS, type TransformEffect, type TransformFit, type TransformFormat, type TransformGravity, type TransformOptions, type TransformWidth, type VariantEntryPreset, type VariantPreset, type VisibilityHint, accessMessage, assertPublic, assertSha, bestTextContrast, computeVariantDimensions, configureSlotResolver, contrastRatio, deriveAccessKey, deriveTransformKid, extractAssetSha, getAmbientGradient, getAssetDimensions, getAssetSrcSet, getAssetUrl, getCdnBase, getHlsLadder, getHlsStreamingUrl, getPaletteBlurBackground, getPaletteCssVars, getPrivateAssetUrl, getPrivateTransformUrl, getSignedTransformUrl, getTenantId, getTextColorForBackground, getTransformSrcSet, getTransformUrl, getVideoTransformUrl, hasPreset, hlsLadderAlignment, invalidateSlotCache, isRequestablePreset, iteratePaletteSwatches, pickAmbientBackground, relativeLuminance, resolveSlot, resolveSlots, serializeTransform, setCdnBase, setTenantId, signAccessUrl, signTransformUrl, toRequestablePresets, transformMessage };
1190
+ export { type AssetDTO, type AssetPalette, type AssetVariant, type HlsRung, MAX_SIGNED_TRANSFORM_TTL_SECONDS, MAX_SIGNED_URL_TTL_SECONDS, MIN_SIGNED_TRANSFORM_TTL_SECONDS, PRESET_EXT, PRESET_LONG, PRESET_MAX_DIM, PRESET_SHORT, type PaletteSwatch, type PrivateTransformOptions, REQUESTABLE_PRESETS, type RequestablePreset, type ResolveSlotOptions, type SignAccessOptions, type SignTransformOptions, type SignedTransformOptions, type SlotDTO, type SlotResolution, TRANSFORM_WIDTHS, type TransformEffect, type TransformFit, type TransformFormat, type TransformGravity, type TransformOptions, type TransformWidth, type VariantEntryPreset, type VariantPreset, type VisibilityHint, accessMessage, assertPublic, assertSha, bestTextContrast, computeVariantDimensions, configureSlotResolver, contrastRatio, deriveAccessKey, deriveTransformKid, extractAssetSha, getAmbientGradient, getAssetDimensions, getAssetSrcSet, getAssetUrl, getByteBudgetTransformUrl, getCdnBase, getHlsLadder, getHlsStreamingUrl, getPaletteBlurBackground, getPaletteCssVars, getPrivateAssetUrl, getPrivateTransformUrl, getSignedTransformUrl, getTenantId, getTextColorForBackground, getTransformSrcSet, getTransformUrl, getVideoTransformUrl, hasPreset, hasSizeLadder, hlsLadderAlignment, invalidateSlotCache, isRequestablePreset, iteratePaletteSwatches, pickAmbientBackground, relativeLuminance, resolveSlot, resolveSlots, serializeTransform, setCdnBase, setTenantId, signAccessUrl, signTransformUrl, toRequestablePresets, transformMessage };
package/dist/index.d.ts CHANGED
@@ -304,43 +304,36 @@ type TransformOptions = {
304
304
  /** Output format. `auto` → the platform's policy decides. */
305
305
  format?: TransformFormat;
306
306
  /**
307
- * @deprecated **Do not set this.** Omit the key that is the correct call for
308
- * every surface this platform serves. Kept in the type for one narrow case
309
- * (a hard, MEASURED byte budget), and struck through on purpose so reaching
310
- * for it is a decision, not an accident.
307
+ * **`quality` no existe en este tipo, y eso es deliberado.**
311
308
  *
312
- * There are only two things you can pass, and neither is worth having:
309
+ * No es un dial del encoder: elige QUÉ BYTES decodifica el servidor. Con un
310
+ * número, `/t/` salta a la variante almacenada más chica que cubra el pedido
311
+ * — que ya pasó por una compresión — y la salida es una segunda generación.
312
+ * Sobre un asset CON escalera eso es pérdida pura (medido: +2…+8 % de peso y
313
+ * −0,50…−0,85 dB). Y `"auto"` era un no-op: byte a byte idéntico a omitir la
314
+ * clave, mismo sha256.
313
315
  *
314
- * **`"auto"` is a no-op.** Byte-for-byte identical to omitting the key
315
- * measured 2026-08-31 on a production laddered asset at `width=1920`:
316
- * both answered 136 680 B, sha256 `3a9ba57d…`, `x-transform-source:
317
- * original`. It buys nothing but the illusion of having chosen.
316
+ * El único uso legítimo —un presupuesto de bytes medido, sobre un asset SIN
317
+ * escalera— tiene su propia puerta, que no se puede llamar a ciegas:
318
+ * {@link getByteBudgetTransformUrl}.
318
319
  *
319
- * **A NUMBER costs you image quality.** It is not an encoder knob — it
320
- * selects WHICH BYTES the server decodes. With a number in hand, `/t/`
321
- * short-circuits to the smallest STORED variant that covers the request, and
322
- * that variant already went through one lossy pass, so the output is a second
323
- * generation. `"auto"` is a *string*, fails that `typeof` test, and therefore
324
- * keeps the master path. Measured on two corpora:
320
+ * ## Por qué no alcanzaba con documentarlo
325
321
  *
326
- * - 5 photographs, `width=800` 7–9 % smaller **and worse 5 of 5**, down
327
- * to −3.01 dB PSNR (`/guides/transform-benchmark/`).
328
- * - 5056 px architectural renders at 1280 / 1920 / 3840 — the widths that
329
- * MATCH `md`/`lg`/`xl` exactly, so no downscale hides the first pass
330
- * **+2…+8 % HEAVIER and −0.50…−0.85 dB, 3 of 3**. Not even a byte saving
331
- * to trade for it.
322
+ * Estuvo `@deprecated` con la medición al lado durante exactamente una
323
+ * versión, y eso ya era mejor que nada. Pero un aviso **avisa**; no impide.
324
+ * `getTransformUrl` recibe un `Pick<AssetDTO, "sha">` —un sha y nada más—,
325
+ * así que un `quality: 75` en el call site era **ciego**: nadie ahí, humano o
326
+ * modelo, podía saber si ese asset tenía escalera. La misma línea era
327
+ * correcta o dañina según un dato que no estaba en la llamada.
332
328
  *
333
- * Raising the number does not undo it: doubly-compressed at 80 is worse than
334
- * single-pass at 60, and 31 % heavier.
329
+ * Y hay un llamador que no lee tildados: **un modelo generando código.**
330
+ * `quality` es el parámetro que todo el mundo espera encontrar en una API de
331
+ * imágenes, así que se escribe solo. Next.js llegó a la misma conclusión por
332
+ * el mismo camino y dejó de recomendar `quality` por imagen: la configuración
333
+ * ya decide, y un valor por llamada sólo agrega formas de equivocarse.
335
334
  *
336
- * If you truly have a byte budget, pin it AND upload that asset with
337
- * `presets: ["original"]` — with no ladder there is nothing to short-circuit
338
- * to, and the pin becomes an honest encoder setting again.
339
- *
340
- * The one-line check is on the response: `x-transform-source` names the
341
- * variant the edge decoded. `original` = one pass; `md`/`lg`/`xl` = two.
335
+ * El parámetro no se documenta como peligroso. **No se puede escribir.**
342
336
  */
343
- quality?: "auto" | number;
344
337
  /** Device pixel ratio. Width/height are multiplied by this before resize. */
345
338
  dpr?: 1 | 2 | 3;
346
339
  /**
@@ -404,6 +397,15 @@ type SignedTransformOptions = TransformOptions;
404
397
  type PrivateTransformOptions = Omit<TransformOptions, "width"> & {
405
398
  width?: number;
406
399
  };
400
+ /**
401
+ * `TransformOptions` + el `quality` que el tipo público ya no admite.
402
+ *
403
+ * INTERNO. Existe porque el serializador tiene que poder emitir `quality=` para
404
+ * {@link getByteBudgetTransformUrl}; no porque un llamador deba construirlo.
405
+ */
406
+ type WithPinnedQuality<T> = T & {
407
+ quality: number;
408
+ };
407
409
  /** @deprecated internal alias kept for the builders below. */
408
410
  type AnyWidthTransformOptions = PrivateTransformOptions;
409
411
  /**
@@ -432,7 +434,7 @@ type AnyWidthTransformOptions = PrivateTransformOptions;
432
434
  * to be an aquienpz asset.
433
435
  */
434
436
  declare function extractAssetSha(url: string | null | undefined): string | null;
435
- declare function serializeTransform(opts: AnyWidthTransformOptions): string;
437
+ declare function serializeTransform(opts: AnyWidthTransformOptions | WithPinnedQuality<AnyWidthTransformOptions>): string;
436
438
  /**
437
439
  * Build a transform URL for a VIDEO asset. Same DSL shape as image
438
440
  * transforms; the server branches on the asset's `kind` column. Video
@@ -506,15 +508,64 @@ declare function getHlsStreamingUrl(asset: Pick<AssetDTO, "sha"> & VisibilityHin
506
508
 
507
509
  declare function getTransformUrl(asset: Pick<AssetDTO, "sha"> & VisibilityHint, opts: TransformOptions): string | null;
508
510
  /**
509
- * Build AND sign a transform URL.
511
+ * ¿Este asset tiene variantes de tamaño almacenadas, o sea algo a lo que `/t/`
512
+ * pueda saltar?
513
+ *
514
+ * `null` cuando **no se sabe** — un DTO sin `presets` no dice que no las tenga,
515
+ * dice que no lo trae. Tres estados, tres respuestas: un booleano acá mentiría
516
+ * en un tercio de los casos, y mentiría hacia el lado inseguro.
517
+ *
518
+ * ⚠️ **Se pregunta con `hasPreset`, JAMÁS con `presets.includes("s")`.** La
519
+ * cadena lleva tokens de varias letras —`transform-<hex>`, `upscale_…`, `pr`—
520
+ * que donan `s`, `m`, `l` y `o` de regalo. Ese es el falso positivo que
521
+ * `test/presets-false-positives.test.ts` existe para cazar.
522
+ */
523
+ declare function hasSizeLadder(asset: {
524
+ presets?: string | null;
525
+ }): boolean | null;
526
+ /**
527
+ * ⭐ **La ÚNICA puerta por la que se puede pinnear un `quality` numérico** —
528
+ * y está construida para que no se pueda usar mal.
529
+ *
530
+ * ## El problema que resuelve
531
+ *
532
+ * `getTransformUrl` recibe un `Pick<AssetDTO, "sha">`: **un sha y nada más.**
533
+ * Con eso, un `quality: 75` en el call site es CIEGO — la misma línea es un
534
+ * ajuste honesto del encoder sobre un asset sin escalera, y una segunda
535
+ * compresión silenciosa sobre uno con escalera. Nada en el tipo, en el nombre
536
+ * ni en el editor distinguía los dos casos. Un aviso avisa; esto impide.
537
+ *
538
+ * ## Las dos barreras
539
+ *
540
+ * 1. **En COMPILACIÓN**: el parámetro exige `presets: string`. Un
541
+ * `{ sha }` pelado —la llamada ciega— ya no compila. Para pasar por acá hay
542
+ * que tener el DTO en la mano, y tener el DTO es saber la respuesta.
543
+ * 2. **En EJECUCIÓN**: si el asset tiene cualquier preset de tamaño, **tira**.
544
+ * Si `presets` no vino, **tira** — «no sé» nunca se resuelve como «dale».
510
545
  *
511
- * A signature names the caller it is what a `strict_transforms` tenant
512
- * requires and what the paid-effect (`effect=genfill`) cost guard requires. It
513
- * does NOT widen the ladder; see {@link SignedTransformOptions}.
546
+ * ## Cuándo es legítimo, con el número
514
547
  *
515
- * Returns `null` only when `opts` serialize to an empty DSL (no transform
516
- * requested) same contract as {@link getTransformUrl}.
548
+ * Un presupuesto de bytes que alguien MIDIÓ, sobre un asset subido con
549
+ * `presets: ["original"]`. Ahí no hay a qué saltar y el pin hace lo que su
550
+ * nombre dice. Medido 2026-08-31 contra producción, ancho 1 920, las seis
551
+ * respuestas `x-transform-source: original`:
552
+ *
553
+ * | quality | bytes | vs auto | PSNR |
554
+ * |---|---|---|---|
555
+ * | 40 | 82 112 | **−33,6 %** | 35,32 dB |
556
+ * | 60 | 106 892 | −13,5 % | 36,85 dB |
557
+ * | *(auto)* | 123 614 | — | 37,65 dB |
558
+ * | 90 | 269 026 | +117,6 % | 41,46 dB |
559
+ *
560
+ * Monótona y sin sorpresas: −33,6 % de peso por −2,32 dB. **Eso sí es un
561
+ * intercambio**, y es la razón por la que el parámetro no se borró del todo.
562
+ *
563
+ * @throws si el asset tiene escalera, si no se sabe si la tiene, o si
564
+ * `quality` no está en 1..100.
517
565
  */
566
+ declare function getByteBudgetTransformUrl(asset: Pick<AssetDTO, "sha"> & {
567
+ presets: string;
568
+ } & VisibilityHint, opts: WithPinnedQuality<TransformOptions>): string | null;
518
569
  declare function getSignedTransformUrl(asset: Pick<AssetDTO, "sha"> & VisibilityHint, opts: SignedTransformOptions, signingKey: string, signOpts: SignTransformOptions): Promise<string> | null;
519
570
  /**
520
571
  * The longest life a signed transform URL may claim: **7 days**.
@@ -1136,4 +1187,4 @@ declare function getAssetDimensions(asset: Pick<AssetDTO, "w" | "h">): {
1136
1187
  height: number;
1137
1188
  } | null;
1138
1189
 
1139
- export { type AssetDTO, type AssetPalette, type AssetVariant, type HlsRung, MAX_SIGNED_TRANSFORM_TTL_SECONDS, MAX_SIGNED_URL_TTL_SECONDS, MIN_SIGNED_TRANSFORM_TTL_SECONDS, PRESET_EXT, PRESET_LONG, PRESET_MAX_DIM, PRESET_SHORT, type PaletteSwatch, type PrivateTransformOptions, REQUESTABLE_PRESETS, type RequestablePreset, type ResolveSlotOptions, type SignAccessOptions, type SignTransformOptions, type SignedTransformOptions, type SlotDTO, type SlotResolution, TRANSFORM_WIDTHS, type TransformEffect, type TransformFit, type TransformFormat, type TransformGravity, type TransformOptions, type TransformWidth, type VariantEntryPreset, type VariantPreset, type VisibilityHint, accessMessage, assertPublic, assertSha, bestTextContrast, computeVariantDimensions, configureSlotResolver, contrastRatio, deriveAccessKey, deriveTransformKid, extractAssetSha, getAmbientGradient, getAssetDimensions, getAssetSrcSet, getAssetUrl, getCdnBase, getHlsLadder, getHlsStreamingUrl, getPaletteBlurBackground, getPaletteCssVars, getPrivateAssetUrl, getPrivateTransformUrl, getSignedTransformUrl, getTenantId, getTextColorForBackground, getTransformSrcSet, getTransformUrl, getVideoTransformUrl, hasPreset, hlsLadderAlignment, invalidateSlotCache, isRequestablePreset, iteratePaletteSwatches, pickAmbientBackground, relativeLuminance, resolveSlot, resolveSlots, serializeTransform, setCdnBase, setTenantId, signAccessUrl, signTransformUrl, toRequestablePresets, transformMessage };
1190
+ export { type AssetDTO, type AssetPalette, type AssetVariant, type HlsRung, MAX_SIGNED_TRANSFORM_TTL_SECONDS, MAX_SIGNED_URL_TTL_SECONDS, MIN_SIGNED_TRANSFORM_TTL_SECONDS, PRESET_EXT, PRESET_LONG, PRESET_MAX_DIM, PRESET_SHORT, type PaletteSwatch, type PrivateTransformOptions, REQUESTABLE_PRESETS, type RequestablePreset, type ResolveSlotOptions, type SignAccessOptions, type SignTransformOptions, type SignedTransformOptions, type SlotDTO, type SlotResolution, TRANSFORM_WIDTHS, type TransformEffect, type TransformFit, type TransformFormat, type TransformGravity, type TransformOptions, type TransformWidth, type VariantEntryPreset, type VariantPreset, type VisibilityHint, accessMessage, assertPublic, assertSha, bestTextContrast, computeVariantDimensions, configureSlotResolver, contrastRatio, deriveAccessKey, deriveTransformKid, extractAssetSha, getAmbientGradient, getAssetDimensions, getAssetSrcSet, getAssetUrl, getByteBudgetTransformUrl, getCdnBase, getHlsLadder, getHlsStreamingUrl, getPaletteBlurBackground, getPaletteCssVars, getPrivateAssetUrl, getPrivateTransformUrl, getSignedTransformUrl, getTenantId, getTextColorForBackground, getTransformSrcSet, getTransformUrl, getVideoTransformUrl, hasPreset, hasSizeLadder, hlsLadderAlignment, invalidateSlotCache, isRequestablePreset, iteratePaletteSwatches, pickAmbientBackground, relativeLuminance, resolveSlot, resolveSlots, serializeTransform, setCdnBase, setTenantId, signAccessUrl, signTransformUrl, toRequestablePresets, transformMessage };
package/dist/index.js CHANGED
@@ -296,6 +296,37 @@ function getTransformUrl(asset, opts) {
296
296
  );
297
297
  return buildTransformUrl(asset, opts);
298
298
  }
299
+ var LADDER_PRESETS = ["thumb", "sm", "md", "lg", "xl"];
300
+ function hasSizeLadder(asset) {
301
+ const raw = asset.presets;
302
+ if (raw == null || raw.trim() === "") return null;
303
+ return LADDER_PRESETS.some((preset) => hasPreset(asset, preset));
304
+ }
305
+ function getByteBudgetTransformUrl(asset, opts) {
306
+ assertSha(asset, "getByteBudgetTransformUrl");
307
+ assertPublic(
308
+ asset,
309
+ "getByteBudgetTransformUrl",
310
+ "getPrivateTransformUrl(asset, opts, signingKey, { expiresInSeconds: 300 })"
311
+ );
312
+ if (!Number.isInteger(opts.quality) || opts.quality < 1 || opts.quality > 100) {
313
+ throw new Error(
314
+ `getByteBudgetTransformUrl: quality must be an integer 1..100, got ${String(opts.quality)}. If you do not have a measured byte budget, use getTransformUrl and omit quality entirely.`
315
+ );
316
+ }
317
+ const ladder = hasSizeLadder(asset);
318
+ if (ladder === null) {
319
+ throw new Error(
320
+ `getByteBudgetTransformUrl: asset ${asset.sha} carries no 'presets', so whether a pinned quality would re-compress it is UNKNOWN \u2014 and unknown is not permission. Fetch the full DTO (assets.get / assets.byHash) and pass it, or use getTransformUrl with no quality.`
321
+ );
322
+ }
323
+ if (ladder) {
324
+ throw new Error(
325
+ `getByteBudgetTransformUrl: asset ${asset.sha} has stored size variants (presets="${asset.presets}"). A pinned quality there does not set the encoder \u2014 it makes /t/ decode one of those already-compressed variants, so the output is a SECOND lossy generation: measured +2..+8% HEAVIER and -0.50..-0.85 dB. Use getTransformUrl with no quality (one pass from the master), or upload this asset with presets: ["original"] if the byte budget is real.`
326
+ );
327
+ }
328
+ return buildTransformUrl(asset, opts);
329
+ }
299
330
  function getSignedTransformUrl(asset, opts, signingKey, signOpts) {
300
331
  assertSha(asset, "getSignedTransformUrl");
301
332
  assertPublic(
@@ -749,6 +780,7 @@ export {
749
780
  getAssetDimensions,
750
781
  getAssetSrcSet,
751
782
  getAssetUrl,
783
+ getByteBudgetTransformUrl,
752
784
  getCdnBase,
753
785
  getHlsLadder,
754
786
  getHlsStreamingUrl,
@@ -763,6 +795,7 @@ export {
763
795
  getTransformUrl,
764
796
  getVideoTransformUrl,
765
797
  hasPreset,
798
+ hasSizeLadder,
766
799
  hlsLadderAlignment,
767
800
  invalidateSlotCache,
768
801
  isRequestablePreset,