@transloadit/utils 4.4.1 → 4.5.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 CHANGED
@@ -24,7 +24,11 @@ const verified = await verifyWebhookSignature({
24
24
  ## Node usage
25
25
 
26
26
  ```ts
27
- import { signParamsSync, getSignedSmartCdnUrl } from '@transloadit/utils/node'
27
+ import {
28
+ getSignedSmartCdnImageCandidates,
29
+ getSignedSmartCdnUrl,
30
+ signParamsSync,
31
+ } from '@transloadit/utils/node'
28
32
 
29
33
  const signature = signParamsSync(paramsString, authSecret)
30
34
  const url = getSignedSmartCdnUrl({
@@ -34,6 +38,19 @@ const url = getSignedSmartCdnUrl({
34
38
  authKey,
35
39
  authSecret,
36
40
  })
41
+ const imageCandidates = getSignedSmartCdnImageCandidates({
42
+ authKey,
43
+ authSecret,
44
+ // Reuse one absolute expiry across a build instead of recomputing it per request.
45
+ expiresAt,
46
+ input: 'https://example.com/image.jpg',
47
+ widths: [320, 640, 960],
48
+ workspace,
49
+ })
50
+
51
+ for (const source of imageCandidates.sources) {
52
+ console.log(source.format, source.quality, source.candidates)
53
+ }
37
54
  ```
38
55
 
39
56
  ## API
@@ -42,3 +59,5 @@ const url = getSignedSmartCdnUrl({
42
59
  - `verifyWebhookSignature({ rawBody, signatureHeader, authSecret })`: validates webhook signatures.
43
60
  - `signParamsSync(paramsString, authSecret, algorithm?)`: Node-only sync signature helper.
44
61
  - `getSignedSmartCdnUrl(options)`: Node-only Smart CDN URL signer.
62
+ - `getSignedSmartCdnImageCandidates(options)`: deterministic structured, signed AVIF and WebP
63
+ candidates plus the original fallback URL.
package/dist/node.d.ts CHANGED
@@ -1,6 +1,43 @@
1
1
  import type { SignatureAlgorithm } from './index.ts';
2
2
  export type { SignatureAlgorithm } from './index.ts';
3
3
  export type SignatureAlgorithmInput = SignatureAlgorithm | (string & {});
4
+ /** Image formats supported by the responsive-image Built-in. */
5
+ export type SmartCdnImageFormat = 'avif' | 'png' | 'webp';
6
+ /** One signed Smart CDN rendition at a specific intrinsic width. */
7
+ export interface SmartCdnImageCandidate {
8
+ url: string;
9
+ width: number;
10
+ }
11
+ /** Ordered candidates for one image format and quality. */
12
+ export interface SmartCdnImageSource {
13
+ candidates: readonly SmartCdnImageCandidate[];
14
+ format: SmartCdnImageFormat;
15
+ quality: number;
16
+ }
17
+ /** Structured data for rendering a responsive image. */
18
+ export interface SmartCdnImageCandidates {
19
+ fallbackUrl: string;
20
+ sources: readonly SmartCdnImageSource[];
21
+ }
22
+ /** Options for deterministic, server-generated Smart CDN image candidates. */
23
+ export interface SmartCdnImageCandidatesOptions {
24
+ /** Transloadit auth key used to sign every candidate URL. */
25
+ authKey: string;
26
+ /** Transloadit auth secret used to sign every candidate URL. */
27
+ authSecret: string;
28
+ /** One absolute expiry in milliseconds since UNIX epoch, shared by every candidate. */
29
+ expiresAt: number;
30
+ /** Formats and their quality values. Defaults to AVIF 45 and WebP 75. */
31
+ formats?: Readonly<Partial<Record<SmartCdnImageFormat, number>>>;
32
+ /** Absolute HTTP(S) source URL accepted by the responsive-image Template. */
33
+ input: string;
34
+ /** Compatible Template override. Defaults to `builtin/serve-image@0.0.1`. */
35
+ template?: string;
36
+ /** Up to 32 intrinsic widths. Each value must be an integer from 1 through 8000. */
37
+ widths: readonly number[];
38
+ /** Workspace slug. */
39
+ workspace: string;
40
+ }
4
41
  export type SmartCdnUrlOptions = {
5
42
  /**
6
43
  * Workspace slug.
@@ -34,4 +71,11 @@ export type SmartCdnUrlOptions = {
34
71
  };
35
72
  export declare const signParamsSync: (paramsString: string, authSecret: string, algorithm?: SignatureAlgorithmInput) => string;
36
73
  export declare const getSignedSmartCdnUrl: (opts: SmartCdnUrlOptions) => string;
74
+ /**
75
+ * Builds deterministic signed Smart CDN candidates for server-rendered `<picture>` elements.
76
+ *
77
+ * Width descriptors are only accurate when callers do not request widths above the source image's
78
+ * intrinsic width. The helper deliberately keeps the Built-in in width-only `fit` mode.
79
+ */
80
+ export declare function getSignedSmartCdnImageCandidates(opts: SmartCdnImageCandidatesOptions): SmartCdnImageCandidates;
37
81
  //# sourceMappingURL=node.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAIpD,YAAY,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAEpD,MAAM,MAAM,uBAAuB,GAAG,kBAAkB,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;AAExE,MAAM,MAAM,kBAAkB,GAAG;IAC/B;;OAEG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAA;IACb;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,CAAC,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC,CAAA;IACrF;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;OAEG;IACH,OAAO,EAAE,MAAM,CAAA;IACf;;OAEG;IACH,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,eAAO,MAAM,cAAc,GACzB,cAAc,MAAM,EACpB,YAAY,MAAM,EAClB,YAAW,uBAAkC,KAC5C,MAKF,CAAA;AAED,eAAO,MAAM,oBAAoB,GAAI,MAAM,kBAAkB,KAAG,MA8B/D,CAAA"}
1
+ {"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAIpD,YAAY,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAEpD,MAAM,MAAM,uBAAuB,GAAG,kBAAkB,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;AAExE,gEAAgE;AAChE,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,CAAA;AAEzD,oEAAoE;AACpE,MAAM,WAAW,sBAAsB;IACrC,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;CACd;AAED,2DAA2D;AAC3D,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,SAAS,sBAAsB,EAAE,CAAA;IAC7C,MAAM,EAAE,mBAAmB,CAAA;IAC3B,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,wDAAwD;AACxD,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,SAAS,mBAAmB,EAAE,CAAA;CACxC;AAED,8EAA8E;AAC9E,MAAM,WAAW,8BAA8B;IAC7C,6DAA6D;IAC7D,OAAO,EAAE,MAAM,CAAA;IACf,gEAAgE;IAChE,UAAU,EAAE,MAAM,CAAA;IAClB,uFAAuF;IACvF,SAAS,EAAE,MAAM,CAAA;IACjB,yEAAyE;IACzE,OAAO,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC,CAAC,CAAA;IAChE,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAA;IACb,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,oFAAoF;IACpF,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;IACzB,sBAAsB;IACtB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B;;OAEG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAA;IACb;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,CAAC,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC,CAAA;IACrF;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;OAEG;IACH,OAAO,EAAE,MAAM,CAAA;IACf;;OAEG;IACH,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AAkED,eAAO,MAAM,cAAc,GACzB,cAAc,MAAM,EACpB,YAAY,MAAM,EAClB,YAAW,uBAAkC,KAC5C,MAKF,CAAA;AAED,eAAO,MAAM,oBAAoB,GAAI,MAAM,kBAAkB,KAAG,MA8B/D,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,gCAAgC,CAC9C,IAAI,EAAE,8BAA8B,GACnC,uBAAuB,CAsDzB"}
package/dist/node.js CHANGED
@@ -1,4 +1,56 @@
1
1
  import { createHmac } from 'node:crypto';
2
+ const defaultSmartCdnImageFormats = {
3
+ avif: 45,
4
+ webp: 75,
5
+ };
6
+ const defaultSmartCdnImageTemplate = 'builtin/serve-image@0.0.1';
7
+ const smartCdnImageFormats = ['avif', 'webp', 'png'];
8
+ const smartCdnImageMaxDimension = 8000;
9
+ const smartCdnImageMaxWidths = 32;
10
+ function isSmartCdnImageFormat(value) {
11
+ return value === 'avif' || value === 'png' || value === 'webp';
12
+ }
13
+ function validateSmartCdnImageDimension(width) {
14
+ if (!Number.isInteger(width) || width < 1 || width > smartCdnImageMaxDimension) {
15
+ throw new RangeError(`width must be an integer from 1 through ${smartCdnImageMaxDimension}`);
16
+ }
17
+ }
18
+ function validateSmartCdnImageQuality(quality) {
19
+ if (!Number.isInteger(quality) || quality < 1 || quality > 100) {
20
+ throw new RangeError('quality must be an integer from 1 through 100');
21
+ }
22
+ }
23
+ function validateSmartCdnImageInput(input) {
24
+ if (typeof input !== 'string' || input.trim() !== input || input.includes('|')) {
25
+ throw new TypeError('input must be a single HTTP or HTTPS URL string');
26
+ }
27
+ if (!URL.canParse(input)) {
28
+ throw new TypeError('input must be an HTTP or HTTPS URL');
29
+ }
30
+ const protocol = new URL(input).protocol;
31
+ if (protocol !== 'http:' && protocol !== 'https:') {
32
+ throw new TypeError('input must be an HTTP or HTTPS URL');
33
+ }
34
+ }
35
+ function validateSmartCdnImageFormats(formats) {
36
+ for (const format of Object.keys(formats)) {
37
+ if (!isSmartCdnImageFormat(format)) {
38
+ throw new TypeError(`Unsupported Smart CDN image format: ${format}`);
39
+ }
40
+ }
41
+ let formatCount = 0;
42
+ for (const format of smartCdnImageFormats) {
43
+ const quality = formats[format];
44
+ if (quality == null) {
45
+ continue;
46
+ }
47
+ validateSmartCdnImageQuality(quality);
48
+ formatCount += 1;
49
+ }
50
+ if (formatCount === 0) {
51
+ throw new TypeError('formats must contain at least one value');
52
+ }
53
+ }
2
54
  export const signParamsSync = (paramsString, authSecret, algorithm = 'sha384') => {
3
55
  const signature = createHmac(algorithm, authSecret)
4
56
  .update(Buffer.from(paramsString, 'utf-8'))
@@ -35,3 +87,59 @@ export const getSignedSmartCdnUrl = (opts) => {
35
87
  queryParams.set('sig', `sha256:${signature}`);
36
88
  return `https://${workspaceSlug}.tlcdn.com/${templateSlug}/${inputField}?${queryParams}`;
37
89
  };
90
+ /**
91
+ * Builds deterministic signed Smart CDN candidates for server-rendered `<picture>` elements.
92
+ *
93
+ * Width descriptors are only accurate when callers do not request widths above the source image's
94
+ * intrinsic width. The helper deliberately keeps the Built-in in width-only `fit` mode.
95
+ */
96
+ export function getSignedSmartCdnImageCandidates(opts) {
97
+ if (typeof opts.authKey !== 'string' || opts.authKey === '') {
98
+ throw new TypeError('authKey is required');
99
+ }
100
+ if (typeof opts.authSecret !== 'string' || opts.authSecret === '') {
101
+ throw new TypeError('authSecret is required');
102
+ }
103
+ if (!Number.isSafeInteger(opts.expiresAt) || opts.expiresAt <= 0) {
104
+ throw new RangeError('expiresAt must be a positive safe integer');
105
+ }
106
+ if (!Array.isArray(opts.widths) || opts.widths.length === 0) {
107
+ throw new TypeError('widths must contain at least one value');
108
+ }
109
+ if (opts.widths.length > smartCdnImageMaxWidths) {
110
+ throw new RangeError(`widths must contain at most ${smartCdnImageMaxWidths} values`);
111
+ }
112
+ validateSmartCdnImageInput(opts.input);
113
+ const widths = [...new Set(opts.widths)];
114
+ if (widths.length > smartCdnImageMaxWidths) {
115
+ throw new RangeError(`widths must contain at most ${smartCdnImageMaxWidths} unique values`);
116
+ }
117
+ for (const width of widths) {
118
+ validateSmartCdnImageDimension(width);
119
+ }
120
+ const formats = opts.formats ?? defaultSmartCdnImageFormats;
121
+ validateSmartCdnImageFormats(formats);
122
+ widths.sort((left, right) => left - right);
123
+ const sources = [];
124
+ for (const format of smartCdnImageFormats) {
125
+ const quality = formats[format];
126
+ if (quality == null) {
127
+ continue;
128
+ }
129
+ const candidates = [];
130
+ for (const width of widths) {
131
+ const url = getSignedSmartCdnUrl({
132
+ authKey: opts.authKey,
133
+ authSecret: opts.authSecret,
134
+ expiresAt: opts.expiresAt,
135
+ input: opts.input,
136
+ template: opts.template ?? defaultSmartCdnImageTemplate,
137
+ urlParams: { f: format, q: quality, r: 'fit', w: width },
138
+ workspace: opts.workspace,
139
+ });
140
+ candidates.push({ url, width });
141
+ }
142
+ sources.push({ candidates, format, quality });
143
+ }
144
+ return { fallbackUrl: opts.input, sources };
145
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@transloadit/utils",
3
- "version": "4.4.1",
3
+ "version": "4.5.1",
4
4
  "description": "Transloadit shared utilities",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,9 +27,10 @@
27
27
  "./package.json": "./package.json"
28
28
  },
29
29
  "scripts": {
30
- "lint:ts": "../../node_modules/.bin/tsc --build tsconfig.build.json",
30
+ "lint:ts": "../../node_modules/.bin/tsc --build tsconfig.build.json && ../../node_modules/.bin/tsc --noEmit --project tsconfig.json",
31
31
  "build": "../../node_modules/.bin/tsc --build tsconfig.build.json",
32
- "check": "yarn lint:ts",
32
+ "check": "yarn lint:ts && yarn test:unit",
33
+ "test:unit": "../../node_modules/.bin/vitest run ./test",
33
34
  "prepack": "yarn build"
34
35
  },
35
36
  "devDependencies": {