@nitida/asset-client 0.23.0 → 0.24.1
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/README.md +5 -4
- package/dist/index.cjs +65 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +101 -38
- package/dist/index.d.ts +101 -38
- package/dist/index.js +63 -14
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +2 -0
- package/src/slots.ts +56 -13
- package/src/transform.ts +165 -38
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
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
|
-
*
|
|
334
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
510
531
|
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
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.
|
|
514
537
|
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
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».
|
|
545
|
+
*
|
|
546
|
+
* ## Cuándo es legítimo, con el número
|
|
547
|
+
*
|
|
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**.
|
|
@@ -638,6 +689,12 @@ type SlotResolution = {
|
|
|
638
689
|
/** The CDN URL the consumer should use. */
|
|
639
690
|
url: string | null;
|
|
640
691
|
};
|
|
692
|
+
/** Per-call resolver scope. When passed, it fully overrides the process globals. */
|
|
693
|
+
type SlotResolverConfig = {
|
|
694
|
+
endpoint?: string;
|
|
695
|
+
apiKey?: string | null;
|
|
696
|
+
tenantCode?: string | null;
|
|
697
|
+
};
|
|
641
698
|
/**
|
|
642
699
|
* Configure the resolver process-wide. Call once at boot from your
|
|
643
700
|
* storefront layout / server entry / worker init.
|
|
@@ -660,6 +717,12 @@ type ResolveSlotOptions = {
|
|
|
660
717
|
preset?: VariantPreset;
|
|
661
718
|
/** TTL for the in-process cache. Default 60s. Set 0 to bypass. */
|
|
662
719
|
ttlMs?: number;
|
|
720
|
+
/**
|
|
721
|
+
* Per-client auth/endpoint scope. Omit to use the process globals
|
|
722
|
+
* (configureSlotResolver). A NitidaClient passes its own here so its
|
|
723
|
+
* resolve() never uses another client's key (audit N15).
|
|
724
|
+
*/
|
|
725
|
+
config?: SlotResolverConfig;
|
|
663
726
|
};
|
|
664
727
|
/**
|
|
665
728
|
* Resolve a single slot to a CDN URL. Returns `{slot: null, url: null}`
|
|
@@ -1136,4 +1199,4 @@ declare function getAssetDimensions(asset: Pick<AssetDTO, "w" | "h">): {
|
|
|
1136
1199
|
height: number;
|
|
1137
1200
|
} | null;
|
|
1138
1201
|
|
|
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 };
|
|
1202
|
+
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
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
|
-
*
|
|
334
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
510
531
|
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
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.
|
|
514
537
|
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
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».
|
|
545
|
+
*
|
|
546
|
+
* ## Cuándo es legítimo, con el número
|
|
547
|
+
*
|
|
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**.
|
|
@@ -638,6 +689,12 @@ type SlotResolution = {
|
|
|
638
689
|
/** The CDN URL the consumer should use. */
|
|
639
690
|
url: string | null;
|
|
640
691
|
};
|
|
692
|
+
/** Per-call resolver scope. When passed, it fully overrides the process globals. */
|
|
693
|
+
type SlotResolverConfig = {
|
|
694
|
+
endpoint?: string;
|
|
695
|
+
apiKey?: string | null;
|
|
696
|
+
tenantCode?: string | null;
|
|
697
|
+
};
|
|
641
698
|
/**
|
|
642
699
|
* Configure the resolver process-wide. Call once at boot from your
|
|
643
700
|
* storefront layout / server entry / worker init.
|
|
@@ -660,6 +717,12 @@ type ResolveSlotOptions = {
|
|
|
660
717
|
preset?: VariantPreset;
|
|
661
718
|
/** TTL for the in-process cache. Default 60s. Set 0 to bypass. */
|
|
662
719
|
ttlMs?: number;
|
|
720
|
+
/**
|
|
721
|
+
* Per-client auth/endpoint scope. Omit to use the process globals
|
|
722
|
+
* (configureSlotResolver). A NitidaClient passes its own here so its
|
|
723
|
+
* resolve() never uses another client's key (audit N15).
|
|
724
|
+
*/
|
|
725
|
+
config?: SlotResolverConfig;
|
|
663
726
|
};
|
|
664
727
|
/**
|
|
665
728
|
* Resolve a single slot to a CDN URL. Returns `{slot: null, url: null}`
|
|
@@ -1136,4 +1199,4 @@ declare function getAssetDimensions(asset: Pick<AssetDTO, "w" | "h">): {
|
|
|
1136
1199
|
height: number;
|
|
1137
1200
|
} | null;
|
|
1138
1201
|
|
|
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 };
|
|
1202
|
+
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
|
@@ -219,10 +219,16 @@ function extractAssetSha(url) {
|
|
|
219
219
|
function serializeTransform(opts) {
|
|
220
220
|
const entries = [];
|
|
221
221
|
const keys = Object.keys(opts).sort();
|
|
222
|
+
const VALUE_RE = /^[a-z0-9._-]+$/;
|
|
222
223
|
for (const k of keys) {
|
|
223
224
|
const v = opts[k];
|
|
224
225
|
if (v == null) continue;
|
|
225
226
|
const serialized = typeof v === "string" ? v.toLowerCase() : String(v);
|
|
227
|
+
if (!VALUE_RE.test(serialized)) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`invalid transform value for "${k}": ${JSON.stringify(serialized)} (only [a-z0-9._-] allowed)`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
226
232
|
entries.push([k, serialized]);
|
|
227
233
|
}
|
|
228
234
|
return entries.map(([k, v]) => `${k}=${v}`).join(",");
|
|
@@ -296,6 +302,37 @@ function getTransformUrl(asset, opts) {
|
|
|
296
302
|
);
|
|
297
303
|
return buildTransformUrl(asset, opts);
|
|
298
304
|
}
|
|
305
|
+
var LADDER_PRESETS = ["thumb", "sm", "md", "lg", "xl"];
|
|
306
|
+
function hasSizeLadder(asset) {
|
|
307
|
+
const raw = asset.presets;
|
|
308
|
+
if (raw == null || raw.trim() === "") return null;
|
|
309
|
+
return LADDER_PRESETS.some((preset) => hasPreset(asset, preset));
|
|
310
|
+
}
|
|
311
|
+
function getByteBudgetTransformUrl(asset, opts) {
|
|
312
|
+
assertSha(asset, "getByteBudgetTransformUrl");
|
|
313
|
+
assertPublic(
|
|
314
|
+
asset,
|
|
315
|
+
"getByteBudgetTransformUrl",
|
|
316
|
+
"getPrivateTransformUrl(asset, opts, signingKey, { expiresInSeconds: 300 })"
|
|
317
|
+
);
|
|
318
|
+
if (!Number.isInteger(opts.quality) || opts.quality < 1 || opts.quality > 100) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
`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.`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
const ladder = hasSizeLadder(asset);
|
|
324
|
+
if (ladder === null) {
|
|
325
|
+
throw new Error(
|
|
326
|
+
`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.`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
if (ladder) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`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.`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
return buildTransformUrl(asset, opts);
|
|
335
|
+
}
|
|
299
336
|
function getSignedTransformUrl(asset, opts, signingKey, signOpts) {
|
|
300
337
|
assertSha(asset, "getSignedTransformUrl");
|
|
301
338
|
assertPublic(
|
|
@@ -387,6 +424,14 @@ var cache = /* @__PURE__ */ new Map();
|
|
|
387
424
|
var endpoint = "https://api.nitida.gofuture.space";
|
|
388
425
|
var apiKey = null;
|
|
389
426
|
var tenantCode = null;
|
|
427
|
+
function effectiveConfig(cfg) {
|
|
428
|
+
if (!cfg) return { endpoint, apiKey, tenantCode };
|
|
429
|
+
return {
|
|
430
|
+
endpoint: cfg.endpoint ? cfg.endpoint.replace(/\/+$/, "") : endpoint,
|
|
431
|
+
apiKey: cfg.apiKey ?? null,
|
|
432
|
+
tenantCode: cfg.tenantCode ?? null
|
|
433
|
+
};
|
|
434
|
+
}
|
|
390
435
|
function configureSlotResolver(opts) {
|
|
391
436
|
if (opts.endpoint) endpoint = opts.endpoint.replace(/\/+$/, "");
|
|
392
437
|
if (opts.apiKey !== void 0) apiKey = opts.apiKey;
|
|
@@ -398,25 +443,25 @@ function invalidateSlotCache(slotKey) {
|
|
|
398
443
|
for (const k of cache.keys())
|
|
399
444
|
if (k.endsWith(`:${slotKey}`)) cache.delete(k);
|
|
400
445
|
}
|
|
401
|
-
var baseHeaders = () => {
|
|
446
|
+
var baseHeaders = (cfg) => {
|
|
402
447
|
const h = {};
|
|
403
|
-
if (apiKey) h.Authorization = `Bearer ${apiKey}`;
|
|
404
|
-
if (tenantCode) h["X-Tenant-Code"] = tenantCode;
|
|
448
|
+
if (cfg.apiKey) h.Authorization = `Bearer ${cfg.apiKey}`;
|
|
449
|
+
if (cfg.tenantCode) h["X-Tenant-Code"] = cfg.tenantCode;
|
|
405
450
|
return h;
|
|
406
451
|
};
|
|
407
|
-
async function fetchSlot(slotKey) {
|
|
408
|
-
const r = await fetch(`${endpoint}/slots/${encodeURIComponent(slotKey)}`, {
|
|
409
|
-
headers: baseHeaders()
|
|
452
|
+
async function fetchSlot(slotKey, cfg) {
|
|
453
|
+
const r = await fetch(`${cfg.endpoint}/slots/${encodeURIComponent(slotKey)}`, {
|
|
454
|
+
headers: baseHeaders(cfg)
|
|
410
455
|
});
|
|
411
456
|
if (r.status === 404) return null;
|
|
412
457
|
if (!r.ok) throw new Error(`slot fetch ${r.status}: ${await r.text()}`);
|
|
413
458
|
return await r.json();
|
|
414
459
|
}
|
|
415
|
-
async function fetchSlotsBulk(slotKeys) {
|
|
460
|
+
async function fetchSlotsBulk(slotKeys, cfg) {
|
|
416
461
|
if (slotKeys.length === 0) return {};
|
|
417
|
-
const r = await fetch(`${endpoint}/slots/resolve`, {
|
|
462
|
+
const r = await fetch(`${cfg.endpoint}/slots/resolve`, {
|
|
418
463
|
method: "POST",
|
|
419
|
-
headers: { ...baseHeaders(), "Content-Type": "application/json" },
|
|
464
|
+
headers: { ...baseHeaders(cfg), "Content-Type": "application/json" },
|
|
420
465
|
body: JSON.stringify({ keys: slotKeys })
|
|
421
466
|
});
|
|
422
467
|
if (!r.ok) throw new Error(`slots resolve ${r.status}: ${await r.text()}`);
|
|
@@ -425,14 +470,15 @@ async function fetchSlotsBulk(slotKeys) {
|
|
|
425
470
|
}
|
|
426
471
|
async function resolveSlot(slotKey, opts = {}) {
|
|
427
472
|
const ttl = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
428
|
-
const
|
|
473
|
+
const cfg = effectiveConfig(opts.config);
|
|
474
|
+
const cacheKey = `${cfg.tenantCode ?? "_"}:${slotKey}`;
|
|
429
475
|
const now = Date.now();
|
|
430
476
|
let dto;
|
|
431
477
|
const hit = cache.get(cacheKey);
|
|
432
478
|
if (hit && now - hit.fetchedAt < ttl) {
|
|
433
479
|
dto = hit.value;
|
|
434
480
|
} else {
|
|
435
|
-
dto = await fetchSlot(slotKey);
|
|
481
|
+
dto = await fetchSlot(slotKey, cfg);
|
|
436
482
|
cache.set(cacheKey, { fetchedAt: now, value: dto });
|
|
437
483
|
}
|
|
438
484
|
return materializeResolution(dto, opts.preset);
|
|
@@ -440,11 +486,12 @@ async function resolveSlot(slotKey, opts = {}) {
|
|
|
440
486
|
async function resolveSlots(slotKeys, opts = {}) {
|
|
441
487
|
if (slotKeys.length === 0) return {};
|
|
442
488
|
const ttl = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
489
|
+
const cfg = effectiveConfig(opts.config);
|
|
443
490
|
const now = Date.now();
|
|
444
491
|
const missing = [];
|
|
445
492
|
const out = {};
|
|
446
493
|
for (const k of slotKeys) {
|
|
447
|
-
const cacheKey = `${tenantCode ?? "_"}:${k}`;
|
|
494
|
+
const cacheKey = `${cfg.tenantCode ?? "_"}:${k}`;
|
|
448
495
|
const hit = cache.get(cacheKey);
|
|
449
496
|
if (hit && now - hit.fetchedAt < ttl) {
|
|
450
497
|
out[k] = materializeResolution(hit.value, opts.preset);
|
|
@@ -453,10 +500,10 @@ async function resolveSlots(slotKeys, opts = {}) {
|
|
|
453
500
|
}
|
|
454
501
|
}
|
|
455
502
|
if (missing.length > 0) {
|
|
456
|
-
const resolved = await fetchSlotsBulk(missing);
|
|
503
|
+
const resolved = await fetchSlotsBulk(missing, cfg);
|
|
457
504
|
for (const k of missing) {
|
|
458
505
|
const dto = resolved[k] ?? null;
|
|
459
|
-
cache.set(`${tenantCode ?? "_"}:${k}`, { fetchedAt: now, value: dto });
|
|
506
|
+
cache.set(`${cfg.tenantCode ?? "_"}:${k}`, { fetchedAt: now, value: dto });
|
|
460
507
|
out[k] = materializeResolution(dto, opts.preset);
|
|
461
508
|
}
|
|
462
509
|
}
|
|
@@ -749,6 +796,7 @@ export {
|
|
|
749
796
|
getAssetDimensions,
|
|
750
797
|
getAssetSrcSet,
|
|
751
798
|
getAssetUrl,
|
|
799
|
+
getByteBudgetTransformUrl,
|
|
752
800
|
getCdnBase,
|
|
753
801
|
getHlsLadder,
|
|
754
802
|
getHlsStreamingUrl,
|
|
@@ -763,6 +811,7 @@ export {
|
|
|
763
811
|
getTransformUrl,
|
|
764
812
|
getVideoTransformUrl,
|
|
765
813
|
hasPreset,
|
|
814
|
+
hasSizeLadder,
|
|
766
815
|
hlsLadderAlignment,
|
|
767
816
|
invalidateSlotCache,
|
|
768
817
|
isRequestablePreset,
|